用C或C ++扩展

如果你知道如何用C语言编程,那么为Python添加新的内置模块是很容易的。这种扩展模块可以做两件不能直接在Python中完成的事情:它们可以实现新的内置对象类型,以及调用C库函数和系统调用。

为了支持扩展,Python API(应用程序员接口)定义了一组函数、宏和变量,它们提供对Python运行时系统大部分方面的访问。Python API通过包含头文件"Python.h"被合并到C源文件中。

扩展模块的编译取决于其预期用途以及系统设置; 细节在后面的章节中给出。

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

一个简单的例子

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

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

首先创建一个文件spammodule.c(根据历史经验,如果一个模块名称为spam,那么包含其实现的C文件名称为spammodule.c;如果模块名称很长,比如spammify,那么模块名称可以是spammify.c)

我们文件的第一行是:

#include <Python.h>

它引入了Python API(如果你喜欢,你可以添加一个描述模块用途的注释和一个版权声明)。

注意:由于Python可能会定义一些影响某些系统上标准头文件的预处理器定义,因此必须在包含Python.h任何标准头文件之前包含这些定义。

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

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

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中的参数列表和C函数的参数列表之间有一个简单的映射关系(例如单个表达式"ls -l")。C函数总是有两个参数,通常命名为self和args。

对于模块级别的函数self变量指向模块对象,对于对象方法self变量指向对象实例。

ARGS参数指向包含参数的Python元组对象。元组中的每一项都对应于调用参数列表中的参数。参数是Python对象 - 为了在C函数中对它们做任何事情,我们必须将它们转换为C值。Python API中的PyArg_ParseTuple()函数用于参数类型检查并将它们转换为C值。它使用模板字符串来确定参数的所需类型以及存储转换值的C变量的类型,稍后再详细介绍。

如果所有参数都具有正确的类型并且被存储到传递的变量地址中,则PyArg_ParseTuple()返回true(非零)。如果传递了一个无效参数列表,它将返回false(零)。在后一种情况下,它也会引发适当的异常,所以调用函数可以立即返回NULL(如我们在示例中所见)。

Intermezzo:错误和例外

整个Python解释器中一个重要的约定如下:当一个函数失败时,它应该设置一个异常条件并返回一个错误值(通常是一个NULL指针)。异常存储在解释器内的静态全局变量中,如果此变量为NULL,则不会发生异常。第二个全局变量存储异常的"关联值"(第二个参数raise)。第三个变量包含栈回溯,以防错误发生在Python代码中。这三个变量是Python中sys.exc_info()执行结果的C等价物(请参阅sysPython库参考中模块部分)。了解他们了解错误是如何传递的非常重要。

Python API定义了许多函数来设置各种类型的异常。

最常见的是PyErr_SetString()。它的参数是一个异常对象和一个C字符串:异常对象通常是一个预定义的对象例如PyExc_ZeroDivisionError;C字符串指示错误的原因,并转换为Python字符串对象存入异常的"关联值"。

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

通过调用PyErr_Occurred()您可以非破坏性地测试是否设置了例外,它将返回当前的异常对象, 如果没有发生异常,则返回NULL。通常您不需要调用PyErr_Occurred()以查看函数调用是否发生错误,因为您应该能够根据返回值进行分析。

当调用另一个函数g的函数f检测到后者失败时,f本身应该返回一个错误值(通常为NULL或-1)。它不应该再调用任何PyErr_*()函数 - 因为g已经调用了。f的调用者应该也返回一个错误指示它的调用者,同样不需要再调用任何PyErr_*()函数,然后继续 - 错误的最详细原因已经由首次检测到它的函数报告(例如此处的g函数)。一旦错误到达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()调用)!

选择哪个异常来完全取决于你。有预先声明的C对象与所有内置的Python异常相对应,比如 PyExc_ZeroDivisionError,您可以直接使用它,但你应该明智地选择异常 - 不要用PyExc_TypeError来表示文件无法打开(应该可能PyExc_IOError)。如果参数列表有问题,PyArg_ParseTuple()函数通常会引发PyExc_TypeError异常。如果需要表示一个其值必须在一个特定的范围或必须满足其他条件的异常,PyExc_ValueError是适当的。

您也可以为模块定义一个新的异常,通常您需要在文件的开头声明一个静态对象变量:

static PyObject *SpamError;

并在模块的初始化函数(PyInit_spam())中使用一个异常对象初始化它(现在先忽略错误检查部分):

PyMODINIT_FUNC
PyInit_spam(void)
{
PyObject *m; m = PyModule_Create(&spammodule);
if (m == NULL)
return NULL; SpamError = PyErr_NewException("spam.error", NULL, NULL);
Py_INCREF(SpamError);
PyModule_AddObject(m, "error", SpamError);
return m;
}

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

还要注意,该SpamError变量保留对新创建的异常类的引用,这是故意的!由于可以通过外部代码从模块中删除异常,所以需要拥有该类的引用,来确保SpamError不会因为被丢弃,从而成为悬挂指针。如果它成为悬挂指针,引发异常的C代码可能会导致崩溃或其他意外副作用。

稍后在本示例中我们将讨论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);
}

回到示例

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

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指针,这在大多数情况下都意味着"错误"。

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

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

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

请注意第三项(METH_VARARGS)。这是一个标志,告诉解释器用于C函数的调用约定。它通常应该是METH_VARARGSMETH_VARARGS | METH_KEYWORDS

仅使用METH_VARARGS时,函数期望将Python级的参数作为能够被PyArg_ParseTuple()解析的元组传递进来,关于这个功能的更多信息在下面提供。

如果关键字参数应该传递给函数,那么METH_KEYWORDS位将被设置。在这种情况下,C函数应该接受第三个参数,该参数将成为关键字字典,使用类似PyObject *PyArg_ParseTupleAndKeywords()的函数来解析参数。

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

static struct PyModuleDef spammodule = {
PyModuleDef_HEAD_INIT,
"spam", /* name of module */
spam_doc, /* module documentation, may be NULL */
-1, /* size of per-interpreter state of the module,
or -1 if the module keeps state in global variables. */
SpamMethods
};

这个结构又必须在模块的初始化函数中传递给解释器,初始化函数必须被命名PyInit_name(),其中name是模块的名称,并且应该是模块文件中唯一定义的非static项目:

PyMODINIT_FUNC
PyInit_spam(void)
{
return PyModule_Create(&spammodule);
}

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

当Python程序第一次导入spam模块时, PyInit_spam()被调用。它调用PyModule_Create()返回模块对象,并根据PyMethodDef模块定义中的表(结构数组)插入内置函数对象到新创建的模块中。 PyModule_Create()返回一个指向它创建的模块对象的指针。如果因致命错误而中止或者模块初始化失败,则返回NULL。init函数必须将模块对象返回给其调用者,以便将其插入sys.modules

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

int
main(int argc, char *argv[])
{
wchar_t *program = Py_DecodeLocale(argv[0], NULL);
if (program == NULL) {
fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
exit(1);
} /* Add a built-in module, before Py_Initialize */
PyImport_AppendInittab("spam", PyInit_spam); /* Pass argv[0] to the Python interpreter */
Py_SetProgramName(program); /* Initialize the Python interpreter. Required. */
Py_Initialize(); /* Optionally import the module; alternatively,
import can be deferred until the embedded script
imports it. */
PyImport_ImportModule("spam"); ... PyMem_RawFree(program);
return 0;
}

注意:从sys.modules删除条目或将编译的模块导入进程中的多个解释器(或者在fork()没有插入的情况下执行exec())会导致某些扩展模块出现问题。扩展模块作者在初始化内部数据结构时应该谨慎行事。

Python源代码分发中包含了一个更实质性的示例模块Modules/xxmodule.c。这个文件可以作为模板使用,或者只是作为一个例子阅读。

注意 与我们的spam示例不同,xxmodule它使用多阶段初始化 (Python 3.5中的新增功能),从PyInit_spam中返回PyModuleDef结构 ,并且将模块创建留给导入机制。

编译和链接

在使用新扩展之前,还有两件事要做:编译并将其与Python系统链接。如果使用动态加载,细节可能取决于系统使用的动态加载样式,查阅Building C and C++ Extensions以及Building C and C++ Extensions on Windows获取更多信息。

如果你不能使用动态加载,或者如果你想让你的模块成为Python解释器的一个永久部分,你将不得不改变配置设置并重建解释器。幸运的是,在Unix上这非常简单:只需将您的文件(spammodule.c例如)放入Modules/解压源代码发行版的目录中,然后在Modules/Setup.local描述文件的文件中添加一行 :

spam spammodule.o

并通过在顶级目录中运行make来重建解释器。您也可以在Modules/子目录中运行make,但是必须先通过运行make Makefile来重新编译Makefile。(每次更改Setup文件时都需要这样做)

如果您的模块需要额外的库进行链接,这些库也可以在配置文件的行中列出,例如:

spam spammodule.o -lX11

从C调用Python函数

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

幸运的是,Python解释器很容易被递归调用,并且有一个标准的接口来调用Python函数。(我不会详细讨论如何使用特定的字符串作为输入来调用Python解析器 - 如果您有兴趣,请查看Python源代码中的-c命令行选项的实现Modules/main.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); /* Add a reference to new callback */
Py_XDECREF(my_callback); /* Dispose of previous callback */
my_callback = temp; /* Remember new callback */
/* Boilerplate to return "None" */
Py_INCREF(Py_None);
result = Py_None;
}
return result;
}

该功能必须使用METH_VARARGS标志向解释器注册,这在The Module’s Method Table and Initialization Function一节中有描述。PyArg_ParseTuple()函数及其参数说明记录在Extracting Parameters in Extension Functions

宏Py_XINCREF()和Py_XDECREF()用来增加/减少一个对象的引用计数,即使是NULL指针也是安全的(但请注意,在这种情况下temp不会为NULL)。有关它们的更多信息,请参阅参考计数部分。

稍后,当需要调用该函数的时候,您可以使用C函数PyObject_CallObject()。这个函数有两个参数,都是指向任意Python对象的指针:Python函数和参数列表。参数列表必须始终是一个元组对象,其长度是参数个数。调用没有参数的Python函数时,传入NULL或空元组; 调用一个参数的Python函数时,传递一个单例元组。当其格式字符串由零或更多格式代码组成时,Py_BuildValue()返回一个元组。例如:

int arg;
PyObject *arglist;
PyObject *result;
...
arg = 123;
...
/* Time to call the callback */
arglist = Py_BuildValue("(i)", arg);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);

PyObject_CallObject()返回一个Python对象指针:这是Python函数的返回值。在这个例子中,创建了一个新的元组作为参数列表,该列表在调用PyObject_CallObject()后通过Py_DECREF调用被立即释放。

返回值PyObject_CallObject()是"new":它是一个全新的对象或者它是引用计数已递增的现有对象,所以除非你想把它保存在一个全局变量中,否则你应该以某种方式对得到的结果执行Py_DECREF(),甚至(尤其是!)如果你对它的价值不感兴趣。

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

if (result == NULL)
return NULL; /* Pass error back */
...use result...
Py_DECREF(result);

根据Python回调函数接口,您可能还需要提供参数列表给PyObject_CallObject()。您可能需要构造一个新的元组作为参数列表,最简单的方法就是调用Py_BuildValue()。例如您想传递一个整型事件编码,则可以使用以下代码:

PyObject *arglist;
...
arglist = Py_BuildValue("(l)", eventcode);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
if (result == NULL)
return NULL; /* Pass error back */
/* Here maybe use the result */
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; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);

在扩展函数中提取参数

PyArg_ParseTuple()函数声明如下:

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

ARG参数必须是包含Python传递给C函数的参数列表的元组对象。format参数必须是一个格式字符串,其语法在Python/C API参考手册Parsing arguments and building values中给出。其余的参数必须是变量的地址,其类型由格式字符串决定。

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

请注意,提供给调用者的任何Python对象引用都是借用引用,不要减少他们的参考计数!

一些示例调用:

#define PY_SSIZE_T_CLEAN  /* Make "s#" use Py_ssize_t rather than int. */
#include <Python.h>
int ok;
int i, j;
long k, l;
const char *s;
Py_ssize_t size; ok = PyArg_ParseTuple(args, ""); /* No arguments */
/* Python call: f() */
ok = PyArg_ParseTuple(args, "s", &s); /* A string */
/* Possible Python call: f('whoops!') */
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* Two longs and a string */
/* Possible Python call: f(1, 2, 'three') */
ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
/* A pair of ints and a string, whose size is also returned */
/* Possible Python call: f((1, 2), 'three') */
{
const char *file;
const char *mode = "r";
int bufsize = 0;
ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
/* A string, and optionally another string and an integer */
/* Possible Python calls:
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);
/* A rectangle and a point */
/* Possible Python call:
f(((0, 0), (400, 300)), (10, 10)) */
}
{
Py_complex c;
ok = PyArg_ParseTuple(args, "D:myfunction", &c);
/* a complex, also providing a function name for errors */
/* Possible Python call: myfunction(1+2j) */
}

扩展函数的关键字参数

PyArg_ParseTupleAndKeywords()函数声明如下:

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

arg和format参数含义同PyArg_ParseTuple()函数,kwdict参数用来接收来自Python运行时的第三参数的关键字词典,kwlist参数是以NULL结尾用于标识参数的字符串列表。成功时PyArg_ParseTupleAndKeywords()返回true,否则返回false,并引发异常。

注意:使用关键字参数时不能分析嵌套元组!传入的关键字参数在kwlist不存在会导致TypeError异常。

下面是一个使用关键字的例子:

#include "Python.h"

static PyObject *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
int voltage;
char *state = "a stiff";
char *action = "voom";
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[] = {
/* The cast of the function is necessary since PyCFunction values
* only take two PyObject* parameters, and keywdarg_parrot() takes
* three.
*/
{"parrot", (PyCFunction)keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,
"Print a lovely skit to standard output."},
{NULL, NULL, 0, NULL} /* sentinel */
}; static struct PyModuleDef keywdargmodule = {
PyModuleDef_HEAD_INIT,
"keywdarg",
NULL,
-1,
keywdarg_methods
}; PyMODINIT_FUNC
PyInit_keywdarg(void)
{
return PyModule_Create(&keywdargmodule);
}

建立任意值

函数PyArg_ParseTuple()声明如下:

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

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

与PyArg_ParseTuple()不同的是:后者要求其第一个参数是一个元组(因为Python参数列表总是在内部表示为元组),Py_BuildValue()并不总是构建一个元组。它仅在格式字符串包含两个或更多格式单元时才构建元组。如果格式字符串为空,则返回None; 如果它只包含一个格式单元,则返回该格式单元描述的任何对象。要强制它返回一个大小为0或1的元组,使用括号包裹format字符串。

示例(在左边的调用,右边的结果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))

参考计数

在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()功能)的方法,以及配置接口和在运行时禁用探测器的功能。循环检测器被视为可选组件,虽然它是默认包含的,但它可以在构建时使用Unix平台(包括Mac OS X)上--without-cycle-gc的configure脚本选项来禁用,如果循环检测器以这种方式被禁用,gc模块将不可用。

Python中的引用计数

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

现在最大的问题仍然是:何时使用Py_INCREF(x)和Py_DECREF(x)?我们先来介绍一些术语。没有人"拥有"一个物体,但是您可以拥有对象的引用。现在对象的引用计数定义为拥有它的引用的数量。当不再需要引用时,引用的所有者负责调用Py_DECREF()。引用的所有权可以转移。有三种方式可以处理拥有的引用:传递它、存储它或调用Py_DECREF(),忘记处理拥有的引用会造成内存泄漏。

也可以借用对象的引用,引用的借用者不应该调用Py_DECREF(),引用的借用者不能比被借用者持有对象时间更长。被借用者释放对象后,借用者继续使用对象会有风险,应该完全避免。

借用引用的优势在于,你不需要关心引用的处理,不会因为提前返回导致内存泄露,缺点就是可能存在使用已经释放了的对象。

借用引用可以通过调用Py_INCREF()变成引用拥有者,这不会影响借用出引用的拥有者的地位 - 它会创建一个新的拥有引用,并给予全部的拥有责任(新拥有者必须正确处理引用就像之前的拥有者那样)。

所有权规则

无论何时将对象引用传入或传出函数,它都是函数接口规范的一部分,而不管所有权是否与引用一起传输。

大多数返回对象引用的函数都会将引用的所有权传递给它,特别是创建新对象的函数,例如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函数返回的对象引用必须是拥有引用 - 拥有权将从函数传递给调用者。

雷区

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

要了解的第一个也是最重要的案例是在不相关对象上使用Py_DECREF(),同时借用对列表项的引用。例如:

void
bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0); PyList_SetItem(list, 1, PyLong_FromLong(0L));
PyObject_Print(item, stdout, 0); /* BUG! */
}

该函数首先借用一个引用list[0],然后用0替换list[1]的值,最后打印借用的引用。看起来没什么问题,对吧?但实际上不是!

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

由于它是用Python编写的,所以该__del__()方法可以执行任意的Python代码。它也执行了一些导致item引用无效的bug()函数?假设传入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包含了这个bug的变种,有人花费了大量的时间在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
...some blocking I/O call...
Py_END_ALLOW_THREADS
PyObject_Print(item, stdout, 0); /* BUG! */
}

空指针

一般来说,将对象引用作为参数的函数并不期望你传递NULL指针给它们,并且如果你这样做的话,将会导致崩溃。返回对象引用的函数通常仅当发生异常才返回NULL,不测试NULL参数的原因是函数通常会将它们接收到的对象传递给其他函数 - 如果每个函数都要测试NULL,则会有大量冗余测试,并且代码运行速度会更慢。

当收到一个可能为NULL的指针时,最好仅在最开始处测试NULL,例如在malloc()或可能引发异常的函数返回处测试。

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

用于检查特定对象类型的宏(Pytype_Check())不会检查NULL指针 - 因为有很多代码在针对各种不同的预期类型时,会连续调用几个宏来测试一个对象,如果检查NULL指针的话会产生冗余测试,所以对于检查特定对象类型的宏没有带NULL检查的变体。

C函数调用机制保证传递给C函数的参数列表(args在这个例子中)永远不是NULL - 事实上它保证它总是一个元组。

让一个NULL指针"逃到"Python用户这是一个严重的错误。

在C++中编写扩展

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

为扩展模块提供C

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

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

因此可移植性不需要对符号可见性做出任何假设,这意味着除了模块的初始化函数之外,扩展模块中的所有符号都应声明为static,以避免与其他扩展模块发生名称冲突(如The Module’s Method Table and Initialization Function所述),这意味着从其他扩展模块访问的符号必须以不同的方式导出。

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

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

无论您选择哪种方法,正确命名Capsules非常重要。函数PyCapsule_New()接受一个const char *类型的名称参数,您可以传入一个NULL,但我们强烈建议您指定一个名称。

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

modulename.attributename

PyCapsule_Import()函数可以非常方便的加载Capsule提供的C API,但前提是Capsule的名称符合此惯例,这种行为使得C API用户可以高度肯定他们加载的Capsule包含正确的C API。

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

导出模块是对A Simple Example模块spam部分的修改。函数spam.system()并不直接调用C库函数system(),而是PySpam_System()函数,现实中它当然会做一些更复杂的事情(比如为每个命令添加spam"), PySpam_System()函数也被导出到其他扩展模块。

函数PySpam_System()是一个简单的C函数,像其他一样声明为static:

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指明头文件被包含在导出模块,而不是客户端模块。最后模块的初始化函数必须注意初始化C API指针数组:

PyMODINIT_FUNC
PyInit_spam(void)
{
PyObject *m;
static void *PySpam_API[PySpam_API_pointers];
PyObject *c_api_object; m = PyModule_Create(&spammodule);
if (m == NULL)
return NULL; /* Initialize the C API pointer array */
PySpam_API[PySpam_System_NUM] = (void *)PySpam_System; /* Create a Capsule containing the API pointer array's address */
c_api_object = PyCapsule_New((void *)PySpam_API, "spam._C_API", NULL); if (c_api_object != NULL)
PyModule_AddObject(m, "_C_API", c_api_object);
return m;
}

请注意PySpam_API声明的是static,否则指针数组在PyInit_spam()终止时会消失!

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

#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif /* Header file for spammodule */ /* C API functions */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (const char *command) /* Total number of C API pointers */
#define PySpam_API_pointers 1 #ifdef SPAM_MODULE
/* This section is used when compiling spammodule.c */ static PySpam_System_RETURN PySpam_System PySpam_System_PROTO; #else
/* This section is used in modules that use spammodule's API */ static void **PySpam_API; #define PySpam_System \
(*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM]) /* Return -1 on error, 0 on success.
* PyCapsule_Import will set an exception if there's an error.
*/
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(),客户端模块必须在其初始化函数中调用函数(或者说宏)import_spam():

PyMODINIT_FUNC
PyInit_client(void)
{
PyObject *m; m = PyModule_Create(&clientmodule);
if (m == NULL)
return NULL;
if (import_spam() < 0)
return NULL;
/* additional initialization can happen here */
return m;
}

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

(一)用C或C ++扩展(翻译)的更多相关文章

  1. 简单服务端缓存API设计

    Want 我们希望设计一套缓存API,适应不同的缓存产品,并且基于Spring框架完美集成应用开发. 本文旨在针对缓存产品定义一个轻量级的客户端访问框架,目标支持多种缓存产品,面向接口编程,目前支持简 ...

  2. 【翻译】理念:无冲突的扩展本地DOM原型

    菜鸟翻译,望大家多多指正哈 原文:http://lea.verou.me/2015/04/idea-extending-native-dom-prototypes-without-collisions ...

  3. 添加了有道生词本的 chrome google翻译扩展和有道翻译扩展

    在chrome发布项目,需要先花美金认证,还得要美国ID,无奈. 直接上源码,需手动导入. 原始项目源码并未开源,个人是从chrome本地文件里拿出来的,拓展来的,侵删(本来想着自已写一个,业余时间, ...

  4. [翻译]opengl扩展教程2

    [翻译]opengl扩展教程2 原文地址https://www.opengl.org/sdk/docs/tutorials/ClockworkCoders/extensions_part2.php [ ...

  5. [翻译]opengl扩展教程1

    [翻译]opengl扩展教程1 原文地址https://www.opengl.org/sdk/docs/tutorials/ClockworkCoders/extensions.php [翻译]ope ...

  6. 【翻译】我钟爱的Visual Studio前端开发工具/扩展

    原文:[翻译]我钟爱的Visual Studio前端开发工具/扩展 怎么样让Visual Studio更好地编写HTML5, CSS3, JavaScript, jQuery,换句话说就是如何更好地做 ...

  7. [翻译].NET Shell Extensions - Shell Context Menus---.net 外壳扩展-右键菜单

    我自己的前言说明: 本文原作者为    Dave Kerr,原文链接为.NET Shell Extensions - Shell Context Menus:,我是在为了完成最新需求的时候查询资料的时 ...

  8. JavaScript 作用域和闭包——另一个角度:扩展你对作用域和闭包的认识【翻译+整理】

    原文地址 --这篇文章有点意思,可以扩展你对作用域和闭包的认识. 本文内容 背景 作用域 闭包 臭名昭著的循环问题 自调用函数(匿名函数) 其他 我认为,尝试向别人解释 JavaScript 作用域和 ...

  9. 翻译 | “扩展asm”——用C表示操作数的汇编程序指令

    本文翻译自GNU关于GCC7.2.0版本的官方说明文档,第6.45.2小节.供查阅讨论,如有不当处敬请指正…… 通过扩展asm,可以让你在汇编程序中使用C中的变量,并从汇编代码跳转到C语言标号.在汇编 ...

  10. [翻译]扩展C#中的异步方法

    翻译自一篇博文,原文:Extending the async methods in C# 异步系列 剖析C#中的异步方法 扩展C#中的异步方法 C#中异步方法的性能特点. 用一个用户场景来掌握它们 在 ...

随机推荐

  1. html收藏

    全屏显示<input type="button" name="fullscreen" value="全屏显示" onclick=&qu ...

  2. y3pP5nCr攀科汲野奶园 O8XY02cm脱罕谘诜驮仆补殖沦ltGLD71R

    {字母=2}谘们土毁低聊临禄霉{字母=3}焚派匠莆胺慷{字母=3}孔毡沃卮肪{字母=1}}{字母=1}尚澈心于逃丫导九壮何前僚九粤绦剖逃仲寺椿澈裳枚盟裳鹊酱滥食孤罕胤狼鞘孜跋柿悸菇沽惫菇卮认鹿锤敦擞众 ...

  3. 2&nbsp;时间管理和内存管理

    时间管理 uC/OS-II的时间管理是通过定时中断来实现的,该定时中断一般为10毫秒或100毫秒发生一次(这个时间片段是OS的作者推荐的,大家可以参考邵贝贝翻译的<嵌入式实时操作系统ucos-I ...

  4. mac配置git mergetool为p4merge(2013笔记整理)

    既有环境: 1)下载安装p4merge 2)安装git 下面是配置p4merge为git mergetool的步骤 1. /usr/local/bin下创建extMerge文件: $ cat > ...

  5. Write your first jQuery plugin

    本文固定链接: http://www.jquery.org.cn/archives/380 一般来说,jQuery插件的开发分为两种:一种是挂在jQuery命名空间下的全局函数,也可称为静态方法:另一 ...

  6. js通过session判断登录与否并确定跳转页面以及回车按钮提交

    本文实例讲述了js判断登录与否并确定跳转页面的方法.分享给大家供大家参考.具体如下: 使用session存储,确定用户是否登录,从而确定页面跳转至哪个页面. 判断本地有无customerID func ...

  7. 2-chrome无法添加扩展程序

    1.更多工具->拓展程序->打开开发者模式->重启浏览器 2.将拓展程序拖入,确认安装

  8. Luogu 2403 [SDOI2010]所驼门王的宝藏

    BZOJ 1924 内存要算准,我MLE了两次. 建立$n + r + c$个点,对于一个点$i$的坐标为$(x, y)$,连边$(n + x, i)$和$(n + r + y, i)$,代表这一列和 ...

  9. Part10-C语言环境初始化-Bss段初始化lesson2

    1.BSS段的作用 初始化的全局变量存放在数据段: 局部变量存放在栈中: malloc的存放在堆: 未初始化的全局变量存放在BSS段: 找到bss段的起始与结束地址,往里面添加0,便初始化好了. 打开 ...

  10. 解决IE与FF 中 input focus 光标移动在最后的方案

    只要把input元素的id传进来即可 function moveCursor(id)  { var id = document.getElementById(id); id.focus(); var  ...