【译】在 C 程序嵌入 Python

1,089 阅读2分钟

【译】在 C 程序嵌入 Python

基于 3.7.12 版本文档翻译。翻译完了才发现原来 3.9.9 有已经翻译好的。。。

我们在前面的章节中讨论了如何通过用 C 和 C++ 函数库来扩展Python。与之相对,我们也可以将 Python 嵌入到 C 和 C++ 程序中,然后根据需要调用Python解释器来运行Python代码。

调用 Python 前,首先要初始化 Python 解释器——在最简单的情况下,一句 Py_Initialize() 即可实现对解释器的初始化(如有需要,也可以通过调用一些函数向解释器传递命令行参数)。在此之后,我们就可以在代码中的任何地方调用解释器了。

调用解释器的方法大概有这么几种:

  1. 将一段 Python 代码字符串传给 PyRun_SimpleString()

  2. 将一个 stdio 文件指针和文件名传给 PyRun_SimpleFile()

  3. 使用 Python objects 来实现更加底层的操作,这种用法详见前面的章节

    Python/C API 参考手册

    该手册详细描述了 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 的运行过程是:

  1. 将 Python 的数据转化为 C 的数据;
  2. 使用转化过来的数据,调用相应的一段 C 程序;
  3. 将上面这段 C 程序输出的 C 格式数据转化为 Python 数据。

将 Python 嵌入 C 程序的运行过程:

  1. 将 C 的数据转化为 Python 的数据;
  2. 使用转化后的数据调用一段 Python 程序;
  3. 将上面这段 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