【译】在 C 程序嵌入 Python
基于 3.7.12 版本文档翻译。翻译完了才发现原来 3.9.9 有已经翻译好的。。。
我们在前面的章节中讨论了如何通过用 C 和 C++ 函数库来扩展Python。与之相对,我们也可以将 Python 嵌入到 C 和 C++ 程序中,然后根据需要调用Python解释器来运行Python代码。
调用 Python 前,首先要初始化 Python 解释器——在最简单的情况下,一句 Py_Initialize() 即可实现对解释器的初始化(如有需要,也可以通过调用一些函数向解释器传递命令行参数)。在此之后,我们就可以在代码中的任何地方调用解释器了。
调用解释器的方法大概有这么几种:
-
将一段 Python 代码字符串传给
PyRun_SimpleString(); -
将一个
stdio文件指针和文件名传给PyRun_SimpleFile(); -
使用 Python objects 来实现更加底层的操作,这种用法详见前面的章节
该手册详细描述了 Python 的 C 接口,其中包含了相当多有用的内容,供参考。
1.1 非常高层次的嵌入(最简单的用法)
使用高层接口,是在程序中嵌入 Python 最简单的办法。该接口只能启动一个 Python 脚本的执行,无法实现和主程序的直接交互。例:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
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);
}
Py_SetProgramName(program); /* 可选,建议写 */
Py_Initialize(); /* 初始化 Python 解释器 */
/* 直接将字符串形式的 Python 代码传给解释器执行 */
PyRun_SimpleString("from time import time,ctime\n"
"print('Today is', ctime(time()))\n");
if (Py_FinalizeEx() < 0) {
exit(120);
}
PyMem_RawFree(program);
return 0;
}
Py_SetProgramName() 应在 Py_Initialize() 之前调用,这样可以告知解释器有关 Python 运行时库路径相关的信息。在使用 Py_Initialize() 函数初始化 Python 解释器后,即可使用 PyRun_SimpleString() 来运行硬编码的 Python 脚本。在执行之后,Py_FinalizeEx() 调用将关闭解释器。在实际应用中可根据需要,使用从其他数据源获取的脚本源代码替换掉这里的硬编码脚本。如果要执行 Python 源代码文件,推荐直接使用 PyRun_SimpleFile() 函数来实现,省掉自己管理内存和加载文件的步骤
1.2 更加深入的嵌入:概览
上文中讲的高层次接口可以帮助我们简单地执行一段 Python 代码,但想通过这种方式来实现通信的话会非常麻烦。如果想要实现更直接的 C 和 Python 之间的通信,就需要使用更低一层级的调用。只要我们愿意多写一些 C 代码,就能实现几乎任何功能。
需要注意的是,将 Python 嵌入到 C 程序,和用 C 程序扩展 Python 是非常相似的,两者在逻辑上是正好反过来的:
用 C 扩展 Python 的运行过程是:
- 将 Python 的数据转化为 C 的数据;
- 使用转化过来的数据,调用相应的一段 C 程序;
- 将上面这段 C 程序输出的 C 格式数据转化为 Python 数据。
将 Python 嵌入 C 程序的运行过程:
- 将 C 的数据转化为 Python 的数据;
- 使用转化后的数据调用一段 Python 程序;
- 将上面这段 Python 程序输出的结果转化为 C 的数据。
本章节不会涉及 Python 和 C 之间数据转换的内容和具体的使用及错误处理内容,如果需要,请查阅前面一些章节中关于使用 C 扩展 Python 的内容。
1.3 纯嵌入(pure-embedding)
下面这段代码稍微复杂一点点,它能够帮你执行给定的 Python 代码中的指定函数
#define PY_SSIZE_T_CLEAN
#include <Python.h>
int
main(int argc, char *argv[])
{
PyObject *pName, *pModule, *pFunc;
PyObject *pArgs, *pValue;
int i;
if (argc < 3) {
fprintf(stderr,"Usage: call pythonfile funcname [args]\n");
return 1;
}
Py_Initialize();
pName = PyUnicode_DecodeFSDefault(argv[1]);
/* 省略掉对 pName 进行错误检查的代码 */
pModule = PyImport_Import(pName);
Py_DECREF(pName);
if (pModule != NULL) {
pFunc = PyObject_GetAttrString(pModule, argv[2]);
/* pFunc is a new reference */
if (pFunc && PyCallable_Check(pFunc)) {
pArgs = PyTuple_New(argc - 3);
for (i = 0; i < argc - 3; ++i) {
pValue = PyLong_FromLong(atoi(argv[i + 3]));
if (!pValue) {
Py_DECREF(pArgs);
Py_DECREF(pModule);
fprintf(stderr, "Cannot convert argument\n");
return 1;
}
/* pValue reference stolen here: */
PyTuple_SetItem(pArgs, i, pValue);
}
pValue = PyObject_CallObject(pFunc, pArgs);
Py_DECREF(pArgs);
if (pValue != NULL) {
printf("Result of call: %ld\n", PyLong_AsLong(pValue));
Py_DECREF(pValue);
}
else {
Py_DECREF(pFunc);
Py_DECREF(pModule);
PyErr_Print();
fprintf(stderr,"Call failed\n");
return 1;
}
}
else {
if (PyErr_Occurred())
PyErr_Print();
fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]);
}
Py_XDECREF(pFunc);
Py_DECREF(pModule);
}
else {
PyErr_Print();
fprintf(stderr, "Failed to load \"%s\"\n", argv[1]);
return 1;
}
if (Py_FinalizeEx() < 0) {
return 120;
}
return 0;
}
这段代码根据 argv[1] 来读取 Python 脚本,通过 argv[2] 获取要被调用的函数名。然后将 argv 中的其他整型参数作为参数传给被实际调用的 Python 函数,最后把 Python 的返回值打印出来。对这个小程序进行编译、链接后,就可以用它来执行 Python 代码了。我们准备下面这样的一个小程序:
def multiply(a,b):
print("Will compute", a, "times", b)
c = 0
for i in range(0, a):
c = c + b
return c
调用并运行,结果应该是:
$ call multiply multiply 3 2
Will compute 3 times 2
Result of call: 6
简单看一下上面的 C 代码,不难发现,大部分代码都是在进行 Python 和 C 之间的数据转换以及错误处理。其中直接和 Python 调用相关的代码是下面这些:
Py_Initialize(); // 初始化解释器
pName = PyUnicode_DecodeFSDefault(argv[1]);
/* 省略错误处理部分代码 */
pModule = PyImport_Import(pName);
首先要初始化解释器,然后用 PyImport_Import() 加载脚本:该函数需要使用 Python 中的字符串作为参数,所以我们在中间的那行代码中,用 PyUnicode_DecodeFSDefault(argv[1]) 这个数据类型转换函数来将 C 字符串argv[1] 构造为 Python 字符串。接下来:
pFunc = PyObject_GetAttrString(pModule, argv[2]);
/* pFunc is a new reference */
if (pFunc && PyCallable_Check(pFunc)) {
...
}
Py_XDECREF(pFunc);
在脚本(pModule)加载完成后,我们就可以用想调用的函数名 argv[2] 作为 PyObject_GetAttrString() 的参数来取回我们想要的 Python 属性了。如果 argv[2] 在 pModule 中存在且可调用,我们就可以确认找到了需要被调用的函数。接下来只需要用参数构造一个元组,然后用下面的方法来调用目标 Python 函数:
pValue = PyObject_CallObject(pFunc, pArgs);
根据被调用函数返回值的不同,pValue 要么为 NULL ,要么为指向一个函数返回值的指针。记得在用完返回值之后将该指针下的变量释放掉。
1.4 扩展嵌入 Python
我们在前面嵌入的 Python 解释器只能接受来自 C 程序传入的值和给出返回值,而不能直接访问 C 程序中的任何东西。 Python 可以通过一些扩展嵌入式解释器的 API 来实现访问 C 中的内容:即,被嵌入的解释器可以通过主程序提供的函数来实现扩展。也就是说,被 Python 调用的 C 函数就相当于是直接为 Python 做的 C 扩展。例如:
static int numargs=0;
/* 返回主程序收到的命令行参数个数 */
static PyObject*
emb_numargs(PyObject *self, PyObject *args)
{
if(!PyArg_ParseTuple(args, ":numargs"))
return NULL;
return PyLong_FromLong(numargs);
}
static PyMethodDef EmbMethods[] = {
{"numargs", emb_numargs, METH_VARARGS,
"Return the number of arguments received by the process."},
{NULL, NULL, 0, NULL}
};
static PyModuleDef EmbModule = {
PyModuleDef_HEAD_INIT, "emb", NULL, -1, EmbMethods,
NULL, NULL, NULL, NULL
};
static PyObject*
PyInit_emb(void)
{
return PyModule_Create(&EmbModule);
}
将这段代码插入到上一个示例代码中的 main() 函数之上,再把下面这两行代码插入到 Py_Initialize() 之上:
numargs = argc;
PyImport_AppendInittab("emb", &PyInit_emb);
这两行代码会初始化 numargs 变量,并让 emb.numargs() 函数在嵌入的 Python 解释器中可用。有了这些扩展,我们嵌入的 Python 脚本就可以包含下面这样的代码了:
import emb
print("Number of arguments", emb.numargs())
这样,我们就对 Python 暴露出了一个 API .
1.5 在 C++ 中嵌入 Python
通常来说 C++ 可以直接通过上面讲过的用法来嵌入 Python 。
1.6 在类 Unix 系统中编译和链接
为方便使用,Python 自带了生成编译、链接参数和标志的脚本
-
可以通过
pythonX.Y-config --cflags来生成推荐的编译参数:lijingwei@lijingwei-PC:~$ /usr/bin/python3.7-config --cflags -I/usr/include/python3.7m -I/usr/include/python3.7m -Wno-unused-result -Wsign-compare -g -fdebug-prefix-map=/build/python3.7-3.7.3.6=. -specs=/usr/share/dpkg/no-pie-compile.specs -fstack-protector -Wformat -Werror=format-security -DNDEBUG -g -fwrapv -O3 -Wall -
可以通过
pythonX.Y-config --ldflags来生成推荐的链接参数:lijingwei@lijingwei-PC:~$ /usr/bin/python3.7-config --ldflags -L/usr/lib/python3.7/config-3.7m-x86_64-linux-gnu -L/usr/lib -lpython3.7m -lcrypt -lpthread -ldl -lutil -lm -Xlinker -export-dynamic -Wl,-O1 -Wl,-Bsymbolic-functions
为了避免搞错要用的 Python ,推荐像上面这样通过绝对路径来指定要用的 python config