1

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"
  1. Python user code loads C mediator library and passes to it a function name funcname and its arguments (types and values) via ctypes.

  2. C mediator library embeds Python, loads required module and call the function with funcname and passed arguments.

  3. 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
9
  • 1
    I think you'd just need a PyObject *, which is how that function should be defined in the first place. Maybe the question is how it got to be a void *? Commented Oct 30, 2023 at 17:38
  • 1
    Write a minimal reproducible example to give us context. Commented Oct 30, 2023 at 17:41
  • 1
    A callable is anything that implements the tp_call or vectorcall protocols (see Call protocol). You pass any generic object and it is checked dynamically for whether it is callable. Commented Oct 30, 2023 at 17:42
  • @MarkTolonen I have added an example. Thank you Commented Oct 30, 2023 at 19:22
  • @tdelaney It is 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, C double is converted to Python float). Commented Oct 30, 2023 at 21:22

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.