目录
用Cython也用了很有一段时间了,这次就介绍一下它的最重要功能——使用Cython来封装C/C++代码。最基本的封装方法可以参见Cython文档中的相关页面:Interfacing with External C Code和Using C++ in Cython,本文介绍主要是比较重要和常用的Cython/C++交互特性,而自定义Python拓展类(而不是封装现有C++)的一些操作可以参考官方教程。
封装C++代码时,最重要的关键词就是extern
,在定义函数时使用这个关键字就说明该声明是外部的,而使用cdef extern from
语句就能指定声明对应的头文件。例如如果要封装函数func
,对应的Cython语句是
|
|
文件结构
首先讲一下Cython的文件结构。如果你之有一个小模块需要封装的话你可以把所有代码写到同一个pyx
里进行编译,否则的话你就可以利用Cython的目录结构来管理多个层次的代码。Cython的文件一共有三种:pyx
,pxi
(注意与pyi
区分)和pxd
(注意与pyd
区分)。
.pyx
是Cython的源文件,类似于.cpp
文件在C++中的地位,而对应.h
头文件地位的则是pyi
。在Cython中添加import 'header.pyi'
的语句就会将header.pyi
文件中的内容原封不动地直接插入当前位置,这与C++的#include
语句的作用是相同的。而pxd
则是另一套符号化的逻辑,.pxd
文件中只能声明函数、声明类型、不能有函数和类型的定义内容(除了inline
函数外),而在cimport
了pxd
的定义之后当前代码便引入了对应的函数或者类型签名。这个工作方式则更符合C++中头文件的实际用途。定义了pxd
后就可以在多个Cython文件之间共享同一个类型了。
不过既然涉及了include
语法,就必然要指定类似于C++的引用路径了。pxi
和pxd
文件的引用路径可以在cythonize过程中手动指定,而pxd
由于是符号化的还可以通过新建__init__.pxd
的方式来实现类似于Python的引用方法。只要在Cython搜索目录下的文件夹中包含__init__.pxd
文件,Cython就会认为这是一个Cython库,之后就可以用cimport
语句通过与Python中import
相类似的语法将对应模块文件(.pxd
文件)引用进来。当然,pxd
文件也可以通过命令参数直接导入。关于如何组织这些文件以及头文件之间的关系,读者可以参考我写的PCL封装库和Cython的相关文档。
函数在pxd中的定义不能显式指定默认参数,而是必须用
*
代替,例如cdef void func(a=0)
在pxd
中声明的话需要改为cdef void func(a=*)
。
类型封装
Cython对C++的类型提供了基本可用的封装语法。为什么说基本可用,是因为Cython目前对模板的支持还非常有限,因此实际上可以说Cython只支持到C++98的程度。不过尽管如此,Cython已经能够完成大多数代码的封装需求了。Cython对class
的支持通过cdef cppclass <class-name>
来实现,这里cppclass
关键词是为了和Cython的class
关键词进行区分。Cython中class
关键词代表的是和Python一致的PyObject
对象,代表的是Python类型,而cppclass
则指代C++原生类型,由于Cython文件中无法直接编写C++代码,因此cdef cppclass
语句通常在cdef extern from
的语法块中,用来封装现有的C++类型。另外一点需要注意的地方是Cython提供封装enum
和struct
的语法,但是针对的是C中的enum
和struct
,而非C++中的enum class
和struct
(C++中struct
和class
几乎没有区别)。如果要封装C++版本的enum
和struct
可以直接使用cppclass
关键词。以下是封装C++类型的一个例子:
|
|
别名与 namespace 关键字
由于Cython最后生成的是全局的C代码,因此在引用C++类时需要明确声明类型含命名空间的全称,这里就需要用到别名的机制。Cython允许从.h
文件中导入声明的时候给类型和方法改名字,具体用法如下
|
|
简而言之就是在方法或者类型名称后添加引号,引号里写上原本C++中的名字。这个机制有很多tricky的用法,它可以用来声明带命名空间的方法和类型、可以用来重命名C++中的运算符、可以用来直接声明实例化的模板类型、甚至可以用来把C++常量声明成类型用于模板参数(这种操作可以参考eigency库中的代码)。
其中针对第一种用法,为了简化带有命名空间对象的声明,Cython加入了namespace
关键字。在cdef
语句中添加namespace
从句可以使得Cython编译器默认给其包含的语句块中所有的类型加上对应的命名空间,例如
|
|
与以下代码是等价的
|
|
模板支持
这个特性在之前介绍Cython类型的文章中也有提到过,这里补充一下它的一些特性。Cython对C++模板的支持通过[]
符号实现,以下是Cython中对vector
的封装代码可供参考
|
|
其中vector[T,ALLOCATOR=*]
对应的就是C++中的vector<T, ALLOCATOR>
符号。模板参数在Cython中同样可以有复数个,也可以有默认值,似乎现在也支持常数作为模板参数,不过我没有尝试过,而据说老版本是不支持常数模板参数的。
之前有提到Cython中对模板的支持是阉割过的,主要特征有以下几点:
- Cython不支持模板参数的类型声明访问。例如上面的
vector
类型声明中不能使用ctypedef allocator_type.size_type size_type
这样的语法,而这样的类型推断在C++中是有很多的。 - Cython不支持模板构造函数中包含新的模板参数 不过Cython一直在改进对模板的支持,因此以后也很有可能会得到改进。
Buffer协议
Cython还针对性地支持了Python的Buffer协议,用来传递一块结构化的内存,这个协议的标准被记录在了提案PEP-3118中。这个协议通过__getbuffer__
和__releasebuffer__
两个Cython自定义的特殊函数实现,通过这个方式Cython代码就可以将C++内存转化为Python识别的内存。因为Numpy支持将支持Buffer协议的对象转换为ndarray,因此这个Buffer协议的通常用法是将一个C++对象变成Numpy的矩阵。具体的使用案例也可以参照我的pcl封装库中的对应代码。
本文介绍了Cython中操作C/C++对象的方法,不过仅仅介绍了一些进阶用法。如果是新手的话还是先参照之前提到两篇文档学习基本的函数、类型封装方法吧~