Skip to content

Commit 7e024cc

Browse files
Initial Numba support (#626)
* add FFI as a requirement for dynamic calls * add basic JIT (Numba) test * initial Numba/JIT support * proper dyncall return type * basic jit test * apparently, numba has Float and float32 and they aren't the same thing * test all supported JIT types * move adding of properties to the end to ensure test is defined first * add support for Numba-jited observers * properly NULL pointers in callback move operators * use cfunc from the top-level module instead of the decorators one * move setting of py:phlex properties until after it has been created * check for missing output suffixes on transform and report a proper error if it does * pass transform concurrency on to the converter nodes * verify conversion result in python providers * delete py_callback_base move constructor/assignment * fix shadowing of ffi_type * add missing decref * improve reporting of conversion errors * temporary "fix" to make the AI happy; to be removed after release * guarantee that a path that is supposed to fail actually does --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 7409297 commit 7e024cc

9 files changed

Lines changed: 792 additions & 304 deletions

File tree

plugins/python/CMakeLists.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ if(Python_NumPy_VERSION VERSION_LESS "2.0.0")
77
)
88
endif()
99

10+
find_package(PkgConfig REQUIRED)
11+
pkg_check_modules(FFI REQUIRED IMPORTED_TARGET libffi)
12+
1013
# Phlex module to run Python algorithms
1114
add_library(
1215
pymodule
@@ -17,8 +20,13 @@ add_library(
1720
src/dciwrap.cpp
1821
src/lifelinewrap.cpp
1922
src/errorwrap.cpp
23+
src/dyncall.cpp
24+
)
25+
26+
target_link_libraries(
27+
pymodule
28+
PRIVATE phlex::module phlex::source PkgConfig::FFI Python::Python Python::NumPy
2029
)
21-
target_link_libraries(pymodule PRIVATE phlex::module phlex::source Python::Python Python::NumPy)
2230
target_compile_definitions(pymodule PRIVATE NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION)
2331

2432
install(TARGETS pymodule LIBRARY DESTINATION lib)

plugins/python/python/phlex/_typing.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,18 @@
1414

1515
import numpy as np
1616

17+
try:
18+
import numba.core.types as nb_types
19+
has_numba = True
20+
except ImportError:
21+
has_numba = False
22+
1723
__all__ = [
1824
"normalize_type",
1925
]
2026

21-
# ctypes and numpy types are likely candidates for use in annotations
27+
# ctypes and numpy types are likely candidates for use in annotations; Numba
28+
# types may appear from callback signatures
2229
# TODO: should users be allowed to add to these?
2330
_PY2CPP: dict[type, str] = {
2431
# numpy types
@@ -40,6 +47,23 @@
4047
# np.uintp: "size_t",
4148
}
4249

50+
if has_numba:
51+
_PY2CPP.update({
52+
nb_types.bool: "bool",
53+
nb_types.int8: "int8_t",
54+
nb_types.int16: "int16_t",
55+
nb_types.int32: "int32_t",
56+
nb_types.int64: "int64_t",
57+
nb_types.uint8: "uint8_t",
58+
nb_types.uint16: "uint16_t",
59+
nb_types.uint32: "uint32_t",
60+
nb_types.uint64: "uint64_t",
61+
nb_types.Float: "float",
62+
nb_types.float32: "float",
63+
nb_types.double: "double",
64+
nb_types.void: "None",
65+
})
66+
4367
# ctypes types that don't map cleanly to intN_t / uintN_t
4468
_CTYPES_SPECIAL: dict[type, str] = {}
4569
for _attr, _cpp in [
@@ -96,8 +120,11 @@ def _build_ctypes_map() -> dict[type, str]:
96120
"unsigned int": _PY2CPP[ctypes.c_uint],
97121
"long": _PY2CPP[ctypes.c_long],
98122
"unsigned long": _PY2CPP[ctypes.c_ulong],
123+
# special cases; not necessarily correct but as expected on major platforms
99124
"long long": "int64_t",
100125
"unsigned long long": "uint64_t",
126+
"float32": "float",
127+
"float64": "double",
101128
}
102129

103130

plugins/python/src/dyncall.cpp

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Dynamic dispatcher from generically packaged args to any C or Python function.
2+
//
3+
// Note: this particular implementation is based on libffi, presumed to be for
4+
// now the minimal dependency, but an alternative could be based on JITing
5+
// using Cling or even Numba's llvmlite.
6+
7+
#include "dyncall.hpp"
8+
#include <stdexcept>
9+
10+
#include <ffi.h>
11+
12+
using namespace phlex::experimental;
13+
14+
phlex::experimental::dcarg phlex::experimental::dcarg::from_str(std::string const& stype)
15+
{
16+
// only types currently used in modulewrap are added, not all ffi types
17+
if (stype == "bool")
18+
return dcarg(false);
19+
else if (stype == "int32_t")
20+
return dcarg(static_cast<std::int32_t>(0));
21+
else if (stype == "uint32_t")
22+
return dcarg(static_cast<std::uint32_t>(0));
23+
else if (stype == "int64_t")
24+
return dcarg(static_cast<ph_long_t>(0));
25+
else if (stype == "uint64_t")
26+
return dcarg(static_cast<ph_ulong_t>(0));
27+
else if (stype == "float")
28+
return dcarg(0.0f);
29+
else if (stype == "double")
30+
return dcarg(0.0);
31+
else if (stype == "void")
32+
return dcarg{};
33+
34+
throw std::invalid_argument("unknown type string: " + stype);
35+
}
36+
37+
void* phlex::experimental::dcarg::value_ptr()
38+
{
39+
return std::visit(
40+
[](auto& val) -> void* {
41+
using T = std::decay_t<decltype(val)>;
42+
if constexpr (std::is_same_v<T, std::monostate>) {
43+
return nullptr;
44+
} else {
45+
return static_cast<void*>(&val);
46+
}
47+
},
48+
m_value);
49+
}
50+
51+
namespace {
52+
static ffi_type* get_ffi_type(dcarg const& d)
53+
{
54+
return std::visit(
55+
[](auto&& val) -> ffi_type* {
56+
using T = std::decay_t<decltype(val)>;
57+
58+
// there are duplicate bodies here b/c bool is represented by uint8,
59+
// just as uint8 is, there being no bool in C; the code is cleaner
60+
// with each type on its own line, however, rather than combining the
61+
// two in a single predicate as a special case
62+
// NOLINTBEGIN(bugprone-branch-clone)
63+
if constexpr (std::is_same_v<T, std::monostate>)
64+
return &ffi_type_void;
65+
else if constexpr (std::is_same_v<T, void*>)
66+
return &ffi_type_pointer;
67+
else if constexpr (std::is_same_v<T, bool>)
68+
return &ffi_type_uint8;
69+
else if constexpr (std::is_same_v<T, std::int8_t>)
70+
return &ffi_type_sint8;
71+
else if constexpr (std::is_same_v<T, std::uint8_t>)
72+
return &ffi_type_uint8;
73+
else if constexpr (std::is_same_v<T, std::int16_t>)
74+
return &ffi_type_sint16;
75+
else if constexpr (std::is_same_v<T, std::uint16_t>)
76+
return &ffi_type_uint16;
77+
else if constexpr (std::is_same_v<T, std::int32_t>)
78+
return &ffi_type_sint32;
79+
else if constexpr (std::is_same_v<T, std::uint32_t>)
80+
return &ffi_type_uint32;
81+
else if constexpr (std::is_same_v<T, ph_long_t>)
82+
return &ffi_type_sint64;
83+
else if constexpr (std::is_same_v<T, ph_ulong_t>)
84+
return &ffi_type_uint64;
85+
else if constexpr (std::is_same_v<T, float>)
86+
return &ffi_type_float;
87+
else if constexpr (std::is_same_v<T, double>)
88+
return &ffi_type_double;
89+
// NOLINTEND(bugprone-branch-clone)
90+
},
91+
d.m_value);
92+
}
93+
}
94+
95+
void phlex::experimental::dyncall(void* fn, dcarg& result, dcargs_t& args, int var_offset)
96+
{
97+
// Perform a dynamic call of function fn, taking arguments `args` and returning
98+
// `result`. Set `var_offset` to the appropriate number of positional arguments
99+
// if the other arguments are variational.
100+
101+
// Except for the memory management unique_ptrs, this code is essentially C,
102+
// because libffi is, and that yields a plethora of warnings from clang-tidy,
103+
// none of which warrant actual changes.
104+
// NOLINTBEGIN
105+
std::size_t nargs = (std::size_t)args.size();
106+
107+
auto t = std::make_unique<ffi_type*[]>(nargs);
108+
auto p = std::make_unique<void*[]>(nargs);
109+
110+
for (dcargs_t::size_type i = 0; i < nargs; ++i) {
111+
auto& a = args[i];
112+
t[i] = get_ffi_type(a);
113+
p[i] = a.value_ptr();
114+
}
115+
116+
ffi_cif cif;
117+
ffi_status status;
118+
if (0 < var_offset)
119+
status =
120+
ffi_prep_cif_var(&cif, FFI_DEFAULT_ABI, var_offset, nargs, get_ffi_type(result), t.get());
121+
else
122+
status = ffi_prep_cif(&cif, FFI_DEFAULT_ABI, nargs, get_ffi_type(result), t.get());
123+
124+
if (status)
125+
throw std::runtime_error("ffi prep failed");
126+
127+
ffi_call(&cif, (void (*)())fn, result.value_ptr(), p.get());
128+
// NOLINTEND
129+
}

plugins/python/src/dyncall.hpp

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#ifndef PLUGINS_PYTHON_SRC_DYNCALL_HPP
2+
#define PLUGINS_PYTHON_SRC_DYNCALL_HPP
3+
4+
// =======================================================================================
5+
//
6+
// Dynamic dispatcher from generically packaged args to any C or Python function.
7+
//
8+
// Design rationale
9+
// ================
10+
//
11+
// Python code is inserted in the Phlex execution graph using generic types to avoid a
12+
// combinatorial explosion of types. This way, all template instantiations can be done at
13+
// compile time. Callback wrappers are then needed to either pack from generic to Python
14+
// or to unpack from generic to C/C++ and perform the call. This dynamic dispatcher
15+
// provides that functionality.
16+
//
17+
// =======================================================================================
18+
19+
#include "Python.h" // for PyObject* get<> specialization only
20+
21+
#include <cstdint>
22+
#include <memory>
23+
#include <string>
24+
#include <variant>
25+
#include <vector>
26+
27+
#if defined(__APPLE__) && defined(__MACH__)
28+
// This is a temporary workaround until we have a solution for handling translation of types
29+
// between C++ and Python.
30+
typedef long ph_long_t;
31+
typedef unsigned long ph_ulong_t;
32+
#else
33+
typedef std::int64_t ph_long_t;
34+
typedef std::uint64_t ph_ulong_t;
35+
#endif
36+
37+
namespace phlex::experimental {
38+
39+
struct dcarg {
40+
using ffi_variant_type = std::variant<std::monostate, // void (default)
41+
void*,
42+
bool,
43+
std::int8_t,
44+
std::uint8_t,
45+
std::int16_t,
46+
std::uint16_t,
47+
std::int32_t,
48+
std::uint32_t,
49+
ph_long_t,
50+
ph_ulong_t,
51+
float,
52+
double>;
53+
54+
ffi_variant_type m_value;
55+
56+
// convenience mapper of human-readable string to dcarg
57+
static dcarg from_str(std::string const& stype);
58+
59+
// factory-style constructors to guarantee value/type match
60+
dcarg() : m_value(std::monostate{}) {}
61+
explicit dcarg(void* v) : m_value(v) {}
62+
explicit dcarg(bool v) : m_value(v) {}
63+
explicit dcarg(std::int8_t v) : m_value(v) {}
64+
explicit dcarg(std::uint8_t v) : m_value(v) {}
65+
explicit dcarg(std::int16_t v) : m_value(v) {}
66+
explicit dcarg(std::uint16_t v) : m_value(v) {}
67+
explicit dcarg(std::int32_t v) : m_value(v) {}
68+
explicit dcarg(std::uint32_t v) : m_value(v) {}
69+
explicit dcarg(ph_long_t v) : m_value(v) {}
70+
explicit dcarg(ph_ulong_t v) : m_value(v) {}
71+
explicit dcarg(float v) : m_value(v) {}
72+
explicit dcarg(double v) : m_value(v) {}
73+
74+
// pointer access to payload
75+
void* value_ptr();
76+
77+
// value access to payload
78+
template <typename T>
79+
T get() const
80+
{
81+
return std::get<T>(m_value);
82+
}
83+
};
84+
85+
// specialization to simplify a very common case
86+
template <>
87+
inline PyObject* dcarg::get<PyObject*>() const
88+
{
89+
return reinterpret_cast<PyObject*>(std::get<void*>(m_value));
90+
}
91+
92+
typedef std::vector<dcarg> dcargs_t;
93+
94+
void dyncall(void* fn, dcarg& result, dcargs_t& args, int var_offset = -1);
95+
96+
} // phlex::experimental
97+
98+
#endif // PLUGINS_PYTHON_SRC_DYNCALL_HPP

0 commit comments

Comments
 (0)