首页 > 解决方案 > 嵌入 CPython:如何构造 Python 可调用对象来包装 C 回调指针?

问题描述

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

使用 CPython扩展和嵌入API,我如何构造一个包装 C 函数指针的 Python“可调用”对象,以便我可以将该对象传递给 Python 代码并让 Python 代码成功回调到 C 代码中?

注意:这是用户dhanasubbu最初发布的问题的修订版,我回答了该问题,但随后被删除。我认为这实际上是一个很好的问题,所以我将我写的内容转换为我自己对这个问题的陈述的自我回答。欢迎其他答案。

标签: pythonccpythonpython-c-api

解决方案


要在 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函数烦恼;只要确保你总是->cfun在从 C 创建实例后初始化,否则Callback_call会崩溃。)

现在,假设您需要调用的实际函数是 named real_callback,而您想要将它传递给的 Python 函数是 named 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 异常和/或从外部上下文接收“关闭”信息,留作练习。


推荐阅读