用 C 或 C++ 扩展 Python —— Extending Python with C or C++

https://docs.python.org/3/extending/extending.html#a-simple-example


1. 用 C 或 C++ 扩展 Python

        如果您了解 C 语言编程,那么向 Python 添加新的内置模块是相当容易的。这类扩展模块可以完成两件直接用 Python 无法做到的事情:实现新的内置对象类型,以及调用 C 库函数和系统调用

        为了支持扩展,Python API(应用程序编程接口)定义了一组函数、宏和变量,这些接口提供了对 Python 运行时系统大部分功能的访问。在 C 源文件中包含头文件 "Python.h" 即可使用 Python API

        扩展模块的编译依赖于其预期用途和系统设置,具体细节将在后续章节中介绍。

        注意:C 扩展接口特定于 CPython扩展模块在其他 Python 实现中无法工作。在许多情况下,可以避免编写 C 扩展并保持对其他实现的可移植性。例如,如果用例是调用 C 库函数或系统调用,应考虑使用ctypes模块或cffi库,而不是编写自定义 C 代码。这些模块允许您编写 Python 代码与 C 代码交互,并且比编写和编译 C 扩展模块在不同 Python 实现之间更具可移植性

1.1 简单示例

       让我们创建一个名为 spam 的扩展模块(这是 Monty Python 粉丝最喜欢的食物……),并假设我们想为 C 库函数 system() 创建一个 Python 接口 [1]。该函数接受一个以空字符结尾的字符串作为参数并返回一个整数。我们希望这个函数可以从 Python 中按如下方式调用:

运行方式:

import spam
status = spam.system("ls -l")


        首先创建一个文件 spammodule.c。(从历史上看,如果模块名为 spam,则包含其实现的 C 文件通常称为 spammodule.c;如果模块名很长,如spammify,则模块名可以直接是spammify.c。)

        文件的前两行可以是:

#define PY_SSIZE_T_CLEAN
#include <Python.h>


        这会引入 Python API(您可以根据需要添加描述模块用途的注释和版权声明)。

        注意:由于 Python 可能会定义一些影响某些系统上标准头文件的预处理器定义,因此必须在包含任何标准头文件之前包含Python.h
        #define PY_SSIZE_T_CLEAN用于指示在某些 API 中应使用Py_ssize_t而不是int。自 Python 3.13 起,这已非必需,但为了向后兼容,我们在此保留它。有关此宏的说明,请参见字符串和缓冲区。

        除了标准头文件中定义的符号外,Python.h定义的所有用户可见符号都有Py或PY前缀。为了方便起见,并且由于 Python 解释器广泛使用这些符号,Python.h包含了一些标准头文件:<stdio.h>、<string.h>、<errno.h>和<stdlib.h>。如果您的系统上不存在后一个头文件,它会直接声明函数malloc()、free()和realloc()。

        接下来,我们在模块文件中添加当 Python 表达式 spam.system(string) 被求值时将调用的 C 函数(我们很快会看到它是如何被调用的):

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = system(command);
    return PyLong_FromLong(sts);
}


        从 Python 中的参数列表(例如单个表达式 "ls -l")到传递给 C 函数的参数,存在直接的转换关系。C 函数始终有两个参数,按惯例命名为self和args。

    self参数指向模块级函数的模块对象;对于方法,它指向对象实例。
    args参数将是一个指向 Python 元组对象的指针,包含参数。元组的每个元素对应调用参数列表中的一个参数。这些参数是 Python 对象,为了在 C 函数中使用它们,我们必须将其转换为 C 值。Python API 中的PyArg_ParseTuple()函数检查参数类型并将其转换为 C 值。它使用模板字符串确定参数的所需类型以及存储转换后值的 C 变量的类型。稍后会详细介绍。


        如果所有参数类型正确且其组件已存储到传递地址的变量中,PyArg_ParseTuple()返回 true(非零);如果传递了无效的参数列表,则返回 false(零)。在后一种情况下,它还会引发适当的异常,因此调用函数可以立即返回 NULL(如示例中所示)。


1.2 插曲:错误和异常


        Python 解释器中一个重要的约定如下:当函数失败时,应设置异常条件并返回错误值(通常为 - 1 或 NULL 指针)异常信息存储在解释器线程状态的三个成员中。如果没有异常,这些成员为 NULL;否则,它们是与sys.exc_info()返回的 Python 元组成员对应的 C 等效项,即异常类型异常实例追溯对象了解它们对于理解错误如何传递非常重要。

        Python API 定义了许多设置各种类型异常的函数。最常见的是PyErr_SetString(),其参数是异常对象和 C 字符串。异常对象通常是预定义的对象,如PyExc_ZeroDivisionError。C 字符串指示错误原因,并转换为 Python 字符串对象,存储为异常的 “关联值”

        另一个有用的函数是PyErr_SetFromErrno(),它只接受一个异常参数,并通过检查全局变量errno来构造关联值。最通用的函数是PyErr_SetObject(),它接受两个对象参数:异常及其关联值。传递给这些函数的对象不需要调用Py_INCREF()

        可以使用PyErr_Occurred()非破坏性地测试是否已设置异常。该函数返回当前异常对象,若没有异常则返回 NULL。通常不需要调用PyErr_Occurred()来查看函数调用中是否发生错误,因为可以从返回值判断。

        当调用另一个函数g的函数f检测到g失败时,f本身应返回错误值(通常为 NULL 或 - 1),而不应调用PyErr_*函数 —— 因为g已经调用过了。f的调用者应同样向其调用者返回错误指示,同样不调用PyErr_*,依此类推 —— 错误的最详细原因已由首次检测到它的函数报告一旦错误到达 Python 解释器的主循环,将中止当前执行的 Python 代码,并尝试查找 Python 程序员指定的异常处理程序

        (在某些情况下,模块可以通过调用另一个PyErr_*函数来提供更详细的错误消息,这种情况下这样做是可以的。但一般来说,这不是必需的,并且可能导致错误原因信息丢失:大多数操作可能因各种原因失败。)

        要忽略由失败的函数调用设置的异常,必须通过调用PyErr_Clear()显式清除异常条件。C 代码仅在不想将错误传递给解释器,而是希望自己完全处理它时(可能通过尝试其他操作假装没有出错)才应调用PyErr_Clear()

        每个失败的malloc()调用必须转换为异常 ——malloc()(或realloc())的直接调用者必须调用PyErr_NoMemory()并自己返回失败指示。所有对象创建函数(例如PyLong_FromLong())已经这样做了,因此本说明仅适用于直接调用malloc()的情况

        还需注意,除了PyArg_ParseTuple()及其相关函数外,返回整数状态的函数通常返回正值或零表示成功,-1 表示失败,类似于 Unix 系统调用。

        最后,当返回错误指示时,务必清理垃圾通过对已创建的对象调用Py_XDECREF()Py_DECREF())!

        选择引发哪种异常完全由您决定。存在与所有内置 Python 异常对应的预声明 C 对象,如PyExc_ZeroDivisionError,可以直接使用。当然,应明智选择异常 —— 不要用PyExc_TypeError表示文件无法打开(这可能应该是PyExc_OSError)。如果参数列表有问题,PyArg_ParseTuple()函数通常会引发PyExc_TypeError。如果参数值必须在特定范围内或满足其他条件,PyExc_ValueError是合适的。

        您还可以定义模块独有的新异常。最简单的方法是在文件开头声明一个静态全局对象变量:

static PyObject *SpamError = NULL;


        并在模块的Py_mod_exec函数(spam_module_exec())中通过调用PyErr_NewException()初始化它:

SpamError = PyErr_NewException("spam.error", NULL, NULL);


        由于SpamError是全局变量,每次重新初始化模块时(调用Py_mod_exec函数时)它都会被覆盖。为避免这种情况,我们可以通过引发ImportError来阻止重复初始化:

static PyObject *SpamError = NULL;

static int
spam_module_exec(PyObject *m)
{
    if (SpamError != NULL) {
        PyErr_SetString(PyExc_ImportError,
                        "cannot initialize spam module more than once");
        return -1;
    }
    SpamError = PyErr_NewException("spam.error", NULL, NULL);
    if (PyModule_AddObjectRef(m, "SpamError", SpamError) < 0) {
        return -1;
    }

    return 0;
}

static PyModuleDef_Slot spam_module_slots[] = {
    {Py_mod_exec, spam_module_exec},
    {0, NULL}
};

static struct PyModuleDef spam_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "spam",
    .m_size = 0,  // non-negative
    .m_slots = spam_module_slots,
};

PyMODINIT_FUNC
PyInit_spam(void)
{
    return PyModuleDef_Init(&spam_module);
}


        注意,异常对象的 Python 名称是spam.error。PyErr_NewException()函数可能创建一个基类为Exception的类(除非传入 NULL 以外的其他类),如内置异常中所述。

        还需注意,SpamError变量保留对新创建异常类的引用 —— 这是有意的!因为异常可能被外部代码从模块中移除,需要对类的自有引用以确保它不会被丢弃,导致SpamError成为悬空指针。如果它成为悬空指针,引发异常的 C 代码可能导致 core dump或其他意外副作用

        目前,缺少删除此引用的Py_DECREF()调用。即使 Python 解释器关闭,全局SpamError变量也不会被垃圾回收,它会 “泄漏”。但我们确保每个进程最多发生一次这种情况。

        我们将在本示例的后面讨论PyMODINIT_FUNC作为函数返回类型的用法。

        可以在扩展模块中使用如下所示的PyErr_SetString()调用来引发spam.error异常:

 

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = system(command);
    if (sts < 0) {
        PyErr_SetString(SpamError, "System command failed");
        return NULL;
    }
    return PyLong_FromLong(sts);
}

 

1.3 回到示例

        回到我们的示例函数,现在您应该能够理解以下语句:

if (!PyArg_ParseTuple(args, "s", &command))
    return NULL;

        如果在参数列表中检测到错误,它将返回 NULL(返回对象指针的函数的错误指示),依赖于PyArg_ParseTuple()设置的异常。否则,参数的字符串值已复制到局部变量command。这是一个指针赋值,您不应该修改它指向的字符串(因此在标准 C 中,变量command应正确声明为const char *command)。

        下一条语句是调用 Unix 函数system(),将我们刚从PyArg_ParseTuple()获得的字符串传递给它:

sts = system(command);

        我们的spam.system()函数必须将sts的值作为 Python 对象返回,这通过函数PyLong_FromLong()完成:

return PyLong_FromLong(sts);

        在这种情况下,它将返回一个整数对象。(是的,即使整数在 Python 中也是堆上的对象!)

        如果有一个 C 函数不返回有用参数(返回void的函数),对应的 Python 函数必须返回None。需要使用以下习惯用法(由Py_RETURN_NONE宏实现):

Py_INCREF(Py_None);
return Py_None;


        Py_None是特殊 Python 对象None的 C 名称。它是一个真正的 Python 对象,而不是 NULL 指针,在大多数上下文中 NULL 指针表示 “错误”,如我们所见。


1.4 模块的方法表和初始化函数

        我承诺会展示如何从 Python 程序中调用spam_system()。首先,我们需要在 “方法表” 中列出其名称和地址:

static PyMethodDef spam_methods[] = {
    ...
    {"system",  spam_system, METH_VARARGS,
     "Execute a shell command."},
    ...
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

        注意第三个条目(METH_VARARGS),这是一个标志,告诉解释器 C 函数使用的调用约定。它通常应始终是METH_VARARGSMETH_VARARGS | METH_KEYWORDS;值为 0 表示使用PyArg_ParseTuple()的过时变体。

        仅使用METH_VARARGS时,函数应期望 Python 级参数作为元组传递,该元组可通过PyArg_ParseTuple()解析;有关此函数的更多信息如下。

        如果应将关键字参数传递给函数,则可以在第三个字段中设置METH_KEYWORDS位。在这种情况下,C 函数应接受第三个PyObject *参数,该参数将是关键字的字典。使用PyArg_ParseTupleAndKeywords()解析此类函数的参数。

方法表必须在模块定义结构中引用:

static struct PyModuleDef spam_module = {
    ...
    .m_methods = spam_methods,
    ...
};


        反过来,此结构必须在模块的初始化函数中传递给解释器。初始化函数必须命名为PyInit_name(),其中name是模块的名称,并且应该是模块文件中定义的唯一非静态项:

PyMODINIT_FUNC
PyInit_spam(void)
{
    return PyModuleDef_Init(&spam_module);
}


        注意,PyMODINIT_FUNC将函数声明为PyObject *返回类型,声明平台所需的任何特殊链接声明,并且对于 C++,将函数声明为extern "C"。

        当每个解释器首次导入其模块spam时,会调用PyInit_spam()。必须通过PyModuleDef_Init()返回模块定义的指针,以便导入机制可以创建模块并将其存储在sys.modules中。

        当嵌入 Python 时,除非PyImport_Inittab表中有条目,否则不会自动调用PyInit_spam()函数。要将模块添加到初始化表中,请使用PyImport_AppendInittab(),可选地随后导入模块:

#define PY_SSIZE_T_CLEAN
#include <Python.h>

int
main(int argc, char *argv[])
{
    PyStatus status;
    PyConfig config;
    PyConfig_InitPythonConfig(&config);

    /* 在Py_Initialize之前添加内置模块 */
    if (PyImport_AppendInittab("spam", PyInit_spam) == -1) {
        fprintf(stderr, "Error: could not extend in-built modules table\n");
        exit(1);
    }

    /* 将argv[0]传递给Python解释器 */
    status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
    if (PyStatus_Exception(status)) {
        goto exception;
    }

    /* 初始化Python解释器。必需。
       如果此步骤失败,将是致命错误。 */
    status = Py_InitializeFromConfig(&config);
    if (PyStatus_Exception(status)) {
        goto exception;
    }
    PyConfig_Clear(&config);

    /* 可选地导入模块;或者,
       可以延迟导入直到嵌入式脚本
       导入它。 */
    PyObject *pmodule = PyImport_ImportModule("spam");
    if (!pmodule) {
        PyErr_Print();
        fprintf(stderr, "Error: could not import module 'spam'\n");
    }

    // ... 在此处使用Python C API ...

    return 0;

  exception:
     PyConfig_Clear(&config);
     Py_ExitStatusException(status);
}


        注意:如果声明全局变量或局部静态变量,模块在重新初始化时可能会遇到意外副作用,例如从sys.modules中删除条目或在进程内的多个解释器中导入已编译模块时(或在fork()之后没有中间exec()时)。如果模块状态尚未完全隔离,作者应考虑将模块标记为不支持子解释器(通过Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED)。

        Python 源代码发行版中包含一个更完整的示例模块,位于Modules/xxlimited.c。该文件可用作模板或直接作为示例阅读。


1.5 编译和链接

        在使用新扩展之前,还需要做两件事:将其与 Python 系统编译和链接。如果使用动态加载,细节可能取决于系统使用的动态加载风格;有关此内容的更多信息,请参见构建 C 和 C++ 扩展章和仅与 Windows 上构建相关的附加信息Windows 上构建 C 和 C++ 扩展章。

        如果无法使用动态加载,或者想使模块成为 Python 解释器的永久部分,则必须更改配置设置并重新构建解释器。幸运的是,在 Unix 上这非常简单:只需将文件(例如spammodule.c)放在解压的源代码发行版的Modules/目录中,向文件Modules/Setup.local添加一行描述文件的内容:

spam spammodule.o


        然后通过在顶级目录中运行make来重新构建解释器。也可以在Modules/子目录中运行make,但必须首先通过运行make Makefile重新构建那里的Makefile。(每次更改Setup文件时都需要这样做。)

        如果模块需要与其他库链接,这些库也可以在配置文件的行中列出,例如:

spam spammodule.o -lX11

1.6 从 C 调用 Python 函数


        到目前为止,我们专注于使 C 函数可从 Python 调用。反过来也很有用:从 C 调用 Python 函数。这在支持所谓 “回调” 函数的库中尤其常见。如果 C 接口使用回调,等效的 Python 通常需要向 Python 程序员提供回调机制;实现将需要从 C 回调中调用 Python 回调函数。其他用途也是可以想象的。

        幸运的是,Python 解释器可以轻松递归调用,并且有调用 Python 函数的标准接口。(我不会详述如何使用特定字符串作为输入调用 Python 解析器 —— 如果感兴趣,请查看 Python 源代码中Modules/main.c中-c命令行选项的实现。)

        调用 Python 函数很简单。首先,Python 程序必须以某种方式将 Python 函数对象传递给您。应该提供一个函数(或其他接口)来执行此操作。当调用此函数时,将 Python 函数对象的指针(注意使用Py_INCREF()!)保存在全局变量中 —— 或任何您认为合适的地方。例如,以下函数可能是模块定义的一部分:

static PyObject *my_callback = NULL;

static PyObject *
my_set_callback(PyObject *dummy, PyObject *args)
{
    PyObject *result = NULL;
    PyObject *temp;

    if (PyArg_ParseTuple(args, "O:set_callback", &temp)) {
        if (!PyCallable_Check(temp)) {
            PyErr_SetString(PyExc_TypeError, "parameter must be callable");
            return NULL;
        }
        Py_XINCREF(temp);         /* 为新回调添加引用 */
        Py_XDECREF(my_callback);  /* 释放先前的回调 */
        my_callback = temp;       /* 记住新回调 */
        /* 返回"None"的样板代码 */
        Py_INCREF(Py_None);
        result = Py_None;
    }
    return result;
}


        此函数必须使用METH_VARARGS标志向解释器注册;这在模块的方法表和初始化函数部分中描述。PyArg_ParseTuple()函数及其参数在扩展函数中提取参数部分中记录。

        宏Py_XINCREF()和Py_XDECREF()递增 / 递减对象的引用计数,并且在 NULL 指针存在时是安全的(但请注意,在此上下文中temp不会为 NULL)。有关它们的更多信息,请参见引用计数部分。

        稍后,当需要调用函数时,调用 C 函数PyObject_CallObject()。此函数有两个参数,均为指向任意 Python 对象的指针:Python 函数和参数列表。参数列表必须始终是元组对象,其长度为参数数量。要调用不带参数的 Python 函数,传入 NULL 或空元组;要调用带一个参数的函数,传入单元素元组。Py_BuildValue()在其格式字符串由括号内的零个或多个格式代码组成时返回元组。例如:

int arg;
PyObject *arglist;
PyObject *result;
...
arg = 123;
...
/* 调用回调的时间 */
arglist = Py_BuildValue("(i)", arg);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);


        PyObject_CallObject()返回 Python 对象指针:这是 Python 函数的返回值。                                PyObject_CallObject()相对于其参数是 “引用计数中立” 的。在示例中,创建了一个新元组作为参数列表,在PyObject_CallObject()调用后立即对其调用Py_DECREF()。

        PyObject_CallObject()的返回值是 “新的”:要么是全新的对象,要么是引用计数已递增的现有对象。因此,除非想将其保存在全局变量中,否则应该以某种方式对结果调用Py_DECREF(),即使(尤其是)对其值不感兴趣。

        然而,在执行此操作之前,检查返回值是否为 NULL 很重要。如果是,Python 函数通过引发异常终止。如果调用PyObject_CallObject()的 C 代码是从 Python 调用的,现在应该向其 Python 调用者返回错误指示,以便解释器可以打印堆栈跟踪,或调用的 Python 代码可以处理异常。如果这不可能或不可取,应通过调用PyErr_Clear()清除异常。例如:

if (result == NULL)
    return NULL; /* 将错误传回 */
...使用result...
Py_DECREF(result);


        根据 Python 回调函数的所需接口,可能还需要向PyObject_CallObject()提供参数列表。在某些情况下,参数列表也由 Python 程序通过指定回调函数的相同接口提供,然后可以像函数对象一样保存和使用它。在其他情况下,可能需要构造一个新元组作为参数列表。最简单的方法是调用Py_BuildValue()。例如,如果想传递整数事件代码,可以使用以下代码:

PyObject *arglist;
...
arglist = Py_BuildValue("(l)", eventcode);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
if (result == NULL)
    return NULL; /* 将错误传回 */
/* 这里可能使用结果 */
Py_DECREF(result);


        注意Py_DECREF(arglist)的位置紧接在调用之后,在错误检查之前!还要注意,严格来说,此代码不完整:Py_BuildValue()可能耗尽内存,这应该被检查。

        也可以使用PyObject_Call()调用带有关键字参数的函数,该函数支持位置参数和关键字参数。如上面的示例,我们使用Py_BuildValue()构造字典:

PyObject *dict;
...
dict = Py_BuildValue("{s:i}", "name", val);
result = PyObject_Call(my_callback, NULL, dict);
Py_DECREF(dict);
if (result == NULL)
    return NULL; /* 将错误传回 */
/* 这里可能使用结果 */
Py_DECREF(result);

 

1.7 扩展函数中的参数提取


PyArg_ParseTuple()函数声明如下:

int PyArg_ParseTuple(PyObject *arg, const char *format, ...);


        arg参数必须是包含从 Python 传递给 C 函数的参数列表的元组对象。format参数必须是格式字符串,其语法在《Python/C API 参考手册》的解析参数和构建值中解释。其余参数必须是变量的地址,其类型由格式字符串确定。

        注意,虽然PyArg_ParseTuple()检查 Python 参数是否具有所需类型,但它无法检查传递给调用的 C 变量地址的有效性:如果在此处出错,代码可能会崩溃或至少覆盖内存中的随机位。因此要小心!

        注意,提供给调用者的任何 Python 对象引用都是借用的引用;不要递减它们的引用计数!

        一些示例调用:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
int ok;
int i, j;
long k, l;
const char *s;
Py_ssize_t size;

ok = PyArg_ParseTuple(args, ""); /* 无参数 */
    /* Python调用: f() */
ok = PyArg_ParseTuple(args, "s", &s); /* 一个字符串 */
    /* 可能的Python调用: f('whoops!') */
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* 两个长整型和一个字符串 */
    /* 可能的Python调用: f(1, 2, 'three') */
ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
    /* 一对整数和一个字符串,其大小也被返回 */
    /* 可能的Python调用: f((1, 2), 'three') */
{
    const char *file;
    const char *mode = "r";
    int bufsize = 0;
    ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
    /* 一个字符串,可选另一个字符串和一个整数 */
    /* 可能的Python调用:
       f('spam')
       f('spam', 'w')
       f('spam', 'wb', 100000) */
}
{
    int left, top, right, bottom, h, v;
    ok = PyArg_ParseTuple(args, "((ii)(ii))(ii)",
             &left, &top, &right, &bottom, &h, &v);
    /* 一个矩形和一个点 */
    /* 可能的Python调用:
       f(((0, 0), (400, 300)), (10, 10)) */
}
{
    Py_complex c;
    ok = PyArg_ParseTuple(args, "D:myfunction", &c);
    /* 一个复数,也为错误提供函数名 */
    /* 可能的Python调用: myfunction(1+2j) */
}

 

1.8 扩展函数的关键字参数


PyArg_ParseTupleAndKeywords()函数声明如下:

int PyArg_ParseTupleAndKeywords(PyObject *arg, PyObject *kwdict,
                                const char *format, char * const *kwlist, ...);


        arg和format参数与PyArg_ParseTuple()函数的参数相同。kwdict参数是作为第三个参数从 Python 运行时接收的关键字字典。kwlist参数是一个以 NULL 结尾的字符串列表,用于标识参数;这些名称从左到右与format中的类型信息匹配。成功时,PyArg_ParseTupleAndKeywords()返回 true,否则返回 false 并引发适当的异常。

        注意:使用关键字参数时无法解析嵌套元组!传递的关键字参数不在kwlist中会导致引发TypeError。

        以下是一个使用关键字的示例模块,基于 Geoff Philbrick(philbrick@hks.com)的示例:

#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
    int voltage;
    const char *state = "a stiff";
    const char *action = "voom";
    const char *type = "Norwegian Blue";

    static char *kwlist[] = {"voltage", "state", "action", "type", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|sss", kwlist,
                                     &voltage, &state, &action, &type))
        return NULL;

    printf("-- This parrot wouldn't %s if you put %i Volts through it.\n",
           action, voltage);
    printf("-- Lovely plumage, the %s -- It's %s!\n", type, state);

    Py_RETURN_NONE;
}

static PyMethodDef keywdarg_methods[] = {
    /* 函数的强制类型转换是必要的,因为PyCFunction值
     * 只接受两个PyObject*参数,而keywdarg_parrot()接受
     * 三个。
     */
    {"parrot", (PyCFunction)(void(*)(void))keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,
     "Print a lovely skit to standard output."},
    {NULL, NULL, 0, NULL}   /* 哨兵 */
};

static struct PyModuleDef keywdarg_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "keywdarg",
    .m_size = 0,
    .m_methods = keywdarg_methods,
};

PyMODINIT_FUNC
PyInit_keywdarg(void)
{
    return PyModuleDef_Init(&keywdarg_module);
}

1.9 构建任意值

        此函数是PyArg_ParseTuple()的对应函数,声明如下:

PyObject *Py_BuildValue(const char *format, ...);


        它识别一组类似于PyArg_ParseTuple()识别的格式单元,但参数(作为函数的输入,而非输出)不能是指针,只能是值。它返回一个新的 Python 对象,适合从 Python 调用的 C 函数返回。

        与PyArg_ParseTuple()的一个区别是:后者要求其第一个参数是元组(因为 Python 参数列表在内部始终表示为元组),而Py_BuildValue()并不总是构建元组。只有当格式字符串包含两个或更多格式单元时,它才会构建元组。如果格式字符串为空,它返回None;如果恰好包含一个格式单元,它返回该格式单元描述的任何对象。要强制它返回大小为 0 或 1 的元组,请将格式字符串括在括号中。

        示例(左侧为调用,右侧为生成的 Python 值):

Py_BuildValue("")                        None
Py_BuildValue("i", 123)                  123
Py_BuildValue("iii", 123, 456, 789)      (123, 456, 789)
Py_BuildValue("s", "hello")              'hello'
Py_BuildValue("y", "hello")              b'hello'
Py_BuildValue("ss", "hello", "world")    ('hello', 'world')
Py_BuildValue("s#", "hello", 4)          'hell'
Py_BuildValue("y#", "hello", 4)          b'hell'
Py_BuildValue("()")                      ()
Py_BuildValue("(i)", 123)                (123,)
Py_BuildValue("(ii)", 123, 456)          (123, 456)
Py_BuildValue("(i,i)", 123, 456)         (123, 456)
Py_BuildValue("[i,i]", 123, 456)         [123, 456]
Py_BuildValue("{s:i,s:i}",
              "abc", 123, "def", 456)    {'abc': 123, 'def': 456}
Py_BuildValue("((ii)(ii)) (ii)",
              1, 2, 3, 4, 5, 6)          (((1, 2), (3, 4)), (5, 6))

1.10 引用计数

        在 C 或 C++ 等语言中,程序员负责堆上内存的动态分配和释放。在 C 中,这通过函数malloc()和free()完成。在 C++ 中,使用含义基本相同的运算符new和delete,以下讨论将限于 C 的情况。

        每个用malloc()分配的内存块最终都应该通过恰好一次对free()的调用来归还给可用内存池。在正确的时间调用free()很重要。如果忘记了块的地址但没有对其调用free(),则其占用的内存直到程序终止才能重用,这称为内存泄漏。另一方面,如果程序对块调用free(),然后继续使用该块,会导致与通过另一个malloc()调用重用该块的冲突,这称为使用已释放的内存,其后果与引用未初始化数据一样糟糕:核心转储、错误结果、神秘崩溃。

        内存泄漏的常见原因是代码中的异常路径。例如,函数可能分配一块内存,进行一些计算,然后再次释放该块。现在,函数需求的变化可能会在计算中添加一个测试,检测错误条件并提前从函数返回。很容易忘记在提前退出时释放分配的内存块,尤其是在后来添加到代码中时。此类泄漏一旦引入,往往长时间未被发现:错误退出仅在所有调用的一小部分中发生,并且大多数现代机器有足够的虚拟内存,因此泄漏仅在频繁使用泄漏函数的长时间运行进程中才会明显。因此,通过制定编码约定或策略来最小化此类错误,防止泄漏发生非常重要。

        由于 Python 大量使用malloc()和free(),它也需要一种策略来避免内存泄漏和使用已释放的内存,选择的方法称为引用计数。其原理很简单:每个对象包含一个计数器,当引用存储到对象时递增,当引用被删除时递减。当计数器达到零时,对象的最后一个引用已被删除,对象被释放。

        另一种策略称为自动垃圾收集。(有时,引用计数也被称为垃圾收集策略,因此我使用 “自动” 来区分两者。)自动垃圾收集的最大优点是用户不需要显式调用free()。(另一个声称的优点是速度或内存使用的改进 —— 但这不是硬事实。)缺点是对于 C,没有真正可移植的自动垃圾收集器,而引用计数可以可移植地实现(只要malloc()和free()函数可用 —— 这是 C 标准保证的)。也许有一天会有一个足够可移植的 C 自动垃圾收集器可用。在此之前,我们必须使用引用计数。

        虽然 Python 使用传统的引用计数实现,但它还提供了一个循环检测器,用于检测引用循环。这允许应用程序不必担心创建直接或间接的循环引用,这些是仅使用引用计数实现的垃圾收集的弱点。引用循环由包含(可能间接)对自身引用的对象组成,因此循环中的每个对象的引用计数都非零。典型的引用计数实现无法回收引用循环中任何对象的内存,或从循环中对象引用的内存,即使循环本身没有进一步的引用。

        循环检测器能够检测垃圾循环并回收它们。gc模块提供了运行检测器的方法(collect()函数),以及配置接口和在运行时禁用检测器的能力。


1.10.1 Python 中的引用计数

        有两个宏Py_INCREF(x)和Py_DECREF(x),用于处理引用计数的递增和递减。当计数达到零时,Py_DECREF()还会释放对象。为了灵活性,它不直接调用free(),而是通过对象类型对象中的函数指针进行调用。为此(和其他目的),每个对象还包含一个指向其类型对象的指针。

        现在剩下的大问题是:何时使用Py_INCREF(x)和Py_DECREF(x)?让我们首先介绍一些术语。没有人 “拥有” 对象,但可以拥有对对象的引用。对象的引用计数现在定义为对它的拥有引用数。引用的所有者负责在不再需要引用时调用Py_DECREF()。引用的所有权可以转移,处理拥有引用有三种方式:传递它、存储它或调用Py_DECREF()。忘记处理拥有引用会导致内存泄漏。

        也可以 “借用” 对对象的引用。引用的借用者不应调用Py_DECREF(),且不得比从中借用的所有者更长时间持有对象。在所有者处理引用后使用借用的引用存在使用已释放内存的风险,应完全避免 [3]。

        借用引用相对于拥有引用的优点是,不需要在代码的所有可能路径上处理引用的释放 —— 换句话说,使用借用引用时,不会因提前退出而泄漏。借用相对于拥有的缺点是,在看似正确的代码中,存在一些微妙的情况,借用的引用可能在从中借用的所有者实际处理它之后被使用。

        可以通过调用Py_INCREF()将借用引用转换为拥有引用,这不会影响从中借用引用的所有者的状态 —— 它创建一个新的拥有引用,并赋予完全的所有者责任(新所有者必须正确处理引用,原所有者也是如此)。


1.10.2 所有权规则

        每当对象引用传入或传出函数时,函数接口规范的一部分是引用是否随引用转移所有权。

        大多数返回对象引用的函数会将所有权随引用传递。特别是,所有创建新对象的函数,如PyLong_FromLong()和Py_BuildValue(),都会将所有权传递给接收者。即使对象实际上不是新的,仍然会收到对该对象的新引用的所有权。例如,PyLong_FromLong()维护常用值的缓存,可以返回对缓存项的引用。

        许多从其他对象提取对象的函数也会随引用转移所有权,例如PyObject_GetAttrString()。然而,这里的情况不太清楚,因为一些常见例程是例外:PyTuple_GetItem()、PyList_GetItem()、PyDict_GetItem()和PyDict_GetItemString()都返回从元组、列表或字典借用的引用。

        函数PyImport_AddModule()也返回借用的引用,即使它实际上可能创建返回的对象:这是可能的,因为对象的拥有引用存储在sys.modules中。

        当将对象引用传递给另一个函数时,通常该函数从您那里借用引用 —— 如果需要存储它,会使用Py_INCREF()成为独立的所有者。有两个重要的例外:PyTuple_SetItem()和PyList_SetItem(),这些函数接管传递给它们的项的所有权 —— 即使它们失败!(注意PyDict_SetItem()及其相关函数不接管所有权 —— 它们是 “正常的”。)

        当从 Python 调用 C 函数时,它从调用者那里借用其参数的引用。调用者拥有对象的引用,因此借用引用的生命周期保证到函数返回。仅当必须存储或传递此类借用引用时,必须通过调用Py_INCREF()将其转换为拥有引用。

        从 Python 调用的 C 函数返回的对象引用必须是拥有引用 —— 所有权从函数转移到其调用者。


1.10.3 危险情况

        有几种情况,看似无害地使用借用引用会导致问题,这些都与解释器的隐式调用有关,这可能导致引用的所有者处理它。

        首先也是最重要的情况是,在借用列表项的引用时,对不相关的对象调用Py_DECREF()。例如:

void
bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);

    PyList_SetItem(list, 1, PyLong_FromLong(0L));
    PyObject_Print(item, stdout, 0); /* 错误! */
}


        此函数首先借用对list[0]的引用,然后将list[1]替换为值 0,最后打印借用的引用。看起来无害,对吗?但事实并非如此!

        让我们跟踪进入PyList_SetItem()的控制流。列表拥有对其所有项的引用,因此当替换项 1 时,必须处理原始项 1。现在假设原始项 1 是用户定义类的实例,并且该类定义了__del__()方法。如果此类实例的引用计数为 1,处理它将调用其__del__()方法。

        由于它是用 Python 编写的,__del__()方法可以执行任意 Python 代码。它是否可能执行某些操作使bug()中的item引用无效?当然!假设传入bug()的列表对__del__()方法是可访问的,它可以执行类似于del list[0]的语句,并且假设这是对该对象的最后一个引用,它将释放与之关联的内存,从而使item无效。

        一旦知道问题的根源,解决方案很简单:临时递增引用计数。函数的正确版本如下:

void
no_bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);

    Py_INCREF(item);
    PyList_SetItem(list, 1, PyLong_FromLong(0L));
    PyObject_Print(item, stdout, 0);
    Py_DECREF(item);
}


        这是一个真实的故事。旧版本的 Python 包含此错误的变体,有人在 C 调试器中花费了大量时间来弄清楚为什么他的__del__()方法会失败……

        借用引用的第二个问题情况是涉及线程的变体。通常,Python 解释器中的多个线程不会互相干扰,因为有一个全局锁保护 Python 的整个对象空间。然而,可以使用宏Py_BEGIN_ALLOW_THREADS暂时释放此锁,并使用Py_END_ALLOW_THREADS重新获取它。这在阻塞 I/O 调用周围很常见,以便在等待 I/O 完成时让其他线程使用处理器。显然,以下函数与前一个函数有相同的问题:

void
bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);
    Py_BEGIN_ALLOW_THREADS
    ...一些阻塞I/O调用...
    Py_END_ALLOW_THREADS
    PyObject_Print(item, stdout, 0); /* 错误! */
}

1.10.4 NULL 指针


        一般来说,接受对象引用作为参数的函数不期望传递 NULL 指针,如果这样做,将导致核心转储(或导致后续核心转储)。返回对象引用的函数通常仅在指示发生异常时返回 NULL。不测试 NULL 参数的原因是函数通常将收到的对象传递给其他函数 —— 如果每个函数都测试 NULL,会有很多冗余测试,代码运行更慢。

        最好仅在 “源头” 测试 NULL:当收到可能为 NULL 的指针时,例如来自malloc()或可能引发异常的函数。

        宏Py_INCREF()和Py_DECREF()不检查 NULL 指针 —— 但是它们的变体Py_XINCREF()和Py_XDECREF()会检查。

        检查特定对象类型的宏(Pytype_Check())不检查 NULL 指针 —— 同样,有很多代码连续调用其中几个来测试对象是否符合各种预期类型,这会生成冗余测试,没有带 NULL 检查的变体。

        C 函数调用机制保证传递给 C 函数的参数列表(示例中的args)永远不会为 NULL—— 实际上,它保证始终是一个元组 [4]。

        让 NULL 指针 “逃逸” 给 Python 用户是严重错误。


1.11 用 C++ 编写扩展

        可以用 C++ 编写扩展模块,但有一些限制。如果主程序(Python 解释器)由 C 编译器编译和链接,则不能使用带有构造函数的全局或静态对象。如果主程序由 C++ 编译器链接,则这不是问题。将由 Python 解释器调用的函数(特别是模块初始化函数)必须使用extern "C"声明。不需要将 Python 头文件括在extern "C" {...}中 —— 如果定义了符号__cplusplus(所有最新的 C++ 编译器都定义此符号),它们已经使用此形式。


1.12 为扩展模块提供 C API

        许多扩展模块只是向 Python 提供新函数和类型,但有时扩展模块中的代码对其他扩展模块可能有用。例如,扩展模块可以实现一个类型 “collection”,其工作方式类似于列表但没有顺序。就像标准 Python 列表类型有一个 C API 允许扩展模块创建和操作列表一样,这个新的集合类型应该有一组 C 函数,供其他扩展模块直接操作。

        乍一看,这似乎很容易:只需编写函数(当然不声明为静态),提供适当的头文件,并记录 C API。事实上,如果所有扩展模块总是与 Python 解释器静态链接,这将有效。然而,当模块用作共享库时,一个模块中定义的符号可能对另一个模块不可见。可见性的细节取决于操作系统;一些系统为 Python 解释器和所有扩展模块使用一个全局命名空间(例如 Windows),而其他系统需要在模块链接时显式列出导入的符号(例如 AIX),或提供不同策略的选择(大多数 Unix)。即使符号是全局可见的,希望调用其函数的模块可能尚未加载!

        因此,可移植性要求不假设符号可见性。这意味着扩展模块中的所有符号都应声明为静态,除了模块的初始化函数,以避免与其他扩展模块的名称冲突(如模块的方法表和初始化函数部分所述)。这还意味着应该从其他扩展模块访问的符号必须以不同的方式导出。

        Python 提供了一种特殊机制,用于将 C 级信息(指针)从一个扩展模块传递到另一个模块:Capsules。Capsule 是一种 Python 数据类型,存储指针(void*)。Capsule 只能通过其 C API 创建和访问,但可以像任何其他 Python 对象一样传递。特别是,可以将它们分配给扩展模块命名空间中的名称。然后,其他扩展模块可以导入此模块,检索此名称的值,然后从 Capsule 中检索指针。

        有许多方法可以使用 Capsules 导出扩展模块的 C API。每个函数可以有自己的 Capsule,或者所有 C API 指针可以存储在一个数组中,其地址发布在 Capsule 中。存储和检索指针的各种任务可以在提供代码的模块和客户端模块之间以不同方式分配。

        无论选择哪种方法,正确命名 Capsules 都很重要。函数PyCapsule_New()接受一个名称参数(const char*);允许传入 NULL 名称,但我们强烈建议指定名称。正确命名的 Capsules 提供一定程度的运行时类型安全;无法区分一个未命名的 Capsule 与另一个。

特别是,用于公开 C API 的 Capsules 应使用以下约定命名:

modulename.attributename


        便利函数PyCapsule_Import()使加载通过 Capsule 提供的 C API 变得容易,但仅当 Capsule 的名称符合此约定时。此行为使 C API 用户高度确信加载的 Capsule 包含正确的 C API。

        以下示例演示了一种方法,该方法将大部分负担放在导出模块的编写者身上,这适用于常用的库模块。它将所有 C API 指针(示例中只有一个!)存储在一个void指针数组中,该数组成为 Capsule 的值。与模块对应的头文件提供一个宏,负责导入模块并检索其 C API 指针;客户端模块在访问 C API 之前只需调用此宏。

        导出模块是简单示例中spam模块的修改版。函数spam.system()不直接调用 C 库函数system(),而是调用函数PySpam_System(),实际上它当然会做更复杂的事情(例如向每个命令添加 “spam”)。函数PySpam_System()也导出给其他扩展模块。

        函数PySpam_System()是一个普通的 C 函数,像其他所有函数一样声明为静态:

static int
PySpam_System(const char *command)
{
    return system(command);
}


        函数spam_system()以简单的方式修改:

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = PySpam_System(command);
    return PyLong_FromLong(sts);
}


        在模块的开头,紧接在#include <Python.h>行之后,必须添加另外两行:

#define SPAM_MODULE
#include "spammodule.h"


        #define用于告诉头文件它正在被包含在导出模块中,而不是客户端模块中。最后,模块的mod_exec函数必须负责初始化 C API 指针数组:

static int
spam_module_exec(PyObject *m)
{
    static void *PySpam_API[PySpam_API_pointers];
    PyObject *c_api_object;

    /* 初始化C API指针数组 */
    PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;

    /* 创建包含API指针数组地址的Capsule */
    c_api_object = PyCapsule_New((void *)PySpam_API, "spam._C_API", NULL);

    if (PyModule_Add(m, "_C_API", c_api_object) < 0) {
        return -1;
    }

    return 0;
}


        注意,PySpam_API声明为静态;否则,指针数组将在PyInit_spam()终止时消失!

        大部分工作在头文件spammodule.h中,如下所示:

#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif

/* spammodule的头文件 */

/* C API函数 */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (const char *command)

/* C API指针总数 */
#define PySpam_API_pointers 1


#ifdef SPAM_MODULE
/* 编译spammodule.c时使用此部分 */

static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;

#else
/* 在使用spammodule API的模块中使用此部分 */

static void **PySpam_API;

#define PySpam_System \
 (*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])

/* 错误时返回-1,成功时返回0。
 * 如果有错误,PyCapsule_Import将设置异常。
 */
static int
import_spam(void)
{
    PySpam_API = (void **)PyCapsule_Import("spam._C_API", 0);
    return (PySpam_API != NULL) ? 0 : -1;
}

#endif

#ifdef __cplusplus
}
#endif

#endif /* !defined(Py_SPAMMODULE_H) */

        客户端模块为了访问函数PySpam_System()必须做的所有事情,就是在其mod_exec函数中调用函数(或更确切地说是宏)import_spam():

static int
client_module_exec(PyObject *m)
{
    if (import_spam() < 0) {
        return -1;
    }
    /* 此处可以进行额外的初始化 */
    return 0;
}

        这种方法的主要缺点是文件spammodule.h相当复杂。然而,每个导出的函数的基本结构是相同的,因此只需学习一次。

        最后应该提到的是,Capsules 提供了额外的功能,这对于分配和释放 Capsule 中存储的指针的内存特别有用。详细信息在《Python/C API 参考手册》的Capsules部分和 Capsules 的实现(Python 源代码发行版中的文件Include/pycapsule.h和Objects/pycapsule.c)中描述。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值