与外部 C 代码交互

Cython 的主要用途之一是包装现有的 C 代码库。这是通过使用外部声明来声明要使用的库中的 C 函数和变量来实现的。

您还可以使用公共声明使 Cython 模块中定义的 C 函数和变量可供外部 C 代码使用。预计对这种需求的频率较低,但您可能希望这样做,例如,如果您正在 将 Python 嵌入另一个应用程序中作为脚本语言。就像 Cython 模块可以用作桥梁以允许 Python 代码调用 C 代码一样,它也可以用来允许 C 代码调用 Python 代码。

外部声明

默认情况下,在模块级别声明的 C 函数和变量对模块是本地的(即它们具有 C 静态存储类)。它们也可以声明为 extern 以指定它们是在其他地方定义的,例如,

cdef extern int spam_counter

cdef extern void order_spam(int tons)

引用 C 头文件

当您像上面的示例那样单独使用 extern 定义时,Cython 会在生成的 C 文件中包含一个声明。如果声明与其他 C 代码将看到的声明不完全匹配,这可能会导致问题。例如,如果您正在包装现有的 C 库,那么确保生成的 C 代码与库的其余部分使用完全相同的声明进行编译非常重要。

要实现这一点,您可以告诉 Cython 这些声明将在 C 头文件中找到,如下所示

cdef extern from "spam.h":

    int spam_counter

    void order_spam(int tons)

cdef extern from 子句执行三件事

  1. 它指示 Cython 在生成的 C 代码中放置一个 #include 语句以用于命名的头文件。

  2. 它阻止 Cython 为关联块中找到的声明生成任何 C 代码。

  3. 它将块中的所有声明视为以 cdef extern 开头。

重要的是要了解 Cython 本身不会读取 C 头文件,因此您仍然需要提供您使用的任何声明的 Cython 版本。但是,Cython 声明并不总是必须与 C 声明完全匹配,在某些情况下它们不应该或不能匹配。特别是

  1. 省略 C 声明的任何平台特定扩展,例如 __declspec()

  2. 如果头文件声明了一个大型结构体,而您只想使用几个成员,则只需要声明您感兴趣的成员。省略其余部分不会造成任何伤害,因为 C 编译器将使用头文件中的完整定义。

    在某些情况下,您可能不需要结构体的任何成员,在这种情况下,您只需在结构体声明的主体中放置 pass,例如

    cdef extern from "foo.h":
        struct spam:
            pass
    

    注意

    您只能在 cdef extern from 块中执行此操作;其他任何地方的结构体声明都必须是非空的。

  3. 如果头文件使用 typedef 名称(例如 word)来引用数字类型的平台相关变体,您将需要一个相应的 ctypedef 语句,但您不需要完全匹配类型,只需使用大致正确的类型(int、float 等)。例如,

    ctypedef int word
    

    无论实际大小如何,word 都会正常工作(前提是头文件正确定义了它)。如果存在的话,转换为和从 Python 类型转换也将用于这种新类型。

  4. 如果头文件使用宏来定义常量,请将其转换为正常的外部变量声明。您也可以将它们声明为 enum,如果它们包含正常的 int 值。请注意,Cython 认为 enum 等同于 int,因此不要对非 int 值执行此操作。

  5. 如果头文件使用宏定义函数,请将其声明为普通函数,并具有适当的参数和结果类型。

  6. 由于历史原因,C 使用关键字 void 来声明不接受参数的函数。在 Cython 中,就像在 Python 中一样,只需将此类函数声明为 foo()

一些额外的技巧和提示

  • 如果您想包含一个 C 头文件,因为它被另一个头文件需要,但不想使用其中的任何声明,请在 extern-from 块中放入 pass

    cdef extern from "spam.h":
        pass
    
  • 如果您想包含一个系统头文件,请在引号内放入尖括号

    cdef extern from "<sysheader.h>":
        ...
    
  • 如果您想包含一些外部声明,但不想指定头文件(因为它被您已经包含的某个其他头文件包含),您可以用 * 代替头文件名

    cdef extern from *:
        ...
    
  • 如果 cdef extern from "inc.h" 块不为空,并且只包含函数或变量声明(以及任何类型的类型声明),Cython 将在 Cython 生成的所有声明之后放置 #include "inc.h" 语句。这意味着包含的文件可以访问 Cython 声明的变量、函数、结构等。

在 C 中实现函数

当您想从 Cython 模块调用 C 代码时,通常该代码将位于您链接扩展的某个外部库中。但是,您也可以直接将 C(或 C++)代码编译为 Cython 模块的一部分。在 .pyx 文件中,您可以放入类似以下内容

cdef extern from "spam.c":
    void order_spam(int tons)

Cython 将假设函数 order_spam() 在文件 spam.c 中定义。如果您还想从另一个模块 cimport 此函数,则必须在 .pxd 文件中声明它(不是 extern!)

cdef void order_spam(int tons)

为了使此工作,spam.corder_spam() 的签名必须与 Cython 使用的签名匹配,特别是函数必须是静态的

static void order_spam(int tons)
{
    printf("Ordered %i tons of spam!\n", tons);
}

结构、联合和枚举声明的样式

在 C 头文件中,结构、联合和枚举的声明主要有两种方式:使用标签名或使用 typedef。还有一些基于这些组合的变体。

重要的是使 Cython 声明与头文件中使用的样式匹配,以便 Cython 可以在它生成的代码中发出对类型的正确引用。为了使这成为可能,Cython 提供了两种不同的语法来声明结构、联合或枚举类型。上面介绍的样式对应于使用标签名。要获得另一种样式,您需要在声明前加上 ctypedef,如下所示。

下表显示了头文件中可能出现的各种样式,以及您应该在 cdef extern from 块中放置的相应 Cython 声明。结构声明用作示例;这同样适用于联合和枚举声明。

C 代码

对应 Cython 代码的可能性

注释

struct Foo {
  ...
};
cdef struct Foo:
  ...

在生成的 C 代码中,Cython 会将该类型称为 struct Foo

typedef struct {
  ...
} Foo;
ctypedef struct Foo:
  ...

在生成的 C 代码中,Cython 会将该类型简单地称为 Foo

typedef struct foo {
  ...
} Foo;
cdef struct foo:
  ...
ctypedef foo Foo #optional

或者

ctypedef struct Foo:
  ...

如果 C 头文件使用带有不同名称的标签和 typedef,您可以在 Cython 中使用两种声明形式中的任何一种(尽管如果您需要向前引用该类型,则必须使用第一种形式)。

typedef struct Foo {
  ...
} Foo;
cdef struct Foo:
  ...

如果头文件对标签和 typedef 使用相同的名称,您将无法为其包含 ctypedef - 但在这种情况下,它不是必需的。

另请参阅 外部扩展类型 的使用。请注意,在以下所有情况下,您在 Cython 代码中简单地将该类型称为 Foo,而不是 struct Foo

指针

在与 C-api 交互时,可能存在需要指针作为参数的函数。指针是包含指向另一个变量的内存地址的变量。

例如

cdef extern from "<my_lib.h>":
    cdef void increase_by_one(int *my_var)

此函数以指向整数的指针作为参数。知道整数的地址允许函数就地修改该值,以便调用者之后可以看到更改。为了从现有变量中获取地址,请使用 & 运算符

cdef int some_int = 42
cdef int *some_int_pointer = &some_int
increase_by_one(some_int_pointer)
# Or without creating the extra variable
increase_by_one(&some_int)
print(some_int)  # prints 44 (== 42+1+1)

如果您想操作指针指向的变量,您可以通过引用其第一个元素来访问它,就像您在 python my_pointer[0] 中所做的那样。例如

cdef void increase_by_one(int *my_var):
    my_var[0] += 1

要深入了解指针,您可以阅读 tutorialspoint 上的本教程。有关 Cython 和 C 语法在操作指针方面的差异,请参阅 语句和表达式

访问 Python/C API 例程

cdef extern from 语句的一种特殊用途是用于访问 Python/C API 中的例程。例如,

cdef extern from "Python.h":

    object PyString_FromStringAndSize(char *s, Py_ssize_t len)

将允许您创建包含空字节的 Python 字符串。

请注意,Cython 附带了可导入的 cpython.* 模块中(几乎)所有 C-API 函数的现成声明。请参阅 https://github.com/cython/cython/tree/master/Cython/Includes/cpython 中的列表

您应该始终使用子模块(例如 cpython.objectcpython.list)来访问这些函数。从历史上看,Cython 在 cpython 模块下直接提供了一些 C-API 函数。但是,这已弃用,最终将被删除,任何新添加的内容都不会添加到其中。

特殊类型

Cython 预定义了 Py_ssize_t 名称以用于 Python/C API 例程。为了使您的扩展与 64 位系统兼容,您应该始终在 Python/C API 例程文档中指定的位置使用此类型。

Windows 调用约定

__stdcall__cdecl 调用约定说明符可以在 Cython 中使用,语法与 Windows 上 C 编译器使用的语法相同,例如,

cdef extern int __stdcall FrobnicateWindow(long handle)

cdef void (__stdcall *callback)(void *)

如果使用 __stdcall,则该函数仅被认为与具有相同签名的其他 __stdcall 函数兼容。

解决命名冲突 - C 命名规范

每个 Cython 模块都有一个单一的模块级命名空间,用于 Python 和 C 命名。如果您想包装一些外部 C 函数并为 Python 用户提供相同名称的 Python 函数,这可能会很不方便。

Cython 提供了几种解决此问题的方法。最好的方法,尤其是当您有许多 C 函数要包装时,是将 extern C 函数声明放入 .pxd 文件中,从而使用不同的命名空间,使用 在 Cython 模块之间共享声明 中描述的功能。将它们写入 .pxd 文件允许它们在模块之间重复使用,避免以正常的 Python 方式发生命名冲突,甚至可以轻松地在 cimport 时重命名它们。例如,如果您的 decl.pxd 文件声明了一个 C 函数 eject_tomato

cdef extern from "myheader.h":
    void eject_tomato(float speed)

那么您可以在 .pyx 文件中 cimport 并包装它,如下所示

from decl cimport eject_tomato as c_eject_tomato

def eject_tomato(speed):
    c_eject_tomato(speed)

或者简单地 cimport .pxd 文件并将其用作前缀

cimport decl

def eject_tomato(speed):
    decl.eject_tomato(speed)

请注意,这没有运行时查找开销,因为这会在 Python 中发生。Cython 在编译时解析 .pxd 文件中的名称。

对于命名空间或导入时重命名不够的特殊情况,例如当 C 中的名称与 Python 关键字冲突时,您可以使用 C 命名规范在声明时为 C 函数提供不同的 Cython 和 C 名称。例如,假设您想包装一个名为 yield() 的外部 C 函数。如果您将其声明为

cdef extern from "myheader.h":
    void c_yield "yield" (float speed)

那么它的 Cython 可见名称将是 c_yield,而它在 C 中的名称将是 yield。然后您可以用它来包装

def call_yield(speed):
    c_yield(speed)

对于函数,可以为变量、结构体、联合体、枚举、结构体和联合体成员以及枚举值指定 C 名称。例如

cdef extern int one "eins", two "zwei"
cdef extern float three "drei"

cdef struct spam "SPAM":
    int i "eye"

cdef enum surprise "inquisition":
    first "alpha"
    second "beta" = 3

请注意,Cython 不会对您提供的字符串进行任何验证或名称修改。它会将裸文本注入 C 代码中,不会修改,因此您完全依赖于此功能。如果您想声明一个名为 xyz 的名称,并让 Cython 将文本“让 C 编译器在此处失败”注入 C 文件,您可以使用 C 名称声明来做到这一点。将此视为一项高级功能,仅用于其他所有方法都失败的罕见情况。

包含逐字 C 代码

对于高级用例,Cython 允许您直接将 C 代码作为 cdef extern from 块的“docstring”写入

cdef extern from *:
    """
    /* This is C code which will be put
     * in the .c file output by Cython */
    static long square(long x) {return x * x;}
    #define assign(x, y) ((x) = (y))
    """
    long square(long x)
    void assign(long& x, long y)

以上等效于将 C 代码放在一个名为 header.h 的文件中,并写入

cdef extern from "header.h":
    long square(long x)
    void assign(long& x, long y)

此功能通常用于编译时的平台特定适配,例如

cdef extern from *:
    """
    #if defined(_WIN32) || defined(MS_WINDOWS) || defined(_MSC_VER)
      #include "stdlib.h"
      #define myapp_sleep(m)  _sleep(m)
    #else
      #include <unistd.h>
      #define myapp_sleep(m)  ((void) usleep((m) * 1000))
    #endif
    """
    # using "myapp_" prefix in the C code to prevent C naming conflicts
    void msleep "myapp_sleep"(int milliseconds) nogil

msleep(milliseconds=1)

也可以组合头文件和逐字 C 代码

cdef extern from "badheader.h":
    """
    /* This macro breaks stuff */
    #undef int
    """
    # Stuff from badheader.h

在这种情况下,C 代码 #undef int 会放在 Cython 生成的 C 代码中的 #include "badheader.h" 之后。

逐字 C 代码也可以用于版本特定适配,例如当一个结构体字段被添加到库中,但在旧版本中不可用时

cdef extern from "struct_field_adaptation.h":
    """
    #define HAS_NEWLY_ADDED_FIELD  (C_LIB_VERSION >= 20)

    #if HAS_NEWLY_ADDED_FIELD
        #define _mylib_get_newly_added_field(a_struct_ptr)  ((a_struct_ptr)->newly_added_field)
        #define _mylib_set_newly_added_field(a_struct_ptr, value)  ((a_struct_ptr)->newly_added_field) = (value)
    #else
        #define _mylib_get_newly_added_field(a_struct_ptr)  (0)
        #define _mylib_set_newly_added_field(a_struct_ptr, value)  ((void) (value))
    #endif
    """

    # Normal declarations provided by the C header file:
    ctypedef struct StructType:
        int field1
        int field2

    StructType *get_struct_ptr()

    # Special declarations conditionally provided above:
    bint HAS_NEWLY_ADDED_FIELD
    int get_newly_added_field "_mylib_get_newly_added_field" (StructType *struct_ptr)
    void set_newly_added_field "_mylib_set_newly_added_field" (StructType *struct_ptr, int value)


cdef StructType *some_struct_ptr = get_struct_ptr()

print(some_struct_ptr.field1)
if HAS_NEWLY_ADDED_FIELD:
    print(get_newly_added_field(some_struct_ptr))

请注意,该字符串像 Python 中的任何其他 docstring 一样被解析。如果您需要将字符转义传递到 C 代码文件中,请使用原始 docstring,即 r""" ... """

从 C 中使用 Cython 声明

Cython 提供了两种方法,用于使来自 Cython 模块的 C 声明可供外部 C 代码使用——公共声明和 C API 声明。

注意

您不需要使用这些方法来使来自一个 Cython 模块的声明可供另一个 Cython 模块使用——您应该使用 cimport 语句来实现这一点。在 Cython 模块之间共享声明。

公共声明

您可以通过使用 public 关键字声明,使在 Cython 模块中定义的 C 类型、变量和函数可供与 Cython 生成的 C 文件链接在一起的 C 代码访问

cdef public struct Bunny:  # a public type declaration
    int vorpalness

cdef public int spam  # a public variable declaration

cdef public void grail(Bunny *)  # a public function declaration

如果 Cython 模块中存在任何公共声明,则会生成一个名为 modulename.h 的头文件,其中包含等效的 C 声明,供其他 C 代码包含。

一个典型的用例是从多个 C 源文件构建扩展模块,其中一个是由 Cython 生成的(例如,在 setup.py 中使用类似 Extension("grail", sources=["grail.pyx", "grail_helper.c"]) 的代码)。在这种情况下,文件 grail_helper.c 只需要添加 #include "grail.h" 就可以访问公开的 Cython 变量。

一个更高级的用例是使用 Cython 将 Python 嵌入到 C 中。在这种情况下,请确保调用 Py_Initialize()Py_Finalize()。例如,在以下包含 grail.h 的代码片段中

#include <Python.h>
#include "grail.h"

int main() {
    Py_Initialize();
    initgrail();  /* Python 2.x only ! */
    Bunny b;
    grail(b);
    Py_Finalize();
}

这段 C 代码可以与 Cython 生成的 C 代码一起构建成一个程序(或库)。请注意,此程序将不包含模块使用的任何外部依赖项。因此,通常情况下,这不会为大多数情况生成真正可移植的应用程序。

在 Python 3.x 中,应避免直接调用模块初始化函数。相反,请使用 inittab 机制 将 Cython 模块链接到单个共享库或程序中。

err = PyImport_AppendInittab("grail", PyInit_grail);
Py_Initialize();
grail_module = PyImport_ImportModule("grail");

如果 Cython 模块位于包中,则 .h 文件的名称将包含模块的完整点分隔名称,例如,名为 foo.spam 的模块将有一个名为 foo.spam.h 的头文件。

注意

在某些操作系统(如 Linux)上,也可以首先以通常的方式构建 Cython 扩展,然后像动态库一样链接到生成的 .so 文件。请注意,这不是可移植的,因此应避免。

C++ 公共声明

当一个文件被编译为 C++ 时,它的公共函数默认情况下被声明为 C++ API(使用 extern "C++")。这禁止从 C 代码调用这些函数。如果这些函数确实是作为纯 C API 的,则需要由用户手动指定 extern 声明。这可以通过在生成 C++ 文件的编译过程中将 CYTHON_EXTERN_C C 宏设置为 extern "C" 来完成。

from setuptools import Extension, setup
from Cython.Build import cythonize

extensions = [
    Extension(
        "module", ["module.pyx"],
        define_macros=[("CYTHON_EXTERN_C", 'extern "C"')],
        language="c++",
    )
]

setup(
    name="My hello app",
    ext_modules=cythonize(extensions),
)

C API 声明

使声明可用于 C 代码的另一种方法是使用 api 关键字声明它们。您可以将此关键字与 C 函数和扩展类型一起使用。将生成一个名为 modulename_api.h 的头文件,其中包含函数和扩展类型的声明,以及一个名为 import_modulename() 的函数。

想要使用这些函数或扩展类型的 C 代码需要包含头文件并调用 import_modulename() 函数。然后可以像往常一样调用其他函数并使用扩展类型。

如果想要使用这些函数的 C 代码是多个共享库或可执行文件的一部分,则需要在使用这些函数的每个共享库中调用 import_modulename() 函数。如果您在调用这些 api 调用之一时遇到段错误(在 linux 上为 SIGSEGV),这很可能是导致段错误的 api 调用的共享库在发生崩溃的 api 调用之前没有调用 import_modulename() 函数的指示。

包含 modulename_api.h 时,Cython 模块中的任何公共 C 类型或扩展类型声明也可用。

# delorean.pyx

cdef public struct Vehicle:
    int speed
    float power

cdef api void activate(Vehicle *v) except *:
    if v.speed >= 88 and v.power >= 1.21:
        print("Time travel achieved")
# marty.c
#include "delorean_api.h"

Vehicle car;

int main(int argc, char *argv[]) {
	Py_Initialize();
	import_delorean();
	car.speed = atoi(argv[1]);
	car.power = atof(argv[2]);
	activate(&car);
	/* Error handling left out - call PyErr_Occurred() to test for Python exceptions. */
	Py_Finalize();
}

注意

在 Cython 模块中定义的任何用作导出函数的参数或返回值类型的类型都需要声明为公共类型,否则它们不会包含在生成的 header 文件中,并且在尝试编译使用 header 的 C 文件时会遇到错误。

使用 api 方法不需要将使用声明的 C 代码以任何方式链接到扩展模块,因为 Python 导入机制用于动态建立连接。但是,只能通过这种方式访问函数,而不能访问变量。还要注意,为了正确设置模块导入机制,用户必须调用 Py_Initialize()Py_Finalize(); 如果在调用 import_modulename() 时遇到段错误,则可能是没有完成此操作。

您可以在同一个函数上同时使用 publicapi,使其可以通过两种方法访问,例如:

cdef public api void belt_and_braces() except *:
    ...

但是,请注意,您应该在给定的 C 文件中包含 modulename.hmodulename_api.h,而不是两者都包含,否则您可能会遇到冲突的双重定义。

如果 Cython 模块位于包中,则

  • 头文件名称包含模块的完整点分隔名称。

  • 导入函数的名称包含完整名称,其中点被双下划线替换。

例如,名为 foo.spam 的模块将有一个名为 foo.spam_api.h 的 API 头文件和一个名为 import_foo__spam() 的导入函数。

多个公共和 API 声明

您可以通过将它们包含在 cdef 块中,一次性将一组项目声明为 public 和/或 api,例如:

cdef public api:
    void order_spam(int tons) except *
    char *get_lunch(float tomato_size) except NULL

这在 .pxd 文件中(参见 在 Cython 模块之间共享声明)可能很有用,以便通过所有三种方法使模块的公共接口可用。

获取和释放 GIL

Cython 提供了获取和释放全局解释器锁 (GIL) 的功能(参见 我们的词汇表外部文档)。当从多线程代码调用可能阻塞的(外部 C)代码时,或者当想要从(本机)C 线程回调中使用 Python 时,这可能很有用。显然,只有对于线程安全的代码或使用其他方法来防止竞争条件和并发问题的代码,才应该释放 GIL。

请注意,获取 GIL 是一个阻塞线程同步操作,因此可能很昂贵。对于较小的计算,可能不值得释放 GIL。通常,I/O 操作和并行代码中的大量计算将从中受益。

释放 GIL

您可以使用 with nogil 语句在代码段周围释放 GIL

with nogil:
    <code to be executed with the GIL released>

在 with 语句体内的代码不能以任何方式操作 Python 对象,并且不能调用任何操作 Python 对象的函数,除非先重新获取 GIL。Cython 在编译时会验证这些操作,但无法查看外部 C 函数,例如。为了使 Cython 的检查有效,必须正确声明它们是否需要 GIL(见下文)。

从 Cython 3.0 开始,一些简单的 Python 语句可以在 nogil 部分内使用:raiseassertprint(Py2 语句,而不是函数)。由于它们往往是单独的 Python 语句,Cython 会为了方便起见自动在它们周围获取和释放 GIL。

获取 GIL

要作为从没有 GIL 的 C 代码中调用的回调使用的 C 函数,需要在操作 Python 对象之前获取 GIL。这可以通过在函数头中指定 with gil 来实现

cdef void my_callback(void *data) with gil:
    ...

如果回调可能从另一个非 Python 线程调用,则必须先通过调用 PyEval_InitThreads() 初始化 GIL。如果你已经在你的模块中使用 cython.parallel,这将已经被处理。

也可以通过 with gil 语句获取 GIL

with gil:
    <execute this block with the GIL acquired>

条件获取 / 释放 GIL

有时使用条件来决定是否在有或没有 GIL 的情况下运行一段代码很有帮助。这段代码无论如何都会运行,区别在于 GIL 是否会被持有或释放。条件必须是常量(在编译时)。

这对于分析、调试、性能测试以及融合类型(见 条件 GIL 获取 / 释放)可能很有用。

DEF FREE_GIL = True

with nogil(FREE_GIL):
    <code to be executed with the GIL released>

    with gil(False):
       <GIL is still released>

将函数声明为可调用且不使用 GIL

你可以在 C 函数头或函数类型中指定 nogil 来声明它可以在没有 GIL 的情况下安全调用。

cdef void my_gil_free_func(int spam) nogil:
    ...

当你在 Cython 中实现这样的函数时,它不能有任何 Python 参数或 Python 对象返回类型。此外,任何涉及 Python 对象的操作(包括调用 Python 函数)都必须先显式获取 GIL,例如通过使用 with gil 块或调用已定义为 with gil 的函数。这些限制由 Cython 检查,如果它在 nogil 代码部分中发现任何 Python 交互,你将收到编译错误。

注意

nogil 函数注释声明在没有 GIL 的情况下调用该函数是安全的。在持有 GIL 的情况下执行它完全是允许的。如果函数被调用者持有,它本身不会释放 GIL。

将函数声明为 with gil(即在进入时获取 GIL)也会隐式地使其签名为 nogil