嵌入CPython:如何构造Python callables来包装C回调指针?

问题描述 投票:2回答:2

假设我将CPython解释器嵌入到用C编写的更大的程序中。程序的C组件偶尔需要调用用Python编写的函数,并将回调函数作为参数提供给它们。

使用CPython extending and embedding API,如何构建一个包含C指针到函数的Python“可调用”对象,以便我可以将该对象传递给Python代码并让Python代码成功回调到C代码中?

注意:这是用户originally posted的问题dhanasubbu的修订版本,我回答,但随后删除了。我认为这实际上是一个很好的问题,所以我把我写的内容转换成了我自己对问题陈述的自我回答。欢迎提供其他答案。

python c cpython python-c-api
2个回答
3
投票

要定义Python中使用该术语的“可调用”扩展类型,请填充类型对象的tp_call槽,它是__call__特殊方法的C等效物。该槽中的函数将是一个调用实际C回调的粘合例程。这是最简单的代码,当C回调不带参数并且什么都不返回时。

typedef struct {
    PyObject_HEAD
    /* Type-specific fields go here. */
    void (*cfun)(void);  /* or whatever parameters it actually takes */
} CallbackObj;

static PyObject *Callback_call(PyObject *self, PyObject *args, PyObject *kw)
{
    /* check that no arguments were passed */
    const char no_kwargs[] = { 0 };
    if (!PyArg_ParseTupleAndKeywords(args, kw, "", no_kwargs))
        return 0;

    CallbackObj *cself = (CallbackObj *)self;
    cself->cfun();
    Py_RETURN_NONE;
}

static PyTypeObject CallbackType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "mymodule.Callback",
    .tp_doc = "Callback function passed to foo, bar, and baz.",
    .tp_basicsize = sizeof(CallbackObj),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_new = PyType_GenericNew,
    .tp_call = Callback_call,
};

像往常一样使用PyType_Ready实例化类型对象。但是,不要将它放在Python可见的任何模块中,因为Python代码无法正确创建此类型的实例。 (因此,我没有使用tp_init函数;只需确保在从C创建实例后始终初始化->cfun,否则Callback_call将崩溃。)

现在,假设您需要调用的实际函数名为real_callback,并且要将其传递给的Python函数名为function_to_call。首先,通过像往常一样调用类型对象来创建其中一个回调对象,并初始化其->cfun字段:

 PyObject *args = PyTuple_New(0);
 CallbackObj *cb = (CallbackObj *)PyObject_CallObject(
     (PyObject *)CallbackType, args);
 Py_DECREF(args);
 cb->cfun = real_callback;

然后你将cb放入一个参数元组,并像往常一样用它调用Python函数对象。

 args = Py_BuildValue("(O)", cb);
 PyObject *ret = PyObject_CallObject(function_to_call, args);
 Py_DECREF(args);
 Py_DECREF(cb);
 // do stuff with ret, here, perhaps
 Py_DECREF(ret);

将此扩展到更复杂的情况,其中C回调需要接受参数和/或返回值和/或引发错误的Python异常和/或从外部上下文接收“闭包”信息,留作练习。


2
投票

我很想使用标准库ctypes module,因为它已经包含适当的C函数指针包装器,并且可以自动处理从Python类型到C类型的转换,用于各种各样的参数。

我在Cython中编写了一个工作示例,因为它是混合Python和C的简单方法,但它应该展示如何使用这些对象:

cdef extern from *:
    """
    int f(int x) {
       return x*2;
    }
    """
    int f(int f)

我定义了一个示例函数f(在docstring中,Cython直接合并到编译文件中)。

import ctypes
from libc.stdint cimport intptr_t

def make_f_wrapper():
    func_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int)
    cdef intptr_t f_ptr = <intptr_t>&f
    return func_type(f_ptr)

这是一个Cython(非常接近Python语法)版本,创建了一个指向f的ctypes指针。我定义函数的参数,将f指针转换为适当大小的整数,然后用该整数初始化包装器对象。

cdef extern from *:
    """
    PyObject* make_f_wrapper_c_impl() {
        PyObject *ctypes_module = NULL, *CFUNCTYPE = NULL, *c_int = NULL, 
                 *func_type = NULL, *ptr_value = NULL, *result = NULL;

        ctypes_module = PyImport_ImportModule("ctypes");
        if (ctypes_module == NULL) goto cleanup;
        CFUNCTYPE = PyObject_GetAttrString(ctypes_module,"CFUNCTYPE");
        if (CFUNCTYPE == NULL) goto cleanup;
        c_int = PyObject_GetAttrString(ctypes_module,"c_int");
        if (c_int == NULL) goto cleanup;
        func_type = PyObject_CallFunctionObjArgs(CFUNCTYPE,c_int,c_int,NULL);
        if (func_type == NULL) goto cleanup;
        ptr_value = PyLong_FromVoidPtr(&f);
        if (ptr_value == NULL) goto cleanup;
        result = PyObject_CallFunctionObjArgs(func_type,ptr_value,NULL);

        cleanup:
        Py_XDECREF(ptr_value);
        Py_XDECREF(func_type);
        Py_XDECREF(c_int);
        Py_XDECREF(CFUNCTYPE);
        Py_XDECREF(ctypes_module);
        return result;
    }
    """
    object make_f_wrapper_c_impl()

def make_f_wrapper_c():
    return make_f_wrapper_c_impl()

上面的代码是上面Pythony代码的C转换 - 它完全相同但由于它使用C API而有点复杂。它只是通过Python接口使用ctypes模块。 C代码再次通过docstring嵌入到Cython文件中;但是,类似的代码可以用在直接编写的C API模块中。

(所有这些Cython片段结合起来形成一个长期的工作示例)

© www.soinside.com 2019 - 2024. All rights reserved.