在 C/C++ 应用程序中嵌入 Cython 模块¶
这是一个存根文档页面。非常欢迎 PR。
快速链接
请参见
--embed选项,用于cython和cythonize前端,用于生成 C 主函数,以及 cython_freeze 脚本,用于将多个扩展模块合并为一个库。请参见 模块初始化函数 在 CPython 中的文档,以及 PEP 489,关于 CPython 3.5 及更高版本中的模块初始化机制。
初始化您的主模块¶
最重要的是,不要调用模块初始化函数,而应该导入模块。这不是初始化扩展模块的正确方法。(它一直都是错误的,但以前可以工作,但从 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 标准库模块),因此可能不是真正可移植的。如果您想生成一个可移植的应用程序,我们建议您使用专门的工具(例如 PyInstaller 或 cx_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 可执行文件进行多进程处理视为不受支持。