Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions bindings/pyroot/cppjit/python/cppjit/ll.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"array_new",
"array_delete",
"bind_value",
"as_std_any",
"signals_as_exception",
"set_signals_as_exception",
"FatalError",
Expand Down Expand Up @@ -71,6 +72,20 @@ def bind_value(type_name, address, dims=None):
return cppjit._backend.bind_value(type_name, address, dims)


def as_std_any(obj):
"""Wrap a reference to the Python object `obj` in a `std::any`.

For C++ code that has to keep `obj` alive, such as a data source reading
from the memory of a NumPy array: the `std::any`, and each copy that C++
makes of it, holds a reference to `obj`, which is released when that copy
is destroyed. The reference count is changed with the GIL taken, so C++
may do this from any thread.

keepalive = ll.as_std_any(array)
"""
return cppjit._backend.as_std_any(obj)


# import low-level python converters
for _name in ["addressof", "as_cobject", "as_capsule", "as_ctypes", "as_memoryview"]:
try:
Expand Down
2 changes: 1 addition & 1 deletion bindings/pyroot/cppjit/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ target_compile_definitions(cppjit_backend PRIVATE
# through the dispatch mechanism. Disable them in this translation unit.
NDEBUG
# libcpyrt owns the dispatch table (cppinterop_dispatch.cxx); consumers in
# other DLLs (ROOTPythonizations, tpython) import it. On Windows the data
# other DLLs (tpython) import it. On Windows the data
# symbols need explicit dllexport - WINDOWS_EXPORT_ALL_SYMBOLS covers
# functions only.
CPPINTEROP_DISPATCH_EXPORTS
Expand Down
6 changes: 5 additions & 1 deletion bindings/pyroot/cppjit/src/cpyrt/API.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,12 @@ bool cpyrt::Overload_CheckExact(PyObject* pyobject) {
}

//-----------------------------------------------------------------------------
void cpyrt::Instance_SetReduceMethod(PyCFunction reduceMethod) {
void cpyrt::Instance_SetReduceMethod(PyObject* reduceMethod) {
PythonGILRAII python_gil_raii;
Py_XINCREF(reduceMethod);
PyObject* previous = CPPInstance::ReduceMethod();
CPPInstance::ReduceMethod() = reduceMethod;
Py_XDECREF(previous);
}

//- access to the python interpreter ----------------------------------------
Expand Down
5 changes: 3 additions & 2 deletions bindings/pyroot/cppjit/src/cpyrt/API.h
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,9 @@ CPYRT_EXTERN bool Overload_CheckExact(PyObject* pyobject);

// Sets the __reduce__ method for the CPPInstance class, which is by default not
// implemented by cppjit but might make sense to implement by frameworks that
// support IO of arbitrary C++ objects, like ROOT.
CPYRT_EXTERN void Instance_SetReduceMethod(PyCFunction reduceMethod);
// support IO of arbitrary C++ objects, like ROOT. reduceMethod is a Python
// callable taking the instance, or nullptr to unset it.
CPYRT_EXTERN void Instance_SetReduceMethod(PyObject* reduceMethod);

//- access to the python interpreter ----------------------------------------

Expand Down
10 changes: 5 additions & 5 deletions bindings/pyroot/cppjit/src/cpyrt/CPPInstance.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -412,18 +412,18 @@ static PySequenceMethods op_as_sequence = {
0, // sq_inplace_repeat
};

PyCFunction& CPPInstance::ReduceMethod() {
static PyCFunction reducer = nullptr;
PyObject*& CPPInstance::ReduceMethod() {
static PyObject* reducer = nullptr;
return reducer;
}

PyObject* op_reduce(PyObject* self, PyObject* args) {
auto& reducer = CPPInstance::ReduceMethod();
PyObject* op_reduce(PyObject* self, PyObject* /* args */) {
PyObject* reducer = CPPInstance::ReduceMethod();
if (!reducer) {
PyErr_SetString(PyExc_NotImplementedError, "");
return nullptr;
}
return reducer(self, args);
return PyObject_CallFunctionObjArgs(reducer, self, nullptr);
}

//----------------------------------------------------------------------------
Expand Down
8 changes: 4 additions & 4 deletions bindings/pyroot/cppjit/src/cpyrt/CPPInstance.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ class CPPInstance {
void CastToArray(Py_ssize_t sz);
Py_ssize_t ArrayLength();

// implementation of the __reduce__ method: doesn't wrap any function by
// default but can be re-assigned by libraries that add C++ object
// serialization support, like ROOT
static PyCFunction& ReduceMethod();
// implementation of the __reduce__ method: a Python callable taking the
// instance, unset by default but can be set by libraries that add C++
// object serialization support, like ROOT
static PyObject*& ReduceMethod();

private:
void CreateExtension();
Expand Down
16 changes: 10 additions & 6 deletions bindings/pyroot/cppjit/src/cpyrt/Executors.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -1093,14 +1093,18 @@ cpyrt::Executor* cpyrt::CreateExecutor(interop::TCppType_t type, cdims_t dims) {
result = new InstancePtrExecutor(klass);
} else if (realTypeStr.find("(*)") != std::string::npos ||
(realTypeStr.find("::*)") != std::string::npos)) {
// this is a function pointer
// this is a function pointer; the executor compiles the return type and
// signature, so take them from the type as spelled: realTypeStr has its
// spaces removed for the lookups above, which turns e.g. "const char"
// into "constchar"
// TODO: find better way of finding the type
auto pos1 = realTypeStr.find('(');
auto pos2 = realTypeStr.find("*)");
auto pos3 = realTypeStr.rfind(')');
const std::string fnTypeStr = interop::GetTypeAsString(resolvedType);
auto pos1 = fnTypeStr.find('(');
auto pos2 = fnTypeStr.find("*)");
auto pos3 = fnTypeStr.rfind(')');
result = new FunctionPointerExecutor(
realTypeStr.substr(0, pos1),
realTypeStr.substr(pos2 + 2, pos3 - pos2 - 1));
fnTypeStr.substr(0, pos1),
fnTypeStr.substr(pos2 + 2, pos3 - pos2 - 1));
} else {
// unknown: void* may work ("user knows best"), void will fail on use of
// return value
Expand Down
197 changes: 197 additions & 0 deletions bindings/pyroot/cppjit/src/cpyrt/cpyrtModule.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ PyObject* Instance_FromVoidPtr(void* addr, const std::string& classname,

// Standard
#include <algorithm>
#include <any>
#include <iostream>
#include <map>
#include <sstream>
Expand Down Expand Up @@ -666,6 +667,191 @@ static PyObject* BindObject(PyObject*, PyObject* args, PyObject* kwds) {
return BindCppObjectNoCast(addr, cast_type);
}

//----------------------------------------------------------------------------
static PyObject* SetReduceMethod(PyObject*, PyObject* reducer) {
// Set the __reduce__ method of all instances; None unsets it.
if (reducer == Py_None)
reducer = nullptr;
else if (!PyCallable_Check(reducer)) {
PyErr_SetString(PyExc_TypeError, "reduce method must be callable or None");
return nullptr;
}
Py_XINCREF(reducer);
PyObject* previous = CPPInstance::ReduceMethod();
CPPInstance::ReduceMethod() = reducer;
Py_XDECREF(previous);
Py_RETURN_NONE;
}

//----------------------------------------------------------------------------
namespace {
// A reference to a Python object, held by C++. Copies hold references of
// their own. C++ may copy or destroy it on any thread, so it takes the GIL to
// change the reference count; once Python is finalized it can't, and leaves
// the reference be.
class PyObjectRef {
PyObject* fObject;

public:
// called from Python, with the GIL held
explicit PyObjectRef(PyObject* object) : fObject(object) {
Py_INCREF(fObject);
}
PyObjectRef(const PyObjectRef& other) : fObject(other.fObject) {
if (Py_IsInitialized()) {
cpyrt::PythonGILRAII python_gil_raii;
Py_INCREF(fObject);
}
}
PyObjectRef(PyObjectRef&& other) noexcept : fObject(other.fObject) {
other.fObject = nullptr;
}
PyObjectRef& operator=(PyObjectRef other) noexcept {
std::swap(fObject, other.fObject);
return *this;
}
~PyObjectRef() {
if (fObject && Py_IsInitialized()) {
cpyrt::PythonGILRAII python_gil_raii;
Py_DECREF(fObject);
}
}
};
} // unnamed namespace

static PyObject* AsStdAny(PyObject*, PyObject* pyobject) {
// Wrap a reference to pyobject in a std::any owned by Python.
return cpyrt::Instance_FromVoidPtr(
new std::any{std::in_place_type<PyObjectRef>, pyobject}, "std::any",
/*python_owns=*/true);
}

//----------------------------------------------------------------------------
namespace {

PyObject* gWarningCategory = nullptr; // nullptr: RuntimeWarning
std::string gWarningModule;

bool WarningHandler(const char* location, const char* msg) {
// Issue a Python warning for a warning that C++ reports, see
// warning_handler() below. Called from C++, maybe without the GIL.
cpyrt::PythonGILRAII gil;

// Python code can't run while an exception is pending, e.g. a previous
// warning that the filters turned into an error during the same call into
// C++: decline, so that C++ reports the warning its own way.
if (PyErr_Occurred())
return false;

// Prints or, per the filters, raises; a raised exception stays pending
// until the call into C++ returns to Python.
PyErr_WarnExplicit(gWarningCategory, msg, location, 0,
gWarningModule.empty() ? nullptr : gWarningModule.c_str(),
nullptr);
return true;
}

} // unnamed namespace

static PyObject* GetWarningHandler(PyObject*, PyObject* args, PyObject* kwds) {
// Configure the compiled warning handler and hand it out as a ctypes
// function pointer, for C++ libraries that report warnings to a callback.
static const char* kwlist[] = {(char*)"category", (char*)"module", nullptr};

PyObject* category = nullptr;
const char* module = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oz:warning_handler",
(char**)kwlist, &category, &module))
return nullptr;

if (category == Py_None)
category = nullptr;
if (category && PyObject_IsSubclass(category, PyExc_Warning) != 1) {
if (!PyErr_Occurred())
PyErr_SetString(PyExc_TypeError,
"category must be a subclass of Warning");
return nullptr;
}

Py_XINCREF(category);
Py_XSETREF(gWarningCategory, category);
gWarningModule = module ? module : "";

// ctypes.CFUNCTYPE(c_bool, c_char_p, c_char_p)(address)
PyObject* ctmod = PyImport_ImportModule("ctypes");
if (!ctmod)
return nullptr;
PyObject* c_bool = PyObject_GetAttrString(ctmod, "c_bool");
PyObject* c_char_p = PyObject_GetAttrString(ctmod, "c_char_p");
PyObject* fntype = nullptr;
if (c_bool && c_char_p)
fntype = PyObject_CallMethod(ctmod, "CFUNCTYPE", "OOO", c_bool, c_char_p,
c_char_p);
Py_XDECREF(c_char_p);
Py_XDECREF(c_bool);
Py_DECREF(ctmod);
if (!fntype)
return nullptr;

PyObject* address = PyLong_FromVoidPtr((void*)&WarningHandler);
PyObject* result = PyObject_CallOneArg(fntype, address);
Py_DECREF(address);
Py_DECREF(fntype);
return result;
}

//----------------------------------------------------------------------------
namespace {

void (*gInputHookFunc)() = nullptr;
int (*gPreviousInputHook)() = nullptr;

int InputHook() {
// The PyOS_InputHook set_input_hook() installs: calls the C++ function
// with the GIL, then the previous hook. Runs no Python code, so a Ctrl-C
// arriving meanwhile stays pending for the prompt.
{
cpyrt::PythonGILRAII gil;
gInputHookFunc();
}
return gPreviousInputHook ? gPreviousInputHook() : 0;
}

} // unnamed namespace

static PyObject* SetInputHook(PyObject*, PyObject* pyfunc) {
// Install the PyOS_InputHook that calls the compiled C++ function pyfunc,
// a void() overload, while the interactive prompt waits for input; None
// restores the previous hook.
if (pyfunc == Py_None) {
if (PyOS_InputHook == &InputHook) {
PyOS_InputHook = gPreviousInputHook;
gPreviousInputHook = nullptr;
}
gInputHookFunc = nullptr;
Py_RETURN_NONE;
}

interop::TCppFuncAddr_t addr = 0;
if (CPPOverload_CheckExact(pyfunc)) {
const auto& methods = ((CPPOverload*)pyfunc)->fMethodInfo->fMethods;
if (methods.size() == 1 && methods[0]->GetMaxArgs() == 0)
addr = methods[0]->GetFunctionAddress();
}
if (!addr) {
PyErr_SetString(PyExc_TypeError,
"a C++ function taking no arguments expected");
return nullptr;
}

gInputHookFunc = (void (*)())addr;
if (PyOS_InputHook != &InputHook) {
gPreviousInputHook = PyOS_InputHook;
PyOS_InputHook = &InputHook;
}
Py_RETURN_NONE;
}

//----------------------------------------------------------------------------
static PyObject* BindValue(PyObject*, PyObject* args, PyObject* kwds) {
// Read through the converter for the given type name, at the given address.
Expand Down Expand Up @@ -949,6 +1135,15 @@ static PyMethodDef gcpyrtMethods[] = {
(char*)"Create an object of given type, from given address."},
{(char*)"bind_value", (PyCFunction)BindValue, METH_VARARGS | METH_KEYWORDS,
(char*)"Read a value of given type, from given address."},
{(char*)"as_std_any", (PyCFunction)AsStdAny, METH_O,
(char*)"Wrap a reference to a Python object in a std::any."},
{(char*)"warning_handler", (PyCFunction)GetWarningHandler,
METH_VARARGS | METH_KEYWORDS,
(char*)"Get a C function pointer that issues Python warnings for "
"warnings reported by C++."},
{(char*)"set_input_hook", (PyCFunction)SetInputHook, METH_O,
(char*)"Call a C++ function while the interactive prompt waits for "
"input."},
{(char*)"move", (PyCFunction)Move, METH_O,
(char*)"Cast the C++ object to become movable."},
{(char*)"add_pythonization", (PyCFunction)AddPythonization, METH_VARARGS,
Expand All @@ -957,6 +1152,8 @@ static PyMethodDef gcpyrtMethods[] = {
METH_VARARGS, (char*)"Remove a pythonizor."},
{(char*)"_pin_type", (PyCFunction)PinType, METH_O,
(char*)"Install a type pinning."},
{(char*)"_set_reduce_method", (PyCFunction)SetReduceMethod, METH_O,
(char*)"Set the __reduce__ method of all instances."},
{(char*)"_add_type_reducer", (PyCFunction)AddTypeReducer, METH_VARARGS,
(char*)"Add a type reducer."},
{(char*)"SetHeuristicMemoryPolicy", (PyCFunction)SetHeuristicMemoryPolicy,
Expand Down
16 changes: 16 additions & 0 deletions bindings/pyroot/cppjit/test/test_datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2777,6 +2777,22 @@ def test55_qt_cache_alias_collision(self):
ns.take_int8(101)
raises(TypeError, ns.take_int8, "e")

def test56_function_pointer_return_multiword_types(self):
"""A returned function pointer whose signature has multi-word types"""

import cppjit

cppjit.cppdef(r"""\
namespace ns56 {
typedef unsigned long (*func_t)(const char *, unsigned int);
unsigned long add_len(const char *s, unsigned int n) { return std::string(s).size() + n; }
func_t get() { return add_len; }
}""")

# the std::function wrapping the returned pointer is compiled from
# its signature, which must keep "const char" and "unsigned int"
assert cppjit.gbl.ns56.get()("abc", 4) == 7


class TestANONENUM:
def test01_anonymous_enum_repeated_access(self):
Expand Down
Loading
Loading