在 C 中编写 python 扩展时,如何将 python 函数传递给 C 函数?

When writing a python extension in C, how does one pass a python function in to a C function?

首先,对于令人困惑的标题,我们深表歉意。

我想要实现的目标如下:假设我有一些函数 foo,它接受一个函数和一个整数作为输入。例如

int foo(int(*func)(), int i) {
    int n = func() + i;
    return n;
}

现在,我想将此函数包装在 python 扩展模块中。所以我开始写我的界面:

#include <Python.h>

extern "C" {
    static PyObject* foo(PyObject* self, PyObject* args);
}

static PyMethodDef myMethods[] = {
    {"foo", foo, METH_VARARGS, "Runs foo"},
    {NULL, NULL, 0, NULL}
}

// Define the module
static struct PyModuleDef myModule = {
    PyModuleDef_HEAD_INIT,
    "myModule",
    "A Module",
    -1,
    myMethods
};

// Initialize the module
PyMODINIT_FUNC PyInit_BSPy(void) {
    return PyModule_Create(&myModule);
}

//Include the function
static PyObject* foo(PyObject* self, PyObject* args){
    // Declare variable/function pointer
    int(*bar)(void);
    unsigned int n;

    // Parse the input tuple
    if (!PyArg_ParseTuple(args, ..., &bar, &n)) {
        return NULL;
    }
}

现在,到了解析输入元组的时候,我感到很困惑,因为我不太确定如何解析它。这个想法很简单:我需要能够在 python 中调用 foo(bar(), n)。但我可以使用一些帮助来实现这一点。

传入的 Python 可调用对象将是一个匹配 Callable 协议的 Python 对象。因此,为了将它传递给您的 C 函数,您必须创建另一个 C 函数作为代理,它与您的函数指针所需的签名相匹配。

举个例子:

static PyObject* foo_cb_callable;
static int py_foo_callback(void) {
    PyObject* retval;
    int result;

    // Call the python function/object saved below
    retval = PyObject_CallObject(foo_cb_callable, NULL);

    // Convert the returned object to an int if possible
    if (retval && PyInt_Check(retval))
        result = (int)PyInt_AsLong(retval);
    else
        result = -1;
    Py_XDECREF(retval);
    return result;
}

// NOTE: I renamed this to avoid conflicting with your "foo"
// function to be called externally.
static PyObject* py_foo(PyObject* self, PyObject* args) {
    unsigned int n;
    int result;

    // Parse the input tuple
    if (!PyArg_ParseTuple(args, "OI", &foo_cb_callable, &n)) {
        return NULL;
    }
    // Ensure the first parameter is a callable (e.g. function)
    if (!PyCallable_Check(foo_cb_callable)) {
        return NULL;
    }

    // Call foo with our custom wrapper
    result = foo(&py_foo_callback, n);
    return Py_BuildValue("i", result);
}

请注意,我在示例中使用了全局回调对象指针,因为您的原始函数指针没有放置自定义用户数据的位置。如果您将通用用户参数添加到 func 回调,则 wrap_foo_callback 可以通过这种方式传递给它,而不是创建一个全局变量。例如:

int foo(int(*func)(void*), void* user_data, int i) {
    int n = func(user_data) + i;
    return n;
}

// ...

static int py_foo_callback(void* callback) {
    PyObject* retval;
    int result;

    // Call the python function/object saved below
    retval = PyObject_CallObject((PyObject*)callback, NULL);

    // Convert the returned object to an int if possible
    if (retval && PyInt_Check(retval))
        result = (int)PyInt_AsLong(retval);
    else
        result = -1;
    Py_XDECREF(retval);
    return result;
}

static PyObject* py_foo(PyObject* self, PyObject* args) {
    PyObject* callback;
    unsigned int n;
    int result;

    // Parse the input tuple
    if (!PyArg_ParseTuple(args, "OI", &callback, &n)) {
        return NULL;
    }
    // Ensure the first parameter is a callable (e.g. function)
    if (!PyCallable_Check(callback)) {
        return NULL;
    }

    // Call foo with our custom wrapper
    result = foo(&py_foo_callback, callback, n);
    return Py_BuildValue("i", result);
}

首先,当你有一个用 C 实现的 Python "extension method",并且该函数接收一个 Python 可调用参数作为参数时,下面是你接收参数的方式,以及如何调用可调用对象:

/* this code uses only C features */
static PyObject *
foo(PyObject *self, PyObject *args)
{
    PyObject *cb;    

    // Receive a single argument which can be any Python object
    // note: the object's reference count is NOT increased (but it's pinned by
    // the argument tuple).
    if (!PyArg_ParseTuple(args, "O", &cb)) {
        return 0;
    }
    // determine whether the object is in fact callable
    if (!PyCallable_Check(cb)) {
        PyErr_SetString(PyExc_TypeError, "foo: a callable is required");
        return 0;
    }
    // call it (no arguments supplied)
    // there are a whole bunch of other PyObject_Call* functions for when you want
    // to supply arguments
    PyObject *rv = PyObject_CallObject(cb, 0);
    // if calling it returned 0, must return 0 to propagate the exception
    if (!rv) return 0;
    // otherwise, discard the object returned and return None
    Py_CLEAR(rv);
    Py_RETURN_NONE;
}

使用这样的逻辑来包装 bsp_init 的问题是指向 Python 可调用对象的指针是一个 data 指针。如果您将该指针直接传递给 bsp_initbsp_init 将尝试将数据作为机器代码调用,并且它会崩溃。如果 bsp_init 通过指向它调用的函数的数据指针传递,您可以使用 "glue" 过程解决此问题:

/* this code also uses only C features */
struct bsp_init_glue_args {
   PyObject *cb;
   PyObject *rv;
};
static void
bsp_init_glue(void *data)
{
   struct bsp_init_glue_args *args = data;
   args->rv = PyObject_CallObject(args->cb, 0);
}

static PyObject *
foo(PyObject *self, PyObject *args)
{
    bsp_init_glue_args ba;
    if (!PyArg_ParseTuple(args, "O", &ba.cb)) {
        return 0;
    }
    if (!PyCallable_Check(ba.cb)) {
        PyErr_SetString(PyExc_TypeError, "foo: a callable is required");
        return 0;
    }
    bsp_init(bsp_init_glue, (void *)&ba, ...);
    if (ba->rv == 0) return 0;
    Py_CLEAR(ba->rv);
    Py_RETURN_NONE;
}

很遗憾,bsp_init 没有此签名,因此您无法执行此操作。但是替代接口 BSPLib::Classic::Init 采用 std::function<void()>,它是上述模式的面向对象包装器,因此您可以改为这样做:

/* this code requires C++11 */
static PyObject *
foo(PyObject *self, PyObject *args)
{
    PyObject *cb;
    PyObject *rv = 0;
    if (!PyArg_ParseTuple(args, "O", &cb)) {
        return 0;
    }
    if (!PyCallable_Check(cb)) {
        PyErr_SetString(PyExc_TypeError, "foo: a callable is required");
        return 0;
    }

    std::function<void()> closure = [&]() {
       rv = PyObject_CallObject(cb, 0);
    };
    BSPLib::Classic::Init(closure, ...);

    if (rv == 0) return 0;
    Py_CLEAR(rv);
    Py_RETURN_NONE;
}

这里的神奇之处在于 [&]() { ... } 符号,它是用于定义和创建局部 class 实例的语法糖,"captures" 变量 cbrv 以便花括号内的代码(将被编译为一个单独的函数)可以与 foo 正确通信。这是一个名为 "lambdas" 的 C++11 特性,这是一个行话术语,可以追溯到理论 CS 的早期,并被 Lisp 永垂不朽。 Here is a tutorial,但我不确定它有多好,因为我已经从里到外了解了这个概念。

不可能在普通 C 中执行此操作,但也不可能从普通 C 调用 BSPLib::Classic::Init(因为您根本无法定义 std::function 对象纯 C ... 好吧,无论如何,不​​是没有对 C++ 标准库和 ABI 进行逆向工程)所以没关系。