Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
3c54f1f
add FFI as a requirement for dynamic calls
wlav Jun 1, 2026
0a1ae32
add basic JIT (Numba) test
wlav Jun 1, 2026
38a02c2
initial Numba/JIT support
wlav Jun 1, 2026
8fb7494
clang-format fixes
wlav Jun 1, 2026
fade533
simplify observer insertion code
wlav Jun 1, 2026
fa90040
proper dyncall return type
wlav Jun 2, 2026
879bd53
basic jit test
wlav Jun 2, 2026
8c90768
query -> selector, following the API change
wlav Jun 2, 2026
f5a4625
apparently, numba has Float and float32 and they aren't the same thing
wlav Jun 2, 2026
12cbdca
test all supported JIT types
wlav Jun 2, 2026
9fa2ba1
changes to make clang-tidy happy
wlav Jun 2, 2026
622e7d2
rejig dyncall to use std::variant instead of a union, per clang-tidy
wlav Jun 3, 2026
35d4fed
clang-format fixes
wlav Jun 3, 2026
e4e696f
clang-tidy workarounds
wlav Jun 3, 2026
2f1d501
try again to make clang-tidy shut up
wlav Jun 3, 2026
ea7dff7
Apply header-guards fixes
github-actions[bot] Jun 3, 2026
c6d6874
Merge branch 'numba-support' of github.com:wlav/phlex into numba-support
wlav Jun 3, 2026
913391c
ruff fix
wlav Jun 3, 2026
9e2d06c
Merge branch 'main' into numba-support
wlav Jun 12, 2026
81d44e7
Merge branch 'main' into numba-support
wlav Jun 18, 2026
74dc348
Revert "Introduce layer_path class (#640)"
wlav Jun 18, 2026
f3a101c
Revert "Revert "Introduce layer_path class (#640)""
wlav Jun 18, 2026
4c4741b
move adding of properties to the end to ensure test is defined first
wlav Jun 18, 2026
90cc0ed
add support for Numba-jited observers
wlav Jun 18, 2026
7b919b9
properly NULL pointers in callback move operators
wlav Jun 18, 2026
6436017
Merge branch 'main' into numba-support
wlav Jun 22, 2026
9bf1f5b
address formatting issues
wlav Jun 22, 2026
29ce5f3
gersemi format fix
wlav Jun 22, 2026
af07260
from coderabbit: spelling errors in comments and missing decrefs
wlav Jun 22, 2026
6829604
from coderabbit: make get() const
wlav Jun 22, 2026
025390c
make clang-tidy happy by adding some dead writes
wlav Jun 24, 2026
36288a6
Merge branch 'main' into numba-support
wlav Jun 26, 2026
51de0fe
use cfunc from the top-level module instead of the decorators one
wlav Jun 26, 2026
de38075
Revert "move adding of properties to the end to ensure test is define…
wlav Jun 26, 2026
c6c0121
move setting of py:phlex properties until after it has been created
wlav Jun 26, 2026
701fb79
check for missing output suffixes on transform and report a proper er…
wlav Jun 26, 2026
89b1951
pass transform concurrency on to the converter nodes
wlav Jun 26, 2026
cafb828
verify conversion result in python providers
wlav Jun 26, 2026
fe5d983
delete py_callback_base move constructor/assignment
wlav Jun 26, 2026
224eefd
coding conventions
wlav Jun 26, 2026
73f52ec
fix shadowing of ffi_type
wlav Jun 26, 2026
1e008e3
coding convention
wlav Jun 26, 2026
5429b2d
add missing decref
wlav Jun 26, 2026
d61f52d
improve reporting of conversion errors
wlav Jun 26, 2026
356718d
temporary "fix" to make the AI happy; to be removed after release
wlav Jun 26, 2026
a0fb468
guarantee that a path that is supposed to fail actually does
wlav Jun 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion plugins/python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ if(Python_NumPy_VERSION VERSION_LESS "2.0.0")
)
endif()

find_package(PkgConfig REQUIRED)
pkg_check_modules(FFI REQUIRED IMPORTED_TARGET libffi)
Comment thread
wlav marked this conversation as resolved.

# Phlex module to run Python algorithms
add_library(
pymodule
Expand All @@ -17,8 +20,13 @@ add_library(
src/dciwrap.cpp
src/lifelinewrap.cpp
src/errorwrap.cpp
src/dyncall.cpp
)

target_link_libraries(
pymodule
PRIVATE phlex::module phlex::source PkgConfig::FFI Python::Python Python::NumPy
)
target_link_libraries(pymodule PRIVATE phlex::module phlex::source Python::Python Python::NumPy)
target_compile_definitions(pymodule PRIVATE NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION)

install(TARGETS pymodule LIBRARY DESTINATION lib)
Expand Down
29 changes: 28 additions & 1 deletion plugins/python/python/phlex/_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@

import numpy as np

try:
import numba.core.types as nb_types
has_numba = True
except ImportError:
has_numba = False

__all__ = [
"normalize_type",
]

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

if has_numba:
_PY2CPP.update({
nb_types.bool: "bool",
nb_types.int8: "int8_t",
nb_types.int16: "int16_t",
nb_types.int32: "int32_t",
nb_types.int64: "int64_t",
nb_types.uint8: "uint8_t",
nb_types.uint16: "uint16_t",
nb_types.uint32: "uint32_t",
nb_types.uint64: "uint64_t",
nb_types.Float: "float",
nb_types.float32: "float",
nb_types.double: "double",
nb_types.void: "None",
})

# ctypes types that don't map cleanly to intN_t / uintN_t
_CTYPES_SPECIAL: dict[type, str] = {}
for _attr, _cpp in [
Expand Down Expand Up @@ -96,8 +120,11 @@ def _build_ctypes_map() -> dict[type, str]:
"unsigned int": _PY2CPP[ctypes.c_uint],
"long": _PY2CPP[ctypes.c_long],
"unsigned long": _PY2CPP[ctypes.c_ulong],
# special cases; not necessarily correct but as expected on major platforms
"long long": "int64_t",
"unsigned long long": "uint64_t",
"float32": "float",
"float64": "double",
}


Expand Down
129 changes: 129 additions & 0 deletions plugins/python/src/dyncall.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Dynamic dispatcher from generically packaged args to any C or Python function.
//
// Note: this particular implementation is based on libffi, presumed to be for
// now the minimal dependency, but an alternative could be based on JITing
// using Cling or even Numba's llvmlite.

#include "dyncall.hpp"
#include <stdexcept>

#include <ffi.h>

using namespace phlex::experimental;

phlex::experimental::dcarg phlex::experimental::dcarg::from_str(std::string const& stype)
{
// only types currently used in modulewrap are added, not all ffi types
if (stype == "bool")
return dcarg(false);
else if (stype == "int32_t")
return dcarg(static_cast<std::int32_t>(0));
else if (stype == "uint32_t")
return dcarg(static_cast<std::uint32_t>(0));
else if (stype == "int64_t")
return dcarg(static_cast<ph_long_t>(0));
else if (stype == "uint64_t")
return dcarg(static_cast<ph_ulong_t>(0));
else if (stype == "float")
return dcarg(0.0f);
else if (stype == "double")
return dcarg(0.0);
else if (stype == "void")
return dcarg{};

throw std::invalid_argument("unknown type string: " + stype);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

void* phlex::experimental::dcarg::value_ptr()
{
return std::visit(
[](auto& val) -> void* {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, std::monostate>) {
return nullptr;
} else {
return static_cast<void*>(&val);
}
},
m_value);
}

namespace {
static ffi_type* get_ffi_type(dcarg const& d)
{
return std::visit(
[](auto&& val) -> ffi_type* {
using T = std::decay_t<decltype(val)>;

// there are duplicate bodies here b/c bool is represented by uint8,
// just as uint8 is, there being no bool in C; the code is cleaner
// with each type on its own line, however, rather than combining the
// two in a single predicate as a special case
// NOLINTBEGIN(bugprone-branch-clone)
if constexpr (std::is_same_v<T, std::monostate>)
return &ffi_type_void;
else if constexpr (std::is_same_v<T, void*>)
return &ffi_type_pointer;
else if constexpr (std::is_same_v<T, bool>)
return &ffi_type_uint8;
else if constexpr (std::is_same_v<T, std::int8_t>)
return &ffi_type_sint8;
else if constexpr (std::is_same_v<T, std::uint8_t>)
return &ffi_type_uint8;
else if constexpr (std::is_same_v<T, std::int16_t>)
return &ffi_type_sint16;
else if constexpr (std::is_same_v<T, std::uint16_t>)
return &ffi_type_uint16;
else if constexpr (std::is_same_v<T, std::int32_t>)
return &ffi_type_sint32;
else if constexpr (std::is_same_v<T, std::uint32_t>)
return &ffi_type_uint32;
else if constexpr (std::is_same_v<T, ph_long_t>)
return &ffi_type_sint64;
else if constexpr (std::is_same_v<T, ph_ulong_t>)
return &ffi_type_uint64;
else if constexpr (std::is_same_v<T, float>)
return &ffi_type_float;
else if constexpr (std::is_same_v<T, double>)
return &ffi_type_double;
// NOLINTEND(bugprone-branch-clone)
},
d.m_value);
}
}

void phlex::experimental::dyncall(void* fn, dcarg& result, dcargs_t& args, int var_offset)
{
// Perform a dynamic call of function fn, taking arguments `args` and returning
// `result`. Set `var_offset` to the appropriate number of positional arguments
// if the other arguments are variational.

// Except for the memory management unique_ptrs, this code is essentially C,
// because libffi is, and that yields a plethora of warnings from clang-tidy,
// none of which warrant actual changes.
// NOLINTBEGIN
std::size_t nargs = (std::size_t)args.size();

auto t = std::make_unique<ffi_type*[]>(nargs);
auto p = std::make_unique<void*[]>(nargs);

for (dcargs_t::size_type i = 0; i < nargs; ++i) {
auto& a = args[i];
t[i] = get_ffi_type(a);
p[i] = a.value_ptr();
}

ffi_cif cif;
ffi_status status;
if (0 < var_offset)
status =
ffi_prep_cif_var(&cif, FFI_DEFAULT_ABI, var_offset, nargs, get_ffi_type(result), t.get());
else
status = ffi_prep_cif(&cif, FFI_DEFAULT_ABI, nargs, get_ffi_type(result), t.get());

if (status)
throw std::runtime_error("ffi prep failed");

ffi_call(&cif, (void (*)())fn, result.value_ptr(), p.get());
// NOLINTEND
}
98 changes: 98 additions & 0 deletions plugins/python/src/dyncall.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#ifndef PLUGINS_PYTHON_SRC_DYNCALL_HPP
#define PLUGINS_PYTHON_SRC_DYNCALL_HPP

// =======================================================================================
//
// Dynamic dispatcher from generically packaged args to any C or Python function.
//
// Design rationale
// ================
//
// Python code is inserted in the Phlex execution graph using generic types to avoid a
// combinatorial explosion of types. This way, all template instantiations can be done at
// compile time. Callback wrappers are then needed to either pack from generic to Python
// or to unpack from generic to C/C++ and perform the call. This dynamic dispatcher
// provides that functionality.
//
// =======================================================================================

#include "Python.h" // for PyObject* get<> specialization only

#include <cstdint>
#include <memory>
#include <string>
#include <variant>
#include <vector>
Comment thread
wlav marked this conversation as resolved.

#if defined(__APPLE__) && defined(__MACH__)
// This is a temporary workaround until we have a solution for handling translation of types
// between C++ and Python.
typedef long ph_long_t;
typedef unsigned long ph_ulong_t;
#else
typedef std::int64_t ph_long_t;
typedef std::uint64_t ph_ulong_t;
#endif
Comment thread
wlav marked this conversation as resolved.

namespace phlex::experimental {

struct dcarg {
using ffi_variant_type = std::variant<std::monostate, // void (default)
void*,
bool,
std::int8_t,
std::uint8_t,
std::int16_t,
std::uint16_t,
std::int32_t,
std::uint32_t,
ph_long_t,
ph_ulong_t,
float,
double>;

ffi_variant_type m_value;

// convenience mapper of human-readable string to dcarg
static dcarg from_str(std::string const& stype);

// factory-style constructors to guarantee value/type match
dcarg() : m_value(std::monostate{}) {}
explicit dcarg(void* v) : m_value(v) {}
explicit dcarg(bool v) : m_value(v) {}
explicit dcarg(std::int8_t v) : m_value(v) {}
explicit dcarg(std::uint8_t v) : m_value(v) {}
explicit dcarg(std::int16_t v) : m_value(v) {}
explicit dcarg(std::uint16_t v) : m_value(v) {}
explicit dcarg(std::int32_t v) : m_value(v) {}
explicit dcarg(std::uint32_t v) : m_value(v) {}
explicit dcarg(ph_long_t v) : m_value(v) {}
explicit dcarg(ph_ulong_t v) : m_value(v) {}
explicit dcarg(float v) : m_value(v) {}
explicit dcarg(double v) : m_value(v) {}

// pointer access to payload
void* value_ptr();

// value access to payload
template <typename T>
T get() const
{
return std::get<T>(m_value);
}
};
Comment thread
wlav marked this conversation as resolved.

// specialization to simplify a very common case
template <>
inline PyObject* dcarg::get<PyObject*>() const
{
return reinterpret_cast<PyObject*>(std::get<void*>(m_value));
}

typedef std::vector<dcarg> dcargs_t;

void dyncall(void* fn, dcarg& result, dcargs_t& args, int var_offset = -1);

} // phlex::experimental

#endif // PLUGINS_PYTHON_SRC_DYNCALL_HPP
Loading
Loading