Skip to content

在C/C ++程序中内嵌Cython模块

原文: https://cython.readthedocs.io/en/latest/src/tutorial/embedding.html

这是一个stub文档页面。欢迎PR。

快速链接:

  1. CPython 文档
  2. Cython Wiki
  3. --embed选项可以在cythoncythonize写上来产生C主函数,用cython_freeze脚本来将多个扩展模块合并为一个库
  4. 内嵌Demo程序
  5. 查看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机制,这都是一种选择。



回到顶部