在 C/C++ 应用程序中嵌入 Cython 模块

这是一个存根文档页面。非常欢迎 PR。

快速链接

初始化您的主模块

最重要的是,不要调用模块初始化函数,而应该导入模块。这不是初始化扩展模块的正确方法。(它一直都是错误的,但以前可以工作,但从 Python 3.5 开始,它就错了,而且不再起作用。)

有关详细信息,请参见 模块初始化函数 在 CPython 中的文档,以及 PEP 489,关于 CPython 3.5 及更高版本中的模块初始化机制。

在 CPython 中,PyImport_AppendInittab() 函数允许注册静态(或动态)链接的扩展模块,以便以后导入。在上面链接的模块初始化函数的文档中给出了一个示例。

嵌入示例代码

以下是一个简单的示例,展示了在 Python 3.x 中嵌入 Cython 模块 (embedded.pyx) 的主要步骤。

首先,这里有一个 Cython 模块,它导出一个 C 函数,供外部代码调用。请注意,say_hello_from_python() 函数被声明为 public,以便将其导出为一个链接器符号,供其他 C 文件使用,在本例中为 embedded_main.c

# embedded.pyx

# The following two lines are for test purposes only, please ignore them.
# distutils: sources = embedded_main.c
# tag: py3only
# tag: no-cpp

TEXT_TO_SAY = 'Hello from Python!'

cdef public int say_hello_from_python() except -1:
    print(TEXT_TO_SAY)
    return 0

程序的 C main() 函数可能如下所示

 1/* embedded_main.c */
 2
 3/* This include file is automatically generated by Cython for 'public' functions. */
 4#include "embedded.h"
 5
 6#ifdef __cplusplus
 7extern "C" {
 8#endif
 9
10int
11main(int argc, char *argv[])
12{
13    PyObject *pmodule;
14    wchar_t *program;
15
16    program = Py_DecodeLocale(argv[0], NULL);
17    if (program == NULL) {
18        fprintf(stderr, "Fatal error: cannot decode argv[0], got %d arguments\n", argc);
19        exit(1);
20    }
21
22    /* Add a built-in module, before Py_Initialize */
23    if (PyImport_AppendInittab("embedded", PyInit_embedded) == -1) {
24        fprintf(stderr, "Error: could not extend in-built modules table\n");
25        exit(1);
26    }
27
28    /* Pass argv[0] to the Python interpreter */
29    Py_SetProgramName(program);
30
31    /* Initialize the Python interpreter.  Required.
32       If this step fails, it will be a fatal error. */
33    Py_Initialize();
34
35    /* Optionally import the module; alternatively,
36       import can be deferred until the embedded script
37       imports it. */
38    pmodule = PyImport_ImportModule("embedded");
39    if (!pmodule) {
40        PyErr_Print();
41        fprintf(stderr, "Error: could not import module 'embedded'\n");
42        goto exit_with_error;
43    }
44
45    /* Now call into your module code. */
46    if (say_hello_from_python() < 0) {
47        PyErr_Print();
48        fprintf(stderr, "Error in Python code, exception was printed.\n");
49        goto exit_with_error;
50    }
51
52    /* ... */
53
54    /* Clean up after using CPython. */
55    PyMem_RawFree(program);
56    Py_Finalize();
57
58    return 0;
59
60    /* Clean up in the error cases above. */
61exit_with_error:
62    PyMem_RawFree(program);
63    Py_Finalize();
64    return 1;
65}
66
67#ifdef __cplusplus
68}
69#endif

(改编自 CPython 文档。)

您可以使用 cython --embed 选项让 Cython 生成一个到模块的 C 文件中的 main() 函数,而不是自己编写这样的函数。或者使用 cython_freeze 脚本嵌入多个模块。请参见 嵌入演示程序,了解完整的示例设置。

请注意,您的应用程序将不包含您使用的任何外部依赖项(包括 Python 标准库模块),因此可能不是真正可移植的。如果您想生成一个可移植的应用程序,我们建议您使用专门的工具(例如 PyInstallercx_freeze)来查找和捆绑这些依赖项。

故障排除

以下是嵌入 Cython 代码时可能出现的一些问题。

未初始化 Python 解释器

Cython 不会编译成“纯独立的 C 代码”。相反,Cython 会编译成一堆依赖于 Python 解释器的 Python C API 调用。因此,在您的主函数中,您**必须**使用 Py_Initialize() 初始化 Python 解释器。您应该在您的 main() 函数中尽早执行此操作。

对于非常简单的程序,您偶尔可以不用它。这纯粹是运气,您不应该依赖它。没有为在没有解释器的情况下运行而设计的 Cython 的“安全子集”。

不初始化 Python 解释器可能会导致崩溃。

您应该只初始化一次解释器 - 许多模块,包括大多数 Cython 模块和 Numpy,目前不喜欢被多次导入。因此,如果您在一个更大的程序中进行偶尔的 Python/Cython 计算,您**不应该**这样做

void run_calculation() {
     Py_Initialize();
     // Use Python/Cython code
     Py_Finalize();
}

您很可能会遇到无法解释的神秘崩溃。

未设置 Python 路径

如果您的模块导入任何内容(甚至可能没有导入),那么它将需要设置 Python 路径,以便它知道在哪里查找模块。与独立解释器不同,嵌入式 Python 不会自动设置它。

PySys_SetPath(...) 是执行此操作的最简单方法(理想情况下,紧接在 Py_Initialize() 之后)。您也可以使用 PySys_GetObject("path"),然后将它返回的列表追加。

如果您忘记执行此操作,您可能会看到导入错误。

未导入 Cython 模块

Cython 不会创建独立的 C 代码 - 它创建旨在作为 Cython 模块导入的 C 代码。 “导入”函数设置了运行代码所需的许多基本基础设施。例如,字符串在导入时初始化,并且像 print 这样的内置函数被找到并存储在您的 Cython 模块中。

因此,如果您决定跳过初始化并直接运行您的公共函数,您可能会遇到崩溃(即使对于像使用字符串这样简单的事情)。

InitTab

在现代 Python (>=3.5) 中设置扩展模块以使其可供导入的首选方法是使用 inittab 机制,该机制在 文档中的其他地方 有详细介绍。这应该在 Py_Initialize() 之前完成。

强制单阶段

如果由于某种原因您无法在 Python 初始化之前将您的模块添加到 inittab(一个常见的原因是尝试导入另一个内置到单个共享库中的 Cython 模块),那么您可以通过为您的 C 编译器定义 CYTHON_PEP489_MULTI_PHASE_INIT=0 来禁用多阶段初始化(对于 gcc,这将是在命令行中使用 -DCYTHON_PEP489_MULTI_PHASE_INIT=0)。如果您这样做,那么您可以直接运行模块 init 函数(在 Python 3 上使用 PyInit_<module_name>)。这不是首选选项

使用多阶段

您可以手动运行多阶段初始化。Cython 开发人员之一编写了一个 指南,展示了如何执行此操作。但是,他认为它足够 hacky,因此只在这里链接,而不是直接复制。如果您无法在初始化解释器之前使用 inittab 机制,这是一个选择。

多处理和 pickle 的问题

如果您尝试在使用嵌入到可执行文件中的 Cython 模块时使用 multiprocessing,它很可能会因与 pickle 模块相关的错误而失败。 multiprocessing 通常使用 pickle 来序列化和反序列化要在另一个解释器中运行的数据。发生的事情取决于多处理的“启动方法”。但是,在 Windows 上使用的“spawn”启动方法上,它会启动Python 解释器的全新副本(而不是嵌入程序的全新副本),然后尝试导入您的 Cython 模块。由于您的 Cython 模块仅通过 inittab 机制可用,而不是通过常规导入,因此该导入失败。

解决方案可能涉及将 multiprocessing.set_executable 设置为指向您的嵌入式程序,然后修改该程序以处理 --multiprocessing-fork 多进程传递给 Python 解释器的命令行参数。您可能还需要调用 multiprocessing.freeze_support()

目前该解决方案尚未经过测试,因此您应该将从嵌入式 Cython 可执行文件进行多进程处理视为不受支持。