在C/C ++程序中内嵌Cython模块
原文: https://cython.readthedocs.io/en/latest/src/tutorial/embedding.html
这是一个stub文档页面。欢迎PR。
快速链接:
- CPython 文档
- Cython Wiki
--embed
选项可以在cython
和cythonize
写上来产生C主函数,用cython_freeze脚本来将多个扩展模块合并为一个库- 内嵌Demo程序
- 查看CPython的模块初始化函数以及PEP-489关于CPython3.5及之后版本的模块初始化机制。
初始化你的主模块
最重要的是,不要用调用模块初始化函数来代替引用模块。这不是初始化一个扩展模块的正确方法。过去可以这么做,但是从Python3.5版本起不行了,并且以后的版本也不行)。
具体的细节,可以查看CPython的模块初始化函数以及PEP-489关于CPython3.5及之后版本的模块初始化机制。
CPython中的 PyImport_AppendInittab()
函数允许注册静态或者动态链接的扩展模块给以后引入。上面的提到的模块初始化函数文档中就给出了一个例子。
内嵌样例代码
下面这个例子简单演示了在Python3.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
函数像这样:
/* embedded_main.c */
/* This include file is automatically generated by Cython for 'public' functions. */
#include "embedded.h"
#ifdef __cplusplus
extern "C" {
#endif
int
main(int argc, char *argv[])
{
PyObject *pmodule;
wchar_t *program;
program = Py_DecodeLocale(argv[0], NULL);
if (program == NULL) {
fprintf(stderr, "Fatal error: cannot decode argv[0], got %d arguments\n", argc);
exit(1);
}
/* Add a built-in module, before Py_Initialize */
if (PyImport_AppendInittab("embedded", PyInit_embedded) == -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. */
pmodule = PyImport_ImportModule("embedded");
if (!pmodule) {
PyErr_Print();
fprintf(stderr, "Error: could not import module 'embedded'\n");
goto exit_with_error;
}
/* Now call into your module code. */
if (say_hello_from_python() < 0) {
PyErr_Print();
fprintf(stderr, "Error in Python code, exception was printed.\n");
goto exit_with_error;
}
/* ... */
/* Clean up after using CPython. */
PyMem_RawFree(program);
Py_Finalize();
return 0;
/* Clean up in the error cases above. */
exit_with_error:
PyMem_RawFree(program);
Py_Finalize();
return 1;
}
#ifdef __cplusplus
}
#endif
(改自CPython文档)
你可以用 cython --embed
让Cython为你模块的C文件生成一个这样的 main
函数,而不必自己写。或者使用cython_freeze脚本来内嵌多个模块。完整的安装文件样例可以查看内嵌Demo程序
请注意你的程序不会包含你使用的任何外部以来(包括Python标准库模块),因此也不是真的可移植。如果你希望生成一个可移植的程序,我们推荐使用一个特殊的工具(比如 Pyinstaller 或者 cx_freeze)来寻找并绑定这些依赖。
故障排查
这里是一些你内嵌Cython代码时可能出现的错误。
没有初始化Python解释器
Cython并不会编译为纯独立的C代码。相反,Cython会编译为一组依赖Python解释器的Python C-API 调用。因此,在你的main函数中,你必须用 Py_Initialize()
初始化Python解释器。你应该在你的main函数中尽可能早的这么做。
在极少的情况下,你不这么做也没问题,比如非常简单的程序。但是这是纯粹的运气,你不应该依赖这种行为。没有一个Cython的“安全子集”可以在没有解释器的情况下工作。
没有初始化Python解释器的最可能的结果就是崩溃。
你应该只初始化解释器一次 -- 因为许多模块比如大多数Cython模块、NumPy都不能被初始化多次。因此,如果你在一个很大的程序中做Python/Cython计算的时候千万别这么做:
void run_calcuation() {
Py_Initialize();
// Use Python/Cython code
Py_Finalize();
}
你有可能会遇到奇怪的无法解释的崩溃。
没有设置Python路径
如果你的模块引入了任何东西(甚至于当你什么都没有引入的时候),它需要Python路径来寻找模块。和独立的解释器不同,内嵌的Python并不能自动完成这件事。
这么做的最简单的方法就是 PySys_SetPath(...)
-- 理想状态下应该直接写在 Py_Initialize()
后面。你也可以用 PySys_GetObject("path")
然后在它返回的列表后面加入(路径)。
如果你忘了这么做,你很可能会遇到引用错误。
没有引入Cython模块
Cython 并不创建独立的C代码 - 它会创建设计作为Cython模块引用的C代码。"Import" 函数设置了许多重要的基础设施让你的代码能够运行。例如,字符串会在Import的时候初始化,内置的例如 print
会被找到并缓存在你的Cython模块的。
因此,如果你准备跳过初始化并直接运行你的公开函数你很可能遇到崩溃(即使只是使用一个简单的字符串)。
InitTab
推荐安装扩展模块以便在现代的Python(版本>=3.5)中用 inittab机制引用(关于这一点详细的信息看这里)。这应该在 Py_Initialize()
之前完成。
强制单阶段
如果出于某些原因你无法在Python被初始化之前将你的模块加入到 inittab 中(一个常见的原因是尝试引入另一个构建为独立共享库的Cython模块),那么你可以通过给编译器定义宏 CYTHON_PEP489_MULTI_PHASE_INIT=0
来禁止多阶段的初始化(对于gcc来说这个是命令行中加入 -DCYTHON_PEP489_MULTI_PHASE_INIT=0
)。如果你这么做然后你可以直接运行模块init函数(Python3上是 PyInit_<module_name>
)。
真的不推荐这么做
多阶段工作
你自己手动完成多阶段的初始化也是有可能的。Cython的其中一个开发人员写了一个指导文章引导你如何做。但是他放这里只为了看上去足够厉害,而不是鼓励你也这么做。但是无论如何,如果你没法在初始化解释器之前使用inittab机制,这都是一种选择。