I am writing code that starts in Python, then goes to C via ctypes and inside C it uses Python embedding to invoke a Python function, that is, the flow looks like this:
Python user code passes function name -> C mediator library -> Python "backend code"
Python user code loads C mediator library and passes to it a function name
funcnameand its arguments (types and values) viactypes.C mediator library embeds Python, loads required module and call the function with
funcnameand passed arguments.Embedded Python execute the function and returns result.
The Python function accepts different parameters and one of them
is a callback function. When I am at the C mediator library, I have a void pointer to this callback. Question: How to convert it to a Python callable?
Thank you!
Minimal (not-)working example consists of files run.py (user code), callstuff.c (C mediator library), dostuff.py (Python "backend" code) and CMakeLists.txt to compile the C mediator library.
# File run.py
import ctypes
import sys
if __name__ == "__main__":
if sys.platform == "darwin":
ext = "dylib"
elif sys.platform == "linux":
ext = "so"
else:
raise ValueError("Handle me somehow")
lib = ctypes.PyDLL(f"build/libcallstuff.{ext}")
initialize = lib.__getattr__("initialize")
initialize()
call = lib.__getattr__("call")
# signature: int call(char *funcname, void * fn_p, int x)
call.restype = ctypes.c_int
call.argtypes = [ctypes.c_char_p, ctypes.c_void_p, ctypes.c_int]
def myfn(x):
return 2 * x
# Prepare all arguments to call func
# fn_p signature is int f(int x)
fn_t = ctypes.CFUNCTYPE(ctypes.c_int, *[ctypes.c_int])
fn_p = ctypes.cast(ctypes.pointer(fn_t(myfn)), ctypes.c_void_p)
a = ctypes.c_int(21)
result = call("apply".encode(), fn_p, a)
print("Result is", result)
finalize = lib.__getattr__("finalize")
finalize()
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stdio.h>
#include <string.h>
/**
* This function invokes function `func` from the Python module `dostuff.py`
* via Python embedding.
* Here, the function is constrained, as the other arguments are passed
* explicitly because there is only one value of `func` for this example.
* In general case, it will be a list that carries types information as ints
* and void pointers to values.
* All memory release and error checks are omitted.
*/
int call(const char *fn_name, void *fn_p, int x) {
printf("I am here\n");
PyObject *pFileName = PyUnicode_FromString("dostuff");
printf("I am here 2\n");
PyObject *pModule = PyImport_Import(pFileName);
printf("I am here 3\n");
PyObject *pFunc = PyObject_GetAttrString(pModule, fn_name);
PyObject *pArgs = PyTuple_New(2); // We have args: f, a, b
PyObject *pValue;
pValue = (PyObject *) fn_p; // ??????? How to convert void *fn_p?
PyTuple_SetItem(pArgs, 0, pValue);
pValue = PyLong_FromLong(x);
PyTuple_SetItem(pArgs, 1, pValue);
PyObject *pResult = PyObject_CallObject(pFunc, pArgs);
if (pResult != NULL) {
return PyLong_AsLong(pResult);
} else {
return -1;
}
}
int initialize() {
Py_Initialize();
return 0;
}
int finalize() {
Py_Finalize();
return 0;
}
# file dostuff.py
from typing import Callable
def apply(f: Callable, x):
return f(x)
cmake_minimum_required(VERSION 3.18)
project(PyCInterop LANGUAGES C)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(Python REQUIRED Interpreter Development)
add_library(callstuff SHARED callstuff.c)
target_link_libraries(callstuff PRIVATE Python::Python)
To compile:
$ cmake -S. -B build -DCMAKE_BUILD_TYPE=Debug && cmake --build build
To run:
$ python run.py
PyObject *, which is how that function should be defined in the first place. Maybe the question is how it got to be avoid *?void *as the function pointer could be from Python, C, or other languages. All arguments are passed between different languages as void pointers along with type identifiers and then converted to corresponding types on the receiving side (for example, Cdoubleis converted to Pythonfloat).