Extending and Embedding the Python Interpreter
本文档主要描述,如何通过C/C+-来创建新的Rython模块(module),从而扩展Python解释器(的使用)。这些Python module不仅可以定义新函数,而且还能定义新的对象类型(class)和对应的(类)方法。
本文档同样也描述了,如果把Python解释器嵌入另外一个程序,从而当做一个扩展语言使用。
最后,文档也展示了,当操作系统支持的时候,如何编译并链接这些扩展模块,以便在(程序)运行时,它们能够被解释器动态地加载。
本文档假定读者具备Python的基本知识。关于Python这门语言的非正式的介绍,可以查看The python Tutorial,而The Python Language Reference给出了Python更正式的定义The Python Standard Library描述了现有的对象类型(object types),函数(functions)和模块 (modules),它们都是使用Python编写,并且内置的,它们都有着广泛的应用范围关开完整的Python/C API接体文档,可以查看单独的Python/C API Reference Manual
Words
exercise caution 谨慎行事
推荐的第三方工具
本指南只介绍了在此版本的CPython下,利用其提供的基本工具,创建扩展模块。其他第三方程序,如 Cython, cffi, SWIG 以及 Numba 都提供了更简单便捷和更复杂功能多样的方法,用来创建Python的C和C++的扩展。
Python Packaging User Guide: Binary Extensions 不仅介绍了几种可用工具,它们使得创建二进制扩展更加便捷,而且还讨论了为什么创建一个扩展模块或许是首要考虑的各种原因。
不使用第三方工具来创建扩展
这一节主要讲述,在不使用第三方工具的情况下,如何创建出C/C++的扩展模块,这主要是为了这些第三方工具的作者使用,并不是鼓励使用这种办法创建自己的C/C++的扩展模块。
把CPython runtime嵌入更大的应用中
相比于创建一个运行于Python解释器当中的扩展当做主应用程序,有时候,更希望把CPython runtime嵌入到一个更大的应用程序中。这部分讲述了成功达成这个目的的一些细节。
1. Extending Python with C or C++
如果懂得如何使用C语言编程,那么很容易在Python中添加新的内置模块(built-in module)。这种扩展模块可以做到在Python中做不到的两件事(1)它们可以实现新的内置对象类型(built-in object type)(2)它们可以调用C的库函数和系统调用。
为了支持这样的扩展模块,Python应用程序接口(API,Application Programmers Interface)定义了一系列的函数、宏以及变量,它们被用来访问Python运行系统的各个方面。Python的这些API在C源文件中以include "Python.h"的方式被包含进来。
一个扩展模块的编译,不仅依赖于它的用途,而且也依赖于系统设置,后面的章节会给出更多的细节。
注意:C的扩展接口(函数)是CPython特有的,这样的扩展模块在其他实现方式的Python上是不工作的。在大多数情况下,可以避免撰写C的扩展,并且可以保留对其他实现方式的Python的可移植性。比如,如果使用场景是调用C的库函数或者调用系统函数,那么就可以考虑使用 ctype 这个Python模块,或者 cffi library,而不是编写自己的C代码。这些模块能够使你通过写Python代码的方式和(对应的)C代码打交道,并且在各个不同的Python实现上,比起自己撰写C代码和编译,更具有移植特性。
1.1. A Simple Example
下面举例,创建一个叫做 spam 的扩展模块,并假设要生成一个Python的接口,用来调用C的库函数 system()。这个函数接受一个以 \0 结尾的字符串作为参数,并且返回一个整型值。假设如下在Python中调用这个函数。
import spam
status = spam.system("ls -l")
我们以创建一个叫做 spammodule.c 的文件开始。
根据历史约定俗成,如果一个模块叫做 spam,那么包含它的实现的C文件就命名为 spammodule.c;如果模块的名字很长,比如 spammify,那么模块(文件的)名字就可以直接叫做 spammify.c。
这个C文件开始的两行是,
#define PY_SSIZE_T_CLEAN
#include <Python.h>
这样就拉取了 Python.h 中包含的Python API,当然可以加入一些注释,用来说明该模块的作用,以及一些版权信息。
注意,由于Python定义了一些预编译的宏,它们可能在某些系统上影响标准库的头文件,所以(保险起见),必须在包含其他标准库头文件之前,首先包含该头文件,即 #include <Python.h>。
同时,也建议在 #include <Python.h> 之前,总是定义宏 PY_SSIZE_T_CLEAN,关于该宏的描述,参考 Extracting Parameters in Extension Functions。
在 Python.h 中,所有用户可见的标识符,都有前缀 Py 或 PY,只有在标准库中定义的变量是例外。这是由于它们(标准库中的这些标识符)被Python解释器大量地使用,Python.h 包含的一些标准库的文件比如:<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 指针参数指向的是模块对象,为的是访问模块层级的函数;对于一个Python method,它指向的是一个对象实例。
args 指针参数指向的是包含有参数的Python元组(tuple)。这个元组中的每一项对应的就是调用参数列表中的一个参数。因为参数是Python对象,为了在我们的C函数中使用,就需要将其转换为C的值。Python API中的函数 [PyArg_ParseTuple()](file:///D:/procs/python-3.11.4-docs-html/c-api/arg.html#c.PyArg_ParseTuple) 就是用来检查参数类型,并将其转换为C值。它使用一个字符串模板来决定所需的参数类型,以及用来存储转换后的C值的C变量。
如果所有参数的类型正确,并且其对应的C值存入到了传入的地址(对应的内存)中,函数 [PyArg_ParseTuple()](file:///D:/procs/python-3.11.4-docs-html/c-api/arg.html#c.PyArg_ParseTuple) 返回 true (非零)。如果传入的是一个不正确的参数列表,那么函数 [PyArg_ParseTuple()](file:///D:/procs/python-3.11.4-docs-html/c-api/arg.html#c.PyArg_ParseTuple) 返回 false (零)。在后者的情况中,它同时会抛出一个合适的异常,据此调用它的函数就能立即返回 NULL。
1.2. Intermezzo: Errors and Exceptions
贯穿于Python解释器中一个约定是,当一个函数执行失败时,它应该设置一个异常条件,并且返回一个错误值(通常是 -1 或 NULL 指针)。异常信息则被存储在了Python解释器的三个线程安全的变量中。当没有异常的时候,它们都是 NULL。当存在异常的时候,它们是和 [sys.exc_info()](file:///D:/procs/python-3.11.4-docs-html/library/sys.html#sys.exc_info) 返回的Python元组对应的三个C变量。了解它们对于理解错误(信息)是如何传递的很重要。
Python API定义了一系列的函数,用来设定各种类型的异常。
最常用的函数是 is [PyErr_SetString()](file:///D:/procs/python-3.11.4-docs-html/c-api/exceptions.html#c.PyErr_SetString)。它的参数一个异常对象和一个C字符串。这个异常对象通常是预定义的对象,比如 PyExc_ZeroDivisionError。这C字符串描述了错误发生的原因,并且它会被转换为Python字符串对象,然后存储在和异常关联的值上。
另一个有用的函数是 PyErr_SetFromErrno(),它只接收一个异常参数,然后根据这个查询一个全局变量 errno 来构造对应的关联值。最一般化的函数是 PyErr_SetObject(),它接收两个对象参数,一个异常,一个和这个异常关联的值。你不需要对传入这些函数的对象调用 Py_INCREF()。
你可以通过非侵入式的测试,来检查一个异常是否由 PyErr_Occurred() 设定。它返回当前的异常对象,当没有异常时则返回 NULL。一般情况下,你不需要通过调用PyErr_Occurred() 来确定在函数调用中是否发生了错误,因为你能够从函数的返回值中就可以得知。
当一个函数 \(f\) 调用另一个函数 \(g\),并且后者失败了,那么函数 \(f\) 本身就应该返回一个错误值(通常是 NULL 或 -1)。它不应该调用那些 PyErr_* 函数,因为这些函数通常已经在 \(g\) 中被调用过了。同样的,调用 \(f\) 的函数也不需要调用那些 PyErr_* 函数,它也只需返回一个错误指示给调用它的函数,以此类推,因为关于错误最详细的发生原因,已经由第一个检测到它的函数报告过了。一旦这个错误到达Python解释器的主循环,它就会中断执行当前的Python代码,然后试图找到一个由Python编程者指定的异常处理机制。
(确实有一些情况下,模块实际上可以通过调用些 PyErr_* 函数来给出关于错误的更详细的信息, 那么在这种情况下,这样做是合适的。然而,按照一般原则,这不是必需的,而且有可能造成关于错误信息丢失的情况,因为大部分操作有可能因为各种各样的问题而失败)
为了忽略一个由函数调用失败引起的异常,那么异常条件就必须显式地使用 PyErr_Clear() 来清除掉。在C代码中,显式调用 PyErr_Clear() 的唯一情况是,它不想把这个错误传递给Python解释器,而是想独自完全处理它(比如很可能尝试其他操作,或者装作无事发生)。
每次调用 malloc() 就必须抛异常,当直接调用 malloc() 或 realloc() 失败时必须调用 PyErr_NoMemory(),然后返回一个错误指示。所有创建了对象的函数(比如 PyLong_FromLong())已经做了这样的事情,所以这是给那些直接调用 malloc() 相关的代码的提示。
同样要注意,除了 PyArg_ParseTuple() 这个重要的例外,对于返回一个整型值表示状态的函数,应该返回一个正值或 0 表示成功,-1表示失败,就像Unix系统调用一样。
最后,当返回一个错误指示的时候,如果要清理“垃圾”时要小心,比如对已经创建的对象调用Py_XDECREF() 或 Py_DECREF()。
选择抛出什么样的异常完全由调用者自己决定。对于Python内置的异常,都有对应的预定义好的C对象,比如 PyExc_ZeroDivisionError,而这些是可以直接使用的。当然,应该合理地选择要抛出的异常,比如,如果要表示一个文件无法打开,就不应该使用 PyExc_TypeError,而应该使用 PyExc_OSError。如果有参数列表错误,函数 PyArg_ParseTuple() 通常抛出异常 PyExc_TypeError。如果有一个参数的值不符合想要的范围,或没有满足某些条件,那么抛出异常 PyExc_ValueError 是合适的。
当然也可以在模块中定义一个新的异常,这个异常就是对这个模块独有的。为了定义这个异常,通常在(模块)文件的开始声明一个 static 静态变量(指针):
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_XINCREF(SpamError);
if (PyModule_AddObject(m, "error", SpamError) < 0) {
Py_XDECREF(SpamError);
Py_CLEAR(SpamError);
Py_DECREF(m);
return NULL;
}
return m;
}
注意,这里给出的异常名称,在Python中是 spam.error。函数 PyErr_NewException() 创建的class的基类是 Exception(除法传入的参数不是 NULL 而是其他class),它在中 Built-in Exceptions 有描述。
还要注意,SpamError 变量保留了一个指向新创建的异常类的引用,而这是故意如此!因为这个异常可以被该模块以外的代码删除,所以为了防止 SpamError 变成一个dangling指针,一个指向这个类的owned reference就应该被保留。如果它变成了一个dangling指针,那么要抛出这个异常的C代码就可能产生core dump,或者其他非预期的副作用。
关于 PyMODINIT_FUNC 被当做一个函数返回值来使用的细节,我们后面再讨论。
这个定义好的 spam.error 异常,就可以在扩展模块中使用 PyErr_SetString() 函数来抛出,如下:
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. Back to the Example
现在回到我们之前的例子,现在应该就能理解下面的语句:
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
当检测到参数列表中有错误出现时,它就会返回 NULL。这个错误是根据 PyArg_ParseTuple() 内部的异常所设置的,这里的 NULL 是错误指示器,用在那些返回值是对象指针的函数里。如果参数列表正常,字符串里的值就被拷贝到局部变量 command 中去。这是一个指针赋值,而且不应该修改这个指针指向的字符串(因此在标准C中,变量 command 应该被声明为 const char *command,这样更合适)。
下面的语句就是调用Unix函数 system(),把从 PyArg_ParseTuple() 得到的字符串作为参数传入:
sts = system(command);
因为我们的 spanm.system() 必须将 sts 以Python对象的形式返回,所以使用函数 PyLong_FromLong() 然后返回:
return PyLong_FromLong(sts);
这种情况下,函数返回一个整型对象(是的,在Python里面,整型数实际上也是位于堆上的对象!)。
如果你的C函数返回一个没有什么用的参数(即函数返回 void),那么对应的Python函数就应该返回 None,你就需要使用如下语句习惯来实现:
Py_INCREF(Py_None);
return Py_None;
这里的 Py_None 是Python中特殊对象 None 在C中的名字,它是一个标准的Python对象,而不是一个 NULL 指针,它一般在上下文中代表发生了错误,就像我们之前见到的一样。
1.4. The Module’s Method Table and Initialization Function
之前承诺提到要展示 spam_system() 这个C函数是如何在Python程序中调用的。首先,我们要把它的名字和(函数)地址在一个叫做“方法表”(method table)的数据结构(其实就是一个C中的静态数组)中列出:
static PyMethodDef SpamMethods[] = {
...
{"system", spam_system, METH_VARARGS, "Execute a shell command."},
...
{NULL, NULL, 0, NULL} /* Sentinel */
};
注意,第三项 METH_VARARGS,它是一个标记,用来告知Python解释器如何调用对应C函数的一种约定。通常情况下,它应该总是 METH_VARARGS 或者 METH_VARARGS | METH_KEYWORDS;值 0 表示使用了一个废弃的变种函数 PyArg_ParseTuple()。
当仅使用 METH_VARARGS 的时候,函数应该只接收Python层面的参数,并将其当做一个元组,以供函数 PyArg_ParseTuple() 进行解析,下面是该函数详细的分析。
如果是以关键字参数的形式传参给函数,那么就可以在第三项上设置 METH_KEYWORDS 这一位(bit)。这种情况下,对应的C函数应该有第三个参数,这第三个参数是一个关键字的字典(keyword dictionary),而且应该使用函数 PyArg_ParseTupleAndKeywords() 去解析传递进来的参数。
在模块定义结构中,必须引用前面提到的函数表(方法表,method table):
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 就是这个模块的名称,并且这个初始化函数必须是模块文件中定义的唯一一个非静态函数(only non-static)。
PyMODINIT_FUNC
PyInit_spam(void)
{
return PyModule_Create(&spammodule);
}
注意,PyMODINIT_FUNC 声明了这个函数是以 PyObject *的类型作为返回值类型,但这个函数也可以被声明为按照系统平台要求的其他特殊链接类型的返回值类型,也可以按照 extern "C" 的方式在C++中声明。
当Python程序第一次导入模块(import module)时,PyInit_spam() 就会被调用。(下面的代码片段中有注释说明)它又调用了函数 PyModule_Create(),这个函数返回一个模块对象,并且会根据模块定义中的表(table,一个 PyMethodDef 结构的数组),把一些内置函数对象插入到这个新创建的模块中去。函数 PyModule_Create() 返回一个指向新创建的模块对象的指针。它可能因为某些错误而中止执行并退出,也有可能因为这个模块不能顺利地被初始化而返回 NULL。这个初始化函数必须给它的调用者返回一个模块对象,以便它能够被插入到 sys.modules 中去。
当嵌入到Python中去的时候,函数 PyInit_spam() 不会被自动调用,除非在 PyImport_Inittab 表中有这一项。为了在初始化表中添加这个模块(即把这个自定义的模块当做一个内置模块来对待),使用 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 */
if (PyImport_AppendInittab("spam", PyInit_spam) == -1) {
fprintf(stderr, "Error: could not extend in-built modules table\n");
exit(1);
}
/* Pass argv[0] to the Python interpreter */
Py_SetProgramName(program);
/* Initialize the Python interpreter. Required.
If this step fails, it will be a fatal error. */
Py_Initialize();
/* Optionally import the module; alternatively,
import can be deferred until the embedded script
imports it. */
PyObject *pmodule = PyImport_ImportModule("spam");
if (!pmodule) {
PyErr_Print();
fprintf(stderr, "Error: could not import module 'spam'\n");
}
...
PyMem_RawFree(program);
return 0;
}
(这里可以看到,PyImport_AppendInittab() 函数是为了将我们自定义的模块Python的模块初始化表中去,而且要在调用 Py_Initialize() 之前执行),而导入模块并不一定要在此时完成,而是可以等到在Python脚本中需要导入该模块的时候再做。
注意,删除 sys.modules 中的某些项,或者在同一个进程中(或者还有后面跟上 fork() ,但没有函数 exec() 介入),把编译好的模块导入到多个Python解释器中,都会导致某些扩展模块产生问题。扩展模块的作者应该在初始化内部数据结构的时候谨慎行事。
一个更加实际的模块例子是 Modules/xxmodule.c,它包含在Python的源代码文件中,这个文件可以当做(创建模块的)模板,或者当做一个例子来阅读。
注意,和本文中 spam 例子不同的是,xxmodule 模块使用了多步初始化(Python 3.5中引入)。如果使用这个办法,那么本文例子中 PyInit_spam() 返回的就是一个 PyModuleDef 结构,并且模块的创建工作就会交给导入机制完成。关于多步初始化更详细的内容,参考 PEP 489。
1.5. Compilation and Linkage
在使用新的扩展模块之前,还有两件事情要做:编译,并且链接到Python系统上去。如果使用的是动态链接,那么(实现)细节就依赖于所使用的系统的动态链接方式;可以参考有关构建扩展模块的章节(Building C and C++ Extensions),以及只适用于在Windows平台上构建方法的更多细节。
如果不能使用动态链接,或者就想使扩展模块成为Python解释器永久的一部分,那么就需要改变设定,并且重新编译Python解释器。幸运的是,这在Unix平台上实现起来十分简单:只需要把(扩展模块)文件(比如本文中的 spammodule.c)放到Python源分发包的 Modules/目录中,然后在 Modules/Setup.local 加入如下一行来描述你的文件即可。
spam spammodule.o
然后在最上层的目录中,使用 make 重新编译Python解释器。当然,也可以就只在 Modules/ 这个字目录中直接执行 make 来编译,但是,之后就必须在这个目录中,通过执行 make Makefile 来重新构建 Makefile,而且这个步骤在每次修改完 Setup 文件之后必须执行。
如果你的扩展模块需要链接额外的库,那么就需要在配置文件中同样列出来,如下。
spam spammodule.o -lX11