-
Cython官方文档中文翻译:使用C库
-
说明
尝试翻译Cython Documentation以助学习。
水平有限,乐迎指正;文档首页:《Cython官方文档中文翻译》
-
使用C库
除了代码加速,
Cython
的另一个应用场景是从Python
中调用外部C库。当Cython代码直接编译为C代码,在代码中直接调用C函数就毫不费力了。下面给出一个完整示例展示在Cython
中使用(并封装)一个外部C库,包括适当的错误处理和为Python
、Cython
代码设计合适API
的考量。假设你需要在一个FIFO(First In First Out,先入先出)序列中存储整型数值的高效方式,因为内存至关重要,数据来自C代码,而你又不能在一个list/deque中创建并存储Python int对象。所以,你就要寻找用
C
实现的队列。经历一番网络搜索,你找到了一个
C
算法库 [CAlg],决定用它的双端队列实现( double ended queue implementation)。为简化操作,你决定将他封装到一个Python扩展类型中,此扩展可以压缩封装所有内存管理。[CAlg]Simon Howard, C Algorithms library, http://c-algorithms.sourceforge.net/
-
定义外部声明
此处下载CAIg
定义在头文件c-algorithms/src/queue.h的这种序列实现的C API,内核看起来如下:
/* queue.h*/ typedef struct _Queue Queue; typedef void *QueueValue; Queue *queue_new(void); void queue_free(Queue *queue); int queue_push_head(Queue *queue, QueueValue data); QueueValue queue_pop_head(Queue *queue); QueueValue queue_peek_head(Queue *queue); int queue_push_tail(Queue *queue, QueueValue data); QueueValue queue_pop_tail(Queue *queue); QueueValue queue_peek_tail(Queue *queue); int queue_is_empty(Queue *queue);
第一步是在一个
.pyd
文件中重定义C API,比如cqueue.pxd:# cqueue.pxd cdef extern from "c-alpgorithms/src/queue.h": ctypedef struct Queue: pass ctypedef void* QueueValue Queue* queue_new() void queue_free(Queue* queue) int queue_push_head(Queue* queue, QueueValue data) QueueValue queue_pop_head(Queue* queue) QueueValue queue_peek_head(Queue* queue) int queue_push_tail(Queue* queue, QueueValue data) QueueValue queue_pop_tail(Queue* queue) QueueValue queue_peek_tail(Queue* queue) bint queue_is_empty(Queue* queue)
这些声明与头文件声明几近相同,所以很多时候你可以直接复制。实际上,你并不需要提供上述所有声明,只需要提供你代码或其他声明中需要的部分就可以了,因此
Cython
可以看到他们充分且一致的子集。然后,考虑适应在Cython
中使用他们时更合适。特别地,你应该为C函数仔细挑选足够好的参数名,因为
Cython
允许你以关键字参数传递他们。后续改变他们时一个后向不兼容的API修改。选择好的名字会使来自Cython代码的这些函数更好用。与头文件相比,我们上面用的第一行中的Queue结构声明有重要不同。本例中,Queue用作一个不透明句柄,只有调用的库知道内部具体是什么。因为Cython代码不知道结构,我们不需要对其内容进行声明,只是提供一个空定义(因为我们不想声明C header中提到的*_Queue类型*)1
另一个异常是最后一行,queue_is_empty()函数返回的整数值实际上是一个C布尔值,也就是说它唯一有价值的就在于是否为0,指明序列是否为空。
Cython
的bint类型最能表现这种情况,这种类型在C
中就是一个常规int类型,但是当转换为Python对象时就映射成Python布尔值True和False。这种在*.pxd*文件中绑定声明的方式常常可以简化使用代码。为每个库定义一个*.pxd文件是一个好方式,有时API很大甚至是为每一个头文件(或函数组)。这可以简化模型间的调用。有时你会用到C标准库中的C函数*,或希望从
CPython
直接调用C-API。就常规需求来说,Cython
绑定了一系列可在Cython
中直接使用的提供上述声明的标准*.pxd文件,主要的包是cpython
、libc
、libcpp
。Numpy包也有一个标准.pxd文件即numpy*。在源文件的Cython/Includes文件夹下可查看完整*.pxd*文件列表。 -
编写封装器类
声明C库的API之后,我们开始设计应该包括C queue的Queue类,写在queue.pyx2文件中,下面是Queue 类的开始:
# queue.pyx cimport cqueue cdef class Queue: cdef cqueue.Queue* _c_queue def __cinit__(self): self._c_queue = cqueue.queue_new()
上文用
__cinit__
而非__init__
,虽然后者也可以用,但不能保证一定能运行(比如,创建子类却网及调用父类的构造函数)。因为不初始化C指针,通常会导致Python解释器的硬崩溃,Cython
提供构建时在考虑调用__init__
之前就立即调用的__cinit__
,因此是初始化新实例cdef作用域的正确位置。然,__cinit__
是在对象构建过程中调用的,self尚未构建完成,必须避免除了给cdef领域赋值之外的任何与self有关的操作。也要意识到上述代码没有参数,尽管子类型可能需要接受一些。无参数
__cinit__()
方法是特例,它不接受传递给构造函数的任何参数,索引它不阻止子类添加参数。如果__cinit__()
签名中使用了参数,他们必须与实例化类型
类等级中那些已声明的__init__
方法相匹配。 -
内存管理
在我们实现其他方法之前,先要明白上述实现其实不安全。万一调用queue_new()出错,这段代码将简单的吞下(swallow)错误,程序很可能一会后崩溃。根据queue_new()函数文档介绍,上述操作失败的唯一原因是内存不足(insufficient memory)。那样的话,就会返回NULL,而通常这里是返回一个指向新队列的指针。
Python
解决这个问题的方式是抛出MemoryError3。因此我们可以更改初始化函数如下:# queue.pyx cimport cqueue cdef class Queue: cdef cqueue.Queue* _c_queue def __cinit__(self): self._c_queue = cqueue.queue_new() if self._c_queue is NULL: raise MemoryError()
当Queue实例不再使用(即所有对其引用已删除),下一步就是清理。为此,CPython提供了一份回调函数,该回调函数由
Cython
作为一个特殊方法__dealloc__()
使用。在我们的案例中,我们只需释放C Queue,当然只有在我们在init方法中成功初始化的前提下。def __dealloc__(self): if self._c_queue is not NULL: cqueue.queue_free(self._c_queue)
-
编译和链接
现在,我们有了一个可供测试的可用
Cython
模块。为编译,我们要为distutils
配置一个setup.py脚本。下面是编译Cython模块的基础脚本:from distutils.core import setup from distutils.extension import Extension from Cython.Build import cythonize setup( ext_modules = cythonize([Extension("queue", ["queue.pyx"])]) )
针对外部C库构建,我们需要确保Cython能找到必要的库。方式有两种:
- 我们告诉distutils哪里找自动编译queue.c实现的c源码;
- 我们构建并安装C-AIg作为系统库,并动态链接;
如果其他应用也用到了C-AIg,后者有效。
-
静态链接
想要自动构建c代码,需要在queue.pyx文件中包含编译器指令:
# distutils: sources = c-algorithms/src/queue.c # distutils: include_dirs = c-algorithms/src/ cimport cqueue cdef class Queue: cdef cqueue.Queue* _c_queue def __cinit__(self): self._c_queue = cqueue.queue_new() if self._c_queue is NULL: raise MemoryError() def __dealloc__(self): if self._c_queue is not NULL: cqueue.queue_free(self._c_queue)
sources
编译器指令给出了distutils
将要编译及链接(静态地)到最终扩展模块中的C文件所在路径。一般所有相关头文件都在include_dirs
中,现在我们可以用下述代码构建工程:$ python setup.py biuld_ext -i
之后测试构建是否成功:
$ python -c 'import queue; Q = queue.Queue()'
-
动态链接
如果我们将要封装地库已经安装在系统内,动态链接(Dynamic linking)是有用的。要执行动态链接,我们首先需要构建和安装c-alg。
在你的系统上构建c-algorithms:
$ cd c-algorithms $ sh autogen.sh $ ./configure $ make
安装CAIg:
$ make install
如此之后,应该有
/usr/local/lib/libcalg.so
这个路径是
Linux
系统上的,其他系统会有区别。故而,在后文中需要依据不同系统上libcalg.so或libcalg.dll所在路径调整余下的教程。在此方法内,我们需要告诉setup脚本与外部库链接,为此需要扩展setup脚本,从:
ext_modules = cythonize([Extension("queue", ["queue.pyx"])])
到:
ext_modules= cythonize([ Extension("queue", ["queue.pyx"], libraries = ["calg"]) ])
至此,我们可以构建工程:
$ python setup.py build_ext -i
如果libcalg没有安装在常规位置,用户可以通过传入合适的C编译器标记提供所需的外部参数:
CFLAGS="-I/usr/local/otherdir/calg/include" \ LDFLAGS="-L/usr/local/otherdir/calg/lib" \ python setup.py build_ext -i
运行模型之前,先确保libcalg在LD_LIBRARY_PATH环境变量内,即通过下述设置:
$ export LD_LIBRARY_PATH = $ LD_LIBRARY_PATH:/usr/local/lib
一旦我们首次编译了模块,就可以导入他并实例化一个新队列(Queue):
$ export PYTHONPATH=. $ python -c 'import queue; Q = queue.Queue()'
至此所谈都是Queue类能做的,下面我们丰富他的功能。
-
映射功能
-
References
cdef struct Queue:pass
与ctypedef struct Queue:pass
之间有一个微妙的不同,前者声明一种在C代码中作为struct Queue的类型,而后者在C代码中引用为Queue,这是Cython无法隐藏的一个C语言怪癖。大部分现代C库使用ctypedef这类结构。 ↩︎.pyx文件的文件名需不同于包含源自C库声明的cqueue.pxd。 ↩︎
在MemoryError案例中,创建一个新的异常实例以提示MemoryError实际可能失败,因为我们内存不足。幸运的是,CPython提供了一个C-API函数PyErr_NoMemory()为我们安全的抛出正确异常。如果你正用一个旧版本,你需要从cpython.exc标准库cimport这个C-API函数并直接调用。 ↩︎