Cython与C/C++的交互

用Cython也用了很有一段时间了,这次就介绍一下它的最重要功能——使用Cython来封装C/C++代码。最基本的封装方法可以参见Cython文档中的相关页面:Interfacing with External C CodeUsing C++ in Cython,本文介绍主要是比较重要和常用的Cython/C++交互特性,而自定义Python拓展类(而不是封装现有C++)的一些操作可以参考官方教程

封装C++代码时,最重要的关键词就是extern,在定义函数时使用这个关键字就说明该声明是外部的,而使用cdef extern from语句就能指定声明对应的头文件。例如如果要封装函数func,对应的Cython语句是

1
2
cdef extern from "func.c":
void func(int arg)

文件结构

首先讲一下Cython的文件结构。如果你之有一个小模块需要封装的话你可以把所有代码写到同一个pyx里进行编译,否则的话你就可以利用Cython的目录结构来管理多个层次的代码。Cython的文件一共有三种:pyxpxi注意与pyi区分)和pxd注意与pyd区分)。

.pyx是Cython的源文件,类似于.cpp文件在C++中的地位,而对应.h头文件地位的则是pyi。在Cython中添加import 'header.pyi'的语句就会将header.pyi文件中的内容原封不动地直接插入当前位置,这与C++的#include语句的作用是相同的。而pxd则是另一套符号化的逻辑,.pxd文件中只能声明函数、声明类型、不能有函数和类型的定义内容(除了inline函数外),而在cimportpxd的定义之后当前代码便引入了对应的函数或者类型签名。这个工作方式则更符合C++中头文件的实际用途。定义了pxd后就可以在多个Cython文件之间共享同一个类型了。

不过既然涉及了include语法,就必然要指定类似于C++的引用路径了。pxipxd文件的引用路径可以在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提供封装enumstruct的语法,但是针对的是C中的enumstruct,而非C++中的enum classstruct(C++中structclass几乎没有区别)。如果要封装C++版本的enumstruct可以直接使用cppclass关键词。以下是封装C++类型的一个例子:

1
2
3
cdef extern from "test.h":
cdef cppclass Test:
void print()

别名与 namespace 关键字

由于Cython最后生成的是全局的C代码,因此在引用C++类时需要明确声明类型含命名空间的全称,这里就需要用到别名的机制。Cython允许从.h文件中导入声明的时候给类型和方法改名字,具体用法如下

1
2
3
4
5
cdef extern from "<header-name>":
cdef void <new-function-name> "<origin-function-name>":
pass
cdef cppclass <new-class-name> "<origin-class-name>":
pass

简而言之就是在方法或者类型名称后添加引号,引号里写上原本C++中的名字。这个机制有很多tricky的用法,它可以用来声明带命名空间的方法和类型、可以用来重命名C++中的运算符、可以用来直接声明实例化的模板类型、甚至可以用来把C++常量声明成类型用于模板参数(这种操作可以参考eigency库中的代码)。

其中针对第一种用法,为了简化带有命名空间对象的声明,Cython加入了namespace关键字。在cdef语句中添加namespace从句可以使得Cython编译器默认给其包含的语句块中所有的类型加上对应的命名空间,例如

1
2
3
cdef extern from "test.h" namespace "ns":
cdef cppclass Test:
pass

与以下代码是等价的

1
2
3
cdef extern from "test.h":
cdef cppclass Test "ns::Test":
pass

模板支持

这个特性在之前介绍Cython类型的文章中也有提到过,这里补充一下它的一些特性。Cython对C++模板的支持通过[]符号实现,以下是Cython中对vector的封装代码可供参考

1
2
3
4
5
cdef extern from "<vector>" namespace "std" nogil:
cdef cppclass vector[T,ALLOCATOR=*]:
ctypedef T value_type
ctypedef ALLOCATOR allocator_type
...

其中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++对象的方法,不过仅仅介绍了一些进阶用法。如果是新手的话还是先参照之前提到两篇文档学习基本的函数、类型封装方法吧~

Shoot me some coffee money XD
0%