楔子
在前面的文章中我们知道了 Cython 如何通过提前编译的方式来对 Python 代码进行加速,本次我们聚焦在另一个方向上:假设有一个现成的 C 源文件,那么如何才能让 Python 访问它呢?
事实上,Python 访问 C 源文件,我在其它文章中介绍过。当时的方式是将 C 源文件编译成动态库,然后通过 Python 自带的 ctypes 模块来调用它,当然除了 ctypes,还有 swig、cffi 等专门的工具。而 Cython 也是支持我们访问 C 源文件的,只不过它是通过包装的方式让我们访问。
因为 Cython 同时理解 C 和 Python,所以它可以在 Python 语言和 C 语言结合的时候控制所有的方方面面,在完成这一壮举的同时,不仅保持了 Python 的风格,还使得 Cython 代码更加容易定位和调试。
如果做得好的话,那么生成的扩展将具有 C 级的性能、最小的包装开销,和一个友好的 Python 接口,我们也不需要怀疑正在使用的是包装的 C 代码。那么下面就来看看 Cython 如何包装 C 源文件。
要用 Cython 包装 C 源文件,我们必须在 Cython 中声明我们要使用的 C 组件的接口。为此,Cython 提供了一个 extern 语句,它的目的就是告诉 Cython,我们希望从指定的 C 头文件中使用 C 结构。语法如下:
cdef extern from "header_name": # 你希望使用哪些 C 结构, 那么就在这里声明 # 如果不需要的话可以写上一个pass
我们知道 C 头文件存放声明,C 源文件存放实现。而在 C 语言中,如果一个源文件想使用另一个源文件定义的函数,那么只需要导入相应的头文件即可,会自动去源文件中寻找对应的实现。
比如在 a.c 中想使用 b.c 里面的一个函数,那么我们需要在 a.c 中 #include "b.h",然后就可以使用 b.c 里面的内容了。而对于当前的 Cython 也是同理,如果想要包装 C 源文件,那么也是要引入对应的头文件的,通过 cdef extern from 来引入。引入之后也可以在 Cython 里面直接使用,真的是非常方便,因为我们说 Cython 它同时理解 C 和 Python。此外 Cython 还会在编译时检查 C 的声明是否正确,如果不正确则出现编译错误。
下面我们就来详细介绍 cdef extern from 的用法。
声明外部的 C 函数以及给类型起别名
extern 块中最常见的声明是 C 函数和 typedef,这些声明几乎可以直接写在 Cython 中,只需要做一下修改。
1)将 typedef 变成 ctypedef;
2)删除类似于 restrict、volatile 等不必要、以及不支持的关键字;
3)确保函数的返回值类型和函数名在同一行;
// 在 C 中,可以这么写,但是 Cython 不行 // 因为 Cython 要求返回值类型和函数名在同一行 int foo(){ return 123 }
4)删除行尾的分号;
下面我们定义一个 C 的头文件:header.h,写上一些简单的 C 声明和宏。
#define M_PI 3.1415926 #define MAX(a, b) ((a) >= (b) ? (a) : (b)) double hypot(double, double); typedef int integral; typedef double real; void func(integral, integral, real); real *func_arrays(integral[], integral[][10], real **);
如果你想在 Cython 中使用的话,那么就把那些想用的写在 Cython 中,当然我们说不能直接照搬,因为 C 和 Cython 的声明还是有些略微的差异的,而差异就是上面说的那些。
cdef extern from "header.h": double M_PI double MAX(double a, double b) double hypot(double x, double y) ctypedef int integral ctypedef double real void func(integral a, integral b, real c) real *func_arrays(integral[] i, integral[][10] j, real **k)
注意:M_PI 这个宏,我们根据值将其声明为 double 类型的变量;同理对于 MAX 宏也是如此,把它当成接收两个 double、返回一个 double 的函数。
另外在 extern 块的声明中,我们为函数参数添加了一个名字。这是推荐的,但并不是强制的;如果有参数名的话,那么可以通过关键字参数调用,对于接口的使用会更加明确。
然后还有一点需要注意,我们上面声明变量和函数的时候,没有使用 cdef。这是因为在 cdef extern from 语句块里面,cdef 可以省略掉,但在外部则不可以省略。
Cython 支持 C 中的所有声明,甚至复杂的函数指针也是可以的,比如:
cdef extern from "header2.h": ctypedef void (*void_int_fptr)(int) void_int_fptr signal(void_int_fptr) # 上面两行等价于 void (*signal(void(*)(int)))(int)
所以我们可以进行非常复杂的声明,当然日常也很少会用到。由于简单的类型声明,像数值、字符串、数组、指针、void 等等已经构成了 C 声明的大多数,因此很多时候我们直接将 C 中的声明复制粘贴过来,然后去掉分号就可以了。
声明并包装 C 结构体、共同体、枚举
关于结构体、共同体和枚举,前面已经介绍过了,这里以结构体为例,再结合头文件说一遍。
// header.h struct Girl1 { char *name; int age; }; typedef struct { char *name; int age; } Girl2;
以上是一个 C 的头文件,我们在 Cython 中导入之后要怎么使用呢?
# cython_test.pyx cdef extern from "header.h": # 定义结构体要和 C 保持一致 # C 使用 struct Girl1{...},那么这里也是如此 # 这里依旧省略了 cdef,写成 cdef struct Girl1 也可以 struct Girl1: char *name int age # C 使用 typedef struct {...} Girl2 # 这里也要使用 ctypedef ctypedef struct Girl2: char *name int age # 对于结构体而言, 里面的成员只能用 C 中的类型 # 而创建结构体实例都是使用 "cdef 结构体类型 变量名 = " 的方式 cdef Girl1 g1 = Girl1("komeiji satori", 16) cdef Girl2 g2 = Girl2("komeiji koishi", age=16) # 可以看到无论是 cdef struct 定义的 # 还是通过 ctypedef 起的类型别名, 使用方式没有任何区别 print(g1) print(g2)
结构体的定义必须是完整的,里面的成员、类型都必须写清楚。所以结构体我们拿到 cdef extern from 外面定义也是可以的,只不过在外面定义的话,第一种定义方式前面的 cdef 关键字不可以省略。
然后我们来进行编译,当涉及到 C 文件时,需要采用手动编译的方式。
from distutils.core import setup, Extension from Cython.Build import cythonize # 我们只导入了一个头文件 ext = [Extension("cython_test", sources=["cython_test.pyx"], include_dirs=["."])] setup(ext_modules=cythonize(ext, language_level=3))
里面的 include_dirs 表示头文件的搜索路径,header.h 位于当前目录。
导入测试:
import cython_test """ {'name': b'komeiji satori', 'age': 16} {'name': b'komeiji koishi', 'age': 16} """
因为里面有 print 所以导入的时候自动打印了,我们看到 C 的结构体到 Python 中会变成字典。
注意:我们使用 cdef extern from 导入头文件的时候,代码块里的声明在 C 头文件中应该已经存在。假设我们还想通过 ctypedef 给 int 起一个别名,但这个逻辑在 C 的头文件中是不存在的,而是我们自己想这么做,那么这个逻辑就不应该放在 cdef extern from 块中,而是应该放在全局区域,否则是不起作用的。
cdef extern from 里面的类型别名、声明什么的,都是根据头文件来的,我们将头文件中想要使用的结构,放在 cdef extern from 中进行声明。而我们自己单独设置的声明(头文件中不存在的逻辑)应该放在外面。
包装 C 函数
在最开始介绍斐波那契数列的时候,我们已经演示过这种方式了,这里再来详细介绍一下。
// header.h typedef struct { char *name; int age; } Girl; // 里面定义一个结构体类型 和 一个函数声明 char *return_info (Girl g); // source.c #include <stdio.h> #include <stdlib.h> #include "header.h" char *return_info (Girl g) { // 堆区申请一块内存 char *info = (char *)malloc(50); // 拷贝一个字符串进去 sprintf(info, "name: %s, age: %d", g.name, g.age); // 返回指针 return info; }
然后在 Cython 里面调用:
from libc.stdlib cimport free cdef extern from "header.h": # C 里面使用 typedef sturct {...} Girl # 那么这里也要使用 ctypedef ctypedef struct Girl: char *name int age # 声明函数时,可以省略 cdef # 当然也可以不省略 char *return_info(Girl) # cdef extern from 里面声明的是 C 函数 # 它类似于使用 cdef 关键字定义的函数,显然无法被外界访问 # 因此我们还需要定义一个包装器 cpdef bytes info(dict d): cdef: # 接收一个字典 str name = d["name"] int age = d["age"] # 根据对应的值创建结构体实例, 但 name 需要转成 bytes 对象 # 因为 char * 对应 Python 的 bytes 对象 cdef Girl g = Girl(name=bytes(name, encoding="utf-8"), age=age) # 构造出结构体实例之后, 传到 C 的函数中 # 得到返回值, 也就是字符串的首地址 cdef char *p = return_info(g) # 这里需要先拷贝给 Python # 会根据 char *p 创建一个 bytes 对象, 然后让变量 res 指向 # 至于为什么不直接返回 p, 是因为 p 是在堆区申请的, 我们需要将它释放掉 cdef bytes res = p free(p) # 返回 res return res
然后来进行编译:
from distutils.core import setup, Extension from Cython.Build import cythonize ext = [Extension("cython_test", sources=["cython_test.pyx", "source.c"], include_dirs=["."])] setup(ext_modules=cythonize(ext, language_level=3))
导入测试:
import cython_test print(cython_test.info({"name": "satori", "age": 16})) """ b'name: satori, age: 16' """
我们看到整体没有任何问题,但很明显这个例子有点刻意了,故意兜这么一个圈子。但这么做主要是想介绍 C 和 Cython 之间的交互方式,以及 Cython 调用 C 库是有多么的方便。
当然我们还可以写一些更加复杂的逻辑,比如定义一个类,但这样也会带来一些方便之处,那就是 __dealloc__。我们把指向堆内存的指针的释放逻辑写在这里面,然后当对象被销毁时会自动调用。
另外 cdef extern from 除了可以引入 C 头文件之外,还可以引入 C 源文件:
// source.c int func(int a, int b) { return a + b; }
以上是一个 C 源文件,我们也是可以直接通过 cdef extern from 来引入的:
cdef extern from "source.c": # func 不能直接被 Python 调用,因为它是 C 的函数 # 我们需要手动创建包装器 int func(int a, int b) def py_func(int a, int b): return func(a, b)
下面进行编译,并且当涉及到 C 文件时,我们需要手动编译。
from distutils.core import setup, Extension from Cython.Build import cythonize ext = [Extension("cython_test", sources=["cython_test.pyx"])] setup(ext_modules=cythonize(ext, language_level=3))
在sources参数里面只写了 cython_test.pyx,并没有写 source.c。原因是它已经在 pyx 文件中通过 cdef extern from 的方式引入了,如果这里再将其指定在 sources 参数中的话,那么相当于将 source.c 里面的内容写入了两遍,在编译的时候就会出现符号多重定义的错误。
但如果导入的是只存放声明的头文件,那么为了在编译的时候能找到具体的实现,就必须要在 sources 参数中指定 C 源文件,否则编译时会出现符号找不到的错误。
编译成扩展模块之后导入一下:
import cython_test print(cython_test.py_func(22, 33)) # 55
我们看到是没有问题的,但规范的做法是头文件存放声明,源文件存放具体实现,然后 cdef extern from 导入头文件,编译时在 sources 参数中指定源文件。
头文件的包含
如果一个头文件包含了另一个头文件,比如:在 a.h 里面引入了 b.h 和 c.h,那我们只需要 cdef extern from "a.h" 即可,然后可以同时使用 a.h、b.h、c.h 里面的内容。
// a.h #include "b.h" #include "c.h" #define ADD(x, y) ((x) + (y)) // b.h #define SUB(x, y) ((x) - (y)) // c.h #define MUL(x, y) ((x) * (y))
在头文件中定义了一个宏,而在 Cython 里面我们可以看成是一个函数,函数参数的类型可以是整型、浮点型,只要在 C 里面合法即可。
cdef extern from "a.h": int ADD(int a, int b) int SUB(int a, int b) int MUL(int a, int b) def py_ADD(int a, int b): return ADD(a, b) def py_SUB(int a, int b): return SUB(a, b) def py_MUL(int a, int b): return MUL(a, b)
注意:SUB 函数和 MUL 函数对应的宏分别定义在 b.h 和 c.h 里面,但是我们只引入了 a.h,原因就是 a.h 里面已经包含了 b.h 和 c.h。
当然下面这种做法也是可以的:
cdef extern from "a.h": int ADD(int a, int b) cdef extern from "b.h": int SUB(int a, int b) cdef extern from "c.h": int MUL(int a, int b) def py_ADD(int a, int b): return ADD(a, b) def py_SUB(int a, int b): return SUB(a, b) def py_MUL(int a, int b): return MUL(a, b)
a.h 里面包含了 b.h 和 c.h,所以上面相当于将 b.h 和 c.h 引入了两遍,但 C 允许将一个头文件 include 多次,所以没问题。
可毕竟 a.h 里面已经包含了所有的内容,我们直接在 cdef extern from "a.h" 里面把想要使用的 C 结构写上即可,没有必要再引入 b.h 和 c.h。如果真的不想写在 cdef extern from "a.h"里面的话,还可以这么做:
cdef extern from "a.h": int ADD(int a, int b) cdef extern from *: int SUB(int a, int b) cdef extern from *: int MUL(int a, int b)
因为 SUB 和 MUL 在引入 a.h 的时候就已经在里面了,只不过我们需要显式地在 extern 块里面声明之后才能使用它。而 cdef extern from * 则表示里面的 C 结构在其它使用 cdef extern from 导入的头文件中已经存在了,因此会去别的已导入的头文件中找,所以下面的做法也是可以的:
cdef extern from "a.h": pass cdef extern from *: int ADD(int a, int b) int SUB(int a, int b) int MUL(int a, int b)
在 Cython 中导入了头文件,但是可以不使用里面的 C 结构,并且不用的话需要使用 pass 做一个占位符。而我们将使用的 C 结构写在了 cdef extern from * 下面,表示这些 C 结构在导入的头文件中已经存在了,而我们目前只导入了 a.h,那么 ADD、SUB、MUL 就都会去 a.h 当中找,所以此时也是可以的。
以上四种导入方式都是合法的,可以自己测试一下。
导入头文件的花样还是比较多的,但最好还是以直观、清晰为主,像最后两种导入方式就有点刻意了。
以注释的形式嵌入 C 代码
如果你用过 CGO 的话估计会深有体会, Go 支持以注释的形式嵌入 C 代码,而 Cython 同样是支持的,并且这些 C 代码要写在 extern 块中。当然我们说是注释其实不太准确,应该是三引号括起来的字符串,或者说 docstring 也可以。
// header.h int add(int a, int b);
以上是一个简单的头文件,里面只有一个 add 函数的声明,但是没有具体实现,因为实现我们放在了 pyx 文件中。
cdef extern from "header.h": """ int add(int a, int b) { return a + b; } """ int add(int a, int b) def my_add(int a, int b): return add(a, b)
然后我们来进行编译:
from distutils.core import setup, Extension from Cython.Build import cythonize ext = [Extension("cython_test", sources=["cython_test.pyx"])] setup(ext_modules=cythonize(ext, language_level=3))
最后导入一下进行测试:
import cython_test print(cython_test.my_add(6, 5)) # 11
是不是很有趣呢?直接将 C 代码写在 docstring 里面,等同于写在源文件中。另外我们说 cdef extern from 除了可以导入头文件之外,还可以导入源文件,所以上面的代码还可以再改一下。当然,虽然 Cython 支持这么做,但还是不建议这么使用。
cdef extern from *: """ int add(int a, int b) { return a + b; } """ int add(int a, int b) def my_add(int a, int b): return add(a, b)
此时没有涉及到任何的头文件、源文件,但确实是合法的 Cython 代码,因为我们将 C 代码写在了 docstring 中。不过显然这么做没什么意义,直接在里面通过 cdef 定义一个 C 级函数即可,没必要先用 C 定义、然后再使用 cdef extern from 引入,之所以这么做只是想表明 Cython 支持这种做法。
并且当涉及到 C 时,绝大部分都不是源文件的形式,而是动态库,至于如何引入动态库后面会说,总之通过 docstring 写入 C 代码这个功能了解一下即可。
小结
以上我们就了解了如何使用 Cython 包装外部的 C 代码,具体做法是通过 cdef extern from 引入头文件,在里面写上想要使用的 C 级结构。但头文件只存放声明,而负责具体实现的源文件,则在编译的时候通过 sources 参数指定。
并且 Cython 除了可以包装以源文件形式存在的 C 代码,还可以包装静态库和动态库。而本篇文章介绍的包装方式,需要 C 代码以源文件(或者说文本文件)的形式存在,至于如何包装静态库和动态库,我们后续再聊。
E N D