假设我将CPython解释器嵌入到用C编写的更大的程序中。程序的C组件偶尔需要调用用Python编写的函数,并将回调函数作为参数提供给它们。
使用CPython extending and embedding API,如何构建一个包含C指针到函数的Python“可调用”对象,以便我可以将该对象传递给Python代码并让Python代码成功回调到C代码中?
注意:这是用户originally posted的问题dhanasubbu的修订版本,我回答,但随后删除了。我认为这实际上是一个很好的问题,所以我把我写的内容转换成了我自己对问题陈述的自我回答。欢迎提供其他答案。
要定义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异常和/或从外部上下文接收“闭包”信息,留作练习。
我很想使用标准库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片段结合起来形成一个长期的工作示例)