15.10 用Cython包装C代码
问题
你想使用 Cython 来创建一个 Python 扩展模块,用来包装某个已存在的 C 函数库。
解决方案
使用 Cython 构建一个扩展模块看上去很手写扩展有些类似, 因为你需要创建很多包装函数。不过,跟前面不同的是,你不需要在 C 语言中做这些——代码看上去更像是 Python。
作为准备,假设本章介绍部分的示例代码已经被编译到某个叫 libsample 的 C 函数库中了。 首先创建一个名叫 csample.pxd 的文件,如下所示:
# csample.pxd
#
# Declarations of "external" C functions and structures
cdef extern from "sample.h":
int gcd(int, int)
bint in_mandel(double, double, int)
int divide(int, int, int *)
double avg(double *, int) nogil
ctypedef struct Point:
double x
double y
double distance(Point *, Point *)这个文件在 Cython 中的作用就跟 C 的头文件一样。 初始声明 cdef extern from "sample.h" 指定了所需的 C 头文件。 接下来的声明都是来自于那个头文件。文件名是 csample.pxd ,而不是 sample.pxd ——这点很重要。
下一步,创建一个名为 sample.pyx 的问题。 该文件会定义包装器,用来桥接 Python 解释器到 csample.pxd 中声明的 C 代码。
该文件更多的细节部分会在讨论部分详细展开。 最后,为了构建扩展模块,像下面这样创建一个 setup.py 文件:
要构建我们测试的目标模块,像下面这样做:
如果一切顺利的话,你应该有了一个扩展模块 sample.so ,可在下面例子中使用:
讨论
本节包含了很多前面所讲的高级特性,包括数组操作、包装隐形指针和释放 GIL。 每一部分都会逐个被讲述到,但是我们最好能复习一下前面几小节。 在顶层,使用 Cython 是基于 C 之上。.pxd 文件仅仅只包含 C 定义(类似 .h 文件), .pyx 文件包含了实现(类似 .c 文件)。cimport 语句被 Cython 用来导入 .pxd 文件中的定义。 它跟使用普通的加载 Python 模块的导入语句是不同的。
尽管 .pxd 文件包含了定义,但它们并不是用来自动创建扩展代码的。 因此,你还是要写包装函数。例如,就算 csample.pxd 文件声明了 int gcd(int, int) 函数, 你仍然需要在 sample.pyx 中为它写一个包装函数。例如:
对于简单的函数,你并不需要去做太多的事。 Cython 会生成包装代码来正确的转换参数和返回值。 绑定到属性上的 C 数据类型是可选的。不过,如果你包含了它们,你可以另外做一些错误检查。 例如,如果有人使用负数来调用这个函数,会抛出一个异常:
如果你想对包装函数做另外的检查,只需要使用另外的包装代码。例如:
在 csample.pxd 文件中的 in_mandel() 声明有个很有趣但是比较难理解的定义。 在这个文件中,函数被声明为然后一个 bint 而不是一个 int。 它会让函数创建一个正确的 Boolean 值而不是简单的整数。 因此,返回值 0 表示 False 而 1 表示 True。
在 Cython 包装器中,你可以选择声明 C 数据类型,也可以使用所有的常见 Python 对象。 对于 divide() 的包装器展示了这样一个例子,同时还有如何去处理一个指针参数。
在这里,rem 变量被显示的声明为一个 C 整型变量。 当它被传入 divide() 函数的时候,&rem 创建一个跟 C 一样的指向它的指针。 avg() 函数的代码演示了 Cython 更高级的特性。 首先 def avg(double[:] a) 声明了 avg() 接受一个一维的双精度内存视图。 最惊奇的部分是返回的结果函数可以接受任何兼容的数组对象,包括被 numpy 创建的。例如:
在此包装器中,a.size0 和 &a[0] 分别引用数组元素个数和底层指针。 语法 &a[0] 教你怎样将指针转换为不同的类型。 前提是 C 中的 avg() 接受一个正确类型的指针。 参考下一节关于 Cython 内存视图的更高级讲述。
除了处理通常的数组外,avg() 的这个例子还展示了如何处理全局解释器锁。语句 with nogil: 声明了一个不需要 GIL 就能执行的代码块。 在这个块中,不能有任何的普通 Python 对象——只能使用被声明为 cdef 的对象和函数。 另外,外部函数必须现实的声明它们能不依赖 GIL 就能执行。 因此,在 csample.pxd 文件中,avg() 被声明为 double avg(double *, int) nogil .
对 Point 结构体的处理是一个挑战。本节使用胶囊对象将 Point 对象当做隐形指针来处理,这个在 15.4 小节介绍过。 要这样做的话,底层 Cython 代码稍微有点复杂。 首先,下面的导入被用来引入 C 函数库和 Python C API 中定义的函数:
函数 del_Point() 和 Point() 使用这个功能来创建一个胶囊对象, 它会包装一个 Point * 指针。cdef del_Point() 将 del_Point() 声明为一个函数, 只能通过 Cython 访问,而不能从 Python 中访问。 因此,这个函数对外部是不可见的——它被用来当做一个回调函数来清理胶囊分配的内存。 函数调用比如 PyCapsule_New()、PyCapsule_GetPointer() 直接来自 Python C API 并且以同样的方式被使用。
distance 函数从 Point() 创建的胶囊对象中提取指针。 这里要注意的是你不需要担心异常处理。 如果一个错误的对象被传进来,PyCapsule_GetPointer() 会抛出一个异常, 但是 Cython 已经知道怎么查找到它,并将它从 distance() 传递出去。
处理 Point 结构体一个缺点是它的实现是不可见的。 你不能访问任何属性来查看它的内部。 这里有另外一种方法去包装它,就是定义一个扩展类型,如下所示:
在这里,cdif 类 Point 将 Point 声明为一个扩展类型。 类属性 cdef csample.Point *_c_point 声明了一个实例变量, 拥有一个指向底层 Point 结构体的指针。 __cinit__() 和 __dealloc__() 方法通过 malloc() 和 free() 创建并销毁底层 C 结构体。 x 和 y 属性的声明让你获取和设置底层结构体的属性值。 distance() 的包装器还可以被修改,使得它能接受 Point 扩展类型实例作为参数, 而传递底层指针给 C 函数。
做了这个改变后,你会发现操作 Point 对象就显得更加自然了:
本节已经演示了很多 Cython 的核心特性,你可以以此为基准来构建更多更高级的包装。 不过,你最好先去阅读下官方文档来了解更多信息。
Last updated
Was this helpful?