第七章 用Cython包装C库

用Cython包装C库

前面我们学习了如何使用Cython通过静态编译将Python性能得到提升,本章我们将关注逆向问题:从C库开始,如何让它能够被Python访问。这样的任务通常是交给专业的工具如SWIG、SIP、Boost.Python、ctypes、cffi或者其他的。Cython虽然不会像某些工具那样自动化处理,但是它提供了简单的包装外部库的方法。Cython也可以让C级别的Cython结构被外部的C访问,这对于我们想要将Python嵌入到C语言中很有用。

在Cython中声明外部C代码

为了用Cython包装C库,首先要在Cython中声明要是用的C组件的接口,最后,Cython提供了extern语句块。这些声明意在告诉Cython我们要用的C组件来自哪个特殊的C头文件,语法如下:

1
2
cdef extern from "header_name":
indented declarations from header file

包含extern语句块有如下作用:

  • cython编译器会在生成的源文件头部添加一句#include “header_name”代码
  • 代码块中的类型、函数和其他的对象声明都可以从Cython访问
  • Cython会在编译的时候检查C声明的类型使用是否正确,不正确会报错
    extern语块中声明的变量和函数都有一个简单的类C风格的语法,他们使用Cython特殊的语法声明struct和union,在第三章有简单介绍过。
    Cython支持extern关键字,可以通过cdef添加任何C声明,语法如下:
    1
    cdef extern external_declaration

当我们用这种方式使用extern,Cython将放置这些声明如变量、struct、union或者其他C声明到有extern修饰符的生成的源码中,Cython的extern声明必须匹配C的声明。这种风格的声明是不推荐的,他和直接在C中使用extern有相同的缺点,建议优先使用extern语块。
对于一个特殊的头文件如果有必要有一个#include预处理指令,但是不需要声明,声明语块可以是空:

1
2
cdef extern from "header.h":
pass

相反,如果头文件不是必要的,也许已经被其他的头文件包含了,但是我们希望与外部有接口,我们可以抑制生成#include语块:

1
2
cdef extern from *:
declarations

在我们进入声明块的细节之前,extern是什么也不会做的。

Cython不会自动包装

extern语块的目的是简单,但是可能会被误导。在Cython中,extern语块和extern声明的存在是为了确保我们以一种正确的类型方式调用和使用的声明的C函数,变量和结构体。extern语块不会自动的为声明的对象生成包装器,extern语块只会在C代码前面添加一行#include “header.h”代码。我们还是要编写def,cdef和cpdef函数调用extern语块中声明的C函数。如果我们不这样做,extern语块中声明的外部C函数就不会被Python代码访问到。Cython不会解析C文件和自动包装C库。
使用Cython包装有上百个函数,结构体和其他结构的庞大的C项目任务很艰难,但是已经不少成功的先例了,他们选择Cython作为包装工具有下面一些原因:

  • Cython生成的包装代码是高度优化的,比其他的工具的速度快一个数量级
  • 通常的目标是定制,改进,简化或者相反Pythonize化包装的接口,所以一个自动包装的工具不会提供太多的好处
  • Cython是一门高级的Python-like语言,不限于特定领域的接口命令,使复杂的包装任务更加容易

声明外部的C函数和typedef

extern语块中最常见的声明是C函数和typedef,这些声明几乎直接从C中转换过来。通常,唯一要修改的有下面几点:

  • 将typedef换成ctypedef
  • 移除不必要的和不支持的关键字,如restrict和volatile
  • 确保返回的函数类型和名称在单独的一行声明过
  • 移除行结束分号
    在参数列表的开始括号之后,可以在若干行上分割一个长函数声明,就像在Python中一样。下面例子中,header.h中有简单的C声明和宏定义:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /*header.h*/
    #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中extern语块对他们的声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
cdef extern from "header.h":
double M_PI
float MAX(float a, float 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)

Cython支持全方位的C声明,甚至是函数指针,当然,简单的类型声明,大部分情况下我们可以直接复制粘贴C函数声明到extern语块中,删除分号就可以了。

1
2
3
4
5
6
7
8
#复杂指针声明案例
cdef extern from "header.h":
void (*signal(void(*)(int)))(int)
#也可以这样声明
cdef extern from "header.h":
ctypedef void (*void_int_fptr)(int)
void_int_fptr signal(void_int_fptr)

声明和包装C的struct,union和enum

为了在extern语块中声明额外的struct,union和enum结构,我们可以使用第三章提到的语法,但是可以省略cdef关键字,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cdef extern from "header_name":
struct struct_name:
struct_members
union union_name:
union_members
enum enum_name:
enum_members
#对应的C代码是
struct struct_name {
struct_members
};
union union_name {
union_members
};
enum enum_name {
enum_members
};

Cython会为生成等价的struct,union和enum结构,相应的,typedef的版本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cdef extern from "header_name":
ctypedef struct struct_alias:
struct_members
ctypedef union union_alias:
union_members
ctypedef enum enum_alias:
enum_members
#对应的C代码是
typedef struct struct_name {
struct_members
} struct_alias;
typedef union union_name {
union_members
} union_alias;
typedef enum enum_name {
enum_members
} enum_alias;

在typedef的版本中,Cython只会使用类型别名来进行声明,但是不会生成定义中的struct,union和enum。
在Cython中静态声明一个struct变量,使用cdef和struct名称或者typedef别名,Cython在任何情况下都会为我们做正确的事情。
定义struct,union和enum时,属性字段是必须的,如果struct没有属性字段,可以使用pass代替。

包装C函数

当我们声明了我们想使用的额外的函数,我们还需要包装他们成一个def函数,cpdef函数或者是cdef class,让他们能被Python访问。
如下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#mt.pxd
cdef extern from "mt19937ar.h":
void init_genrand(unsigned long s)
double genrand_real1()
#mt_random.pyx
cimport mt
def init_state(unsigned long s):
init_genrand(s)
def rand():
return genrand_real1()
#setup.py
from distutils.core import setup, Extension
from Cython.Build import cythonize
ext = Extension("mt_random", sources=["mt_random.pyx", "mt19937ar.c"])
setup(name="mersenne_random", ext_modules = cythonize([ext]))
#编译
$ python setup.py build_ext --inplace

为了使它们一起编译,我们使用了distutils脚本,另外我们还必须包含mt19937ar.c源文件,然后使用第二章讲到的内容进行编译。如果命令执行成功,会生成一个mt_random.so或者mt_random.pyd文件,这取决于编译系统是Linux,Mac OS还是Windows。然后就可以在ipython中使用它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In [1]: import mt_random
In [2]: mt_random.init_state(42)
In [3]: mt_random.rand()
Out[3]: 0.37454011439684315
In [4]: mt_random.init_genrand(42)
Traceback (most recent call last):
File "<ipython-input-2-34528a64a483>", line 1, in <module>
mt_random.init_genrand(42)
AttributeError: 'module' object has no attribute 'init_genrand'
In [5]: mt_random.genrand_real1()
Traceback (most recent call last):
File "<ipython-input-3-23619324ba3f>", line 1, in <module>
mt_random.genrand_real1()
AttributeError: 'module' object has no attribute 'genrand_real1'

我们不能直接在Python中使用init_genrand或者genrand_real1函数。

使用扩展类型包装C语言struct

我们的C代码有些面一些声明函数:

1
2
3
4
5
6
7
typedef struct _mt_state mt_state;
mt_state *make_mt(unsigned long s);
void free_mt(mt_state *state);
double genrand_real1(mt_state *state);

Cython的extern语句声明他们只需要简单的复制粘贴就好了:

1
2
3
4
5
cdef extern from "mt19937ar-struct.h":
ctypedef struct mt_state
mt_state *make_mt(unsigned long s)
void free_mt(mt_state *state)
double genrand_real1(mt_state *state)

因为mt_state是透明的,Cython不必访问他的任何字段,所以用ctypedef声明就够了,本质上,mt_state是一个命名占位符。
但是extern语块中的声明Python都不能访问,所以有必要将他们包装成扩展类型,这里命名为MT。因为初始化一个MT对象之前创建mt_state的堆分配操作必须发生在C层面,所以要在正确的地方实现cinit()方法和相对应的dealloc()方法释放资源。然后我们定义def或者cpdef方法调用相应的C函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#mt_random_type.pyx
cdef class MT:
cdef mt_state *_thisptr
def __cinit__(self, unsigned long s):
self._thisptr = make_mt(s)
if self._thisptr == NULL:
msg = "Insufficient memory."
raise MemoryError(msg)
def __dealloc__(self):
if self._thisptr != NULL:
free_mt(self._thisptr)
cpdef double rand(self):
return genrand_real1(self._thisptr)

为了使用这个扩展类型的包装,我们必须编译它为一个扩展模块。我们编译这个包装的mt_random_type.pyx文件和mt19937ar-struct.c源代码。

1
2
3
4
5
6
7
8
9
10
#setup_mt_type.py
from distutils.core import setup, Extension
from Cython.Build import cythonize
ext_type = Extension("mt_random_type", sources=["mt_random_type.pyx", "mt19937ar-struct.c"])
setup( name="mersenne_random", ext_modules = cythonize([ext_type]) )
#进行编译
$ python setup_mt_type.py build_ext --inplace

然后可以导入使用:

1
2
3
4
In [1]: from mt_random_type import MT
In [2]: mt1, mt2 = MT(0), MT(0)
In [3]: mt1.rand() == mt2.rand()
Out[3]: True

在Cython中包装C struct,上面的例子是比较常见和推荐的模式,struct指针只在内部使用,定义cinit()和dealloc()方法用来初始化和自动释放内存,定义对应的cpdef方法供Python访问,甚至可以在Python子类中被重写。

常量,其他修饰符和控制Cython生成的内容

在第三章提到过,Cython语言理解const关键字,但是def关键字声明没有用,它要在特定情况下在extern语块中声明保证Cython生成正确的代码。const关键字在声明函数参数的时候没有必要,可以省略掉,当我们声明typedef用到了const或者一个函数的返回值用到了const的时候可能需要保留。

1
2
3
4
5
6
7
8
#C语言中的声明
typedef const int * const_int_ptr;
const double *returns_ptr_to_const(const_int_ptr);
#Cython中的声明
cdef extern from "header.h":
ctypedef const int * const_int_ptr
const double *returns_ptr_to_const(const_int_ptr)

其他C级别的修饰符,如volatile和restrict应该在extern语块中删除,他们会导致编译时错误。
有时候在Cython中使用函数,struct或者typedef的别名很有用,它允许我们在Cython中引用C级别中的一个名字但是不同于在C中的实际的名字。假设我们想包装使用C语言中的print函数,但是会和Cython中的print造成冲突,我们就可以使用别名:

1
2
cdef extern from "printer.h":
void _print "print"(fmt_str, arg)

在Cython中调用_print函数就是调用C中的print函数,同样的typedefs,structs,unions和enums也可以这样使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cdef extern from "pathological.h":
# typedef void * class
ctypedef void * klass "class"
# int finally(void) function
int _finally "finally"()
# struct del { int a, b; };
struct _del "del":
int a, b
# enum yield { ALOT; SOME; ALITTLE; };
enum _yield "yield":
ALOT
SOME
ALITTLE

在任何情况下,引号中的字符串表示C语言中的代码,Cython没有检查引号中的内容,所以这个特性可以用来控制C级别的声明,注意不要滥用。

暴露Cython代码给C使用

正如我们在第三章看到的,Cython允许我们使用cdef关键字声明C级别的函数、变量和结构体,看到了我们如何在Cython中直接使用C级别的结构。假设,在一个应用中,一个外部的C函数调用cdef Cython函数是很有用的,本质上是用C语言包装Python。虽然这情况种使用需求很小,但是确实有需求,Cython提供了两种机制来支持这个需求。

第一种机制是通过public关键字,我们已经看到public关键字可以声明扩展类型的属性外不可见性。这里我们看看他不同的用途。如果我们添加public关键字到C级别的cdef类型,变量或者函数声明,然后这个结构可以被编译的C代码访问或者被扩展模块链接。
如下面例子:

1
2
3
4
5
6
#transcendentals.pyx
cdef public double PI = 3.1415926
cdef public double get_e():
print "calling get_e()"
return 2.718281828

当我们从transcendentals.pyx生成扩展模块时,public修饰的结构会被cython编译器输出一个transcendentals.h头文件添加到transcendentals.c中。这个头文件声明了对Cython源码的公共C接口。它必须被包含在外部的C代码中,如果外部的C代码想调用get_e()函数或者使用变量PI。
外部的C代码调用我们的Cython代码必须同时确保使用Py_Initialize初始化Python解释器和使用inittranscendentals初始化模块,在我们使用任何public结构之前。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//main.c
#include "Python.h"
#include "transcendentals.h"
#include <math.h>
#include <stdio.h>
int main(int argc, char **argv)
{
Py_Initialize();
inittranscendentals();
printf("pi**e: %f\n", pow(PI, get_e()));
Py_Finalize();
return 0;
}

编译运行过程:

1
2
3
4
5
6
7
8
9
10
#首先编译transcendentals.pyx代码生成transcendentals.c
$ cython transcendentals.pyx
#然后编译我们的main.c代码
$ gcc $(python-config --cflags) $(python-config --ldflags) transcendentals.c main.c
#然后运行结果
$ ./a.out
calling get_e()
pi**e: 22.459157

第二个机制是使用api关键字,它只能连接到C级别的函数和扩展类型。
例子如下:

1
2
3
cdef api double get_e():
print "calling get_e()"
return 2.718281828

api和public修饰符都可以运用于同一个对象。
和public关键字的使用方法类似,api关键字会导致cython编译器生成transcendentals_api.h头文件,它可以被外部的C代码使用,用来调用api声明的函数和方法的Cython代码。这种方法更灵活,它使用Python的导入机制动态地导入API声明的函数,而无需显式地编译扩展模块源或链接到动态库。唯一的要求是import_transcendentals应该在使用get_e()之前被调用。

1
2
3
4
5
6
7
8
9
//main.c
#include "transcendentals_api.h"
#include <stdio.h>
int main(int argc, char **argv)
{
import_transcendentals();
printf("e: %f\n", get_e());
return 0;
}

注意,通过api关键字的方法并不能访问变量PI,我们应该通过函数来使用变量PI,api关键字只能访问函数和扩展类型。这是api机制通过动态运行时导入提供的灵活性的折中。

错误检查并引发异常

外部C函数通过返回代码或错误标志来传递错误状态是很常见的。为了正确的包装这些函数,我们我们必须在包装函数中测试这些情况,并在发出错误时,显式地抛出一个Python异常。使用一个expect子句自动将C错误返回代码转换为Python异常是很方便的,但这样做是行不通的,这不是expect的目的。当一个外部的C函数设置C错误状态Cython不能自动检测。然而,expect子句可以和cdef回调结合使用。

回调

正如我们前面看到的,Cython支持C函数指针。使用此功能,我们可以包装C函数通过函数指针回调。回调可以是一个纯C函数,它不调用Python或C API,或者它可以调用任意的Python代码,这取决于用例。这个强大的特性允许我们传递运行时创建的Python函数来控制底层C函数的行为。跨语言的回调工作很复杂,特别是当它涉及到适当的异常处理。
具体细节不讲解,举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#想要包装C标准库中的qsort函数
cdef extern from "stdlib.h":
void qsort(void *array, size_t count, size_t size, int (*compare)(const void *, const void *))
#我们创建pysort函数用来排序Python的数字序列,通过调用C的qsort和不同的比较函数
cdef extern from "stdlib.h":
void *malloc(size_t size)
void free(void *ptr)
ctypedef int (*qsort_cmp)(const void *, const void *)
def pyqsort(list x, reverse=False):
cdef:
int *array
int i, N
#分配C数组
N = len(x)
array = <int*>malloc(sizeof(int) * N)
if array == NULL:
raise MemoryError("Unable to allocate array.")
#用Python数组填充C数组
for i in range(N):
array[i] = x[i]
cdef qsort_cmp cmp_callback
#选择一个适当的回调函数
if reverse:
cmp_callback = reverse_int_compare
else:
cmp_callback = int_compare
#调用qsort排序数组
qsort(<void*>array, <size_t>N, sizeof(int), cmp_callback)
#将结果转换成Python的序列类型,并释放资源
for i in range(N):
x[i] = array[i]
free(array)
#比较回调函数
cdef int int_compare(const void *a, const void *b):
cdef int ia, ib
ia = (<int*>a)[0]
ib = (<int*>b)[0]
return ia - ib
#逆向比较回调
cdef int reverse_int_compare(const void *a, const void *b):
return -int_compare(a, b)

编译运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
In [1]: import pyximport; pyximport.install()
Out[1]: (None, <pyximport.pyximport.PyxImporter at 0x101c7c650>)
In [2]: from pyqsort import pyqsort
In [3]: pyqsort?
Type: builtin_function_or_method
String Form:<built-in function pyqsort>
Docstring: <no docstring>
In [4]: from random import shuffle
In [5]: intlist = range(10)
In [6]: shuffle(intlist)
In [7]: print intlist
[2, 1, 3, 7, 6, 4, 0, 9, 5, 8]
In [8]: pyqsort(intlist)
In [9]: print intlist
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [10]: pyqsort(intlist, reverse=True)
In [11]: print intlist
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

回调和异常传播

到目前为止,任何cmp抛出的异常都会被忽略掉,为了解决这个限制,我们可以在声明cdef回调函数的使用使用except 语句,并且except 语句是函数声明的一部分,最终我们上面的qsort函数声明会变成下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cdef extern from "stdlib.h":
void qsort(void *array, size_t count, size_t size, int (*compare)(const void *, const void *) except *)
ctypedef int (*qsort_cmp)(const void *, const void *) except *
cdef int int_compare(const void *a, const void *b) except *:
# ...
cdef int reverse_int_compare(const void *a, const void *b) except *:
# ...
cdef int py_cmp_wrapper(const void *a, const void *b) except *:
# ...
cdef int reverse_py_cmp_wrapper(const void *a, const void *b) except *:

因为我们使用了except *子句,每一次的回调之后都会检查异常,这就意味着将花费更多的开销,然而为此改进错误处理是值得的。

文章目录
  1. 1. 在Cython中声明外部C代码
  2. 2. Cython不会自动包装
  3. 3. 声明外部的C函数和typedef
  4. 4. 声明和包装C的struct,union和enum
  5. 5. 包装C函数
  6. 6. 使用扩展类型包装C语言struct
  7. 7. 常量,其他修饰符和控制Cython生成的内容
  8. 8. 暴露Cython代码给C使用
  9. 9. 错误检查并引发异常
  10. 10. 回调
  11. 11. 回调和异常传播
|