现在游戏业务的cache基本都是接入tcaplus,以前业务自己拥有cache的时候我们可以很方便去通过脚本去访问玩家的数据,所以最近想能不能对tcaplus生成Python的访问接口,由于tcaplus只提供了C++ API屏蔽了底层的协议数据细节,不太好直接写Python访问接口,偶然间接触到了SWIG,差不多花了一周时间,通过官方手册完成了SWIG对Tcaplus Python 接口的封装。
这一篇文章主要是介绍在看手册过程中遇到的一些问题和思考总结, 并没有涵盖所有的点,因为SWIG3.0的手册有653页,还是英文的,看手册主要还是为了能够封装Tcaplus Python访问接口,所以只是SWIG一部分比较重要的知识点,下面就简单介绍一下SWIG的一些基础概念和对API进行Python封装的实现原理,欢迎批评指正。
1. SWIG的产生
SWIG(Simplified Wrapper and Interface Generator),一个包装和接口生成器工具,可以为C/C++程序构建生成各种脚本语言的调用接口,这样就可以通过脚本语言来直接调用C/C++编写的程序。最初在1995年由一群实验室的大佬为了解决在大量模拟数据下,经常变换不同的脚本语言来解决问题的需要。 可以说:SWIG存在的目的就是为了简化C/C++和其他脚本语言的交互。
SWIG的产生
为什么要用脚本语言去调用C/C++库呢,应为C/C++有以下众所周知的优点:
- 高性能
- 操作系统级的编程
- 庞大的用户群体和社区
当然C/C++作为静态类型的语言也有如下的一些不友好:
- 写UI很痛苦:MFC, X11,GTK
- 对于修改逻辑需要重新编译,这是静态语言很不友好的地方
- 模块化很麻烦
- 不安全:空指针,内存溢出,泄漏等
为了解决这些限制,许多程序员得出的结论是:用不同的语言来做不同的事情。例如通过Python或者Tcl来编写图形化界面,Java更容易编写分布式计算的软件。不同的编程语言有不同的长处和弱点,任何编程语言都不可能是完美的。因此,通过组合语言您可以共同利用每种语言的最佳特性,并大大简化软件开发的过程。
2. SWIG脚本语言如何和C进行交互
首先声明本文中只以Python语言来阐述SWIG的使用。
理解脚本语言如何和C/C++交互,首先简单说一下Python的标准实现CPython,Python标准的解析器实现是由C编写的,基础功能模块也都是C编写的,然后将其编译成了python解析器和相关so, 所以对于CPython来说,其本身解析过程最终都是通过执行底层C代码来进行实现的。
官方标准CPython提供了对应的API允许对Python进行扩展,CPython扩展需要在C/C++代码中嵌入很多<Python.h>中的API,为了能够调用C/C++的函数,需要声明如何调用函数,参数的类型转换等等,很麻烦。最终将C/C++代码编译成so,在Python中进行加载和使用。
那么SWIG的目的就是要为C/C++ API提供脚本语言的接口,SWIG所有做的就是解决脚本语言和C/C++交互的问题,SWIG所做的事情其实就是两件事:
- 根据要调用的C API生成Wrapper函数,作为胶水来让脚本解析器和底层C函数进行交互.
- 为生成的Wrapper函数生成脚本语言的调用接口。
这样完成了对C/C++函数脚本语言接口的生成,通过直接使用脚本语言的接口,会调用对应的Wrapper函数,Wrapper函数会将脚本语言传入的参数,转换成C的参数,然后调用对应的C的接口,执行完后,Wrapper函数会将C返回的结果,转换成脚本语言的数据类型返回给脚本上层。
为了说过SWIG是如何做的,这里举一个SWIG手册上的一个例子:
example.c中有一个C写的fact函数,我们希望在Python中进行调用:
1 | // example.c |
SWIG需要一个类似IDL的接口文件,来描述需要Wrapper的C函数fact(),如下:具体语法后面会介绍:
1 | // example.i |
然后执行以下命令就会最终生成_example.so(只能生成动态库哦,如果生成静态库要重编python),用来在Python中引入:
1 | swig -python example.i |
swig –python example.i命令会生成两个文件:example_wrap.c和example.py
可以看到example_wrap.c中对于fact()函数生成的wrapper函数如下:
1 | SWIGINTERN PyObject *_wrap_fact(PyObject *SWIGUNUSEDPARM(self), PyObject *args) { |
生成对应的example.py中对于fact封装的脚本接口如下:
1 | // example.py |
Python中执行如下:
1 | import example |
看了这些应该对SWIG的工作原理了解了吧,上述wrapper的代码其实了解Python扩展的人就知道,就是调用Python扩展提供的API来实现的,在Python/C API的官方手册中有介绍用于编写Python扩展的很多接口,其实编写Python扩展不仅仅是上面我截取的wrapper函数,还有其他很多要注意的,所以对于想要为C/C++库直接通过这些API进行Python扩展编写还是很耗费时间和经历的,所以SWIG帮我们做了这些事情,还是很方便的。我们只需要为SWIG编写一个IDL文件,此外我们还要要做的可能就是为要使用的C/C++库再封装一层,这样就不会对整个要使用的API进行wrapper了,省去很多麻烦和学习成本。
3. SWIG基础
下面就是说明一下SWIG一些基础知识,里面基本都是SWIG官方手册中的内容,我梳理了一些重要的和我在封装tcaplus API遇到需要关心的进行了一些说明,希望能够对大家有所帮助:
3.1 SWIG输入
SWIG的使用在前面<SWIG脚本语言如何和C进行交互>中简单说明了,这里介绍一下SWIG 输入接口的一些语法。
SWIG的输入即接口文件通常是以”.i” 或者”.swg” 为后缀,类似于其他IDL。接口文件包含一下内容:
- wrapper中要使用的C/C++声明
- SWIG指令
- 要wrap的C/C++数据,函数,结构体/类
一些情况下,SWIG可以直接通过原生的header file或者source file(强烈不要使用源文件特别是C++源文件)来生成Wrapper代码,但更多的情况是需要结合SWIG指令的。
SWIG接口文件的通常格式如下:
1 | %module mymodule |
- %module指令后面表示生成脚本语言对应的模块名字
- %{}%指令之间的代码会直接拷贝到SWIG生成的wrapper源码文件中。这之间的代码一般就是头文件和一些声明,它们是生成的wrapper源码编译所必须的。
- 第三部分就是SWIG需要wrapper的C/C++函数、类的声明,SWIG会对这部分的C/C++函数、参数进行封装,最终将脚本调用的函数和参数转换成C函数和参数,然后调用对应的C函数(声明放在%{…%}部分)
SWIG接口文件中%开头的都是SWIG指令部分,由SWIG的进行解析。
SWIG的IDL语法是支持预处理的。和C语法基本都是类似的,包括:文件引入include/import,条件编译ifdef,宏展开等。
SWIG支持通过%include来引入文件到接口文件中,SWIG的%include就是C中的include所做的预处理,仅仅做内容的引入,但SWIG中的%include指令对同一个文件只会引入一次,不需要额外的include保护。
SWIG中在没有指令%{…%}包裹下直接使用#include声明是会被SWIG忽略的,除非在执行SWIG命令时加上-includeall选项,SWIG之所以对C/C++ 传统的#include进行忽略是因为:我们并不是希望SWIG去包装引入的头文件中的所有系统头文件和一些辅助文件。所以由此就可以知道%include引入的文件,会忽略其中的#include部分。
SWIG的%include指令的搜索路径顺序如下:
- 当前目录
- swig -I 选项设置的目录
- 当前目录下的./swig_lib目录
- swig -swiglib命令保存的目录,该命令执行结果是swig安装所在的目录:例如:/usr/local/share/swig/1.3.30
3.2 SWIG和C++
因为C++的复杂性,尤其到C++11你会发现你像学习一门新语言,所以对于SWIG来说不能够支持C++的所有特性,比较SWIG只是一个解析器,做不到GCC的全部功能。但SWIG版本一直也在升级,慢慢也会做到完善,但对于使用来说,基本没有问题,因为我们很多时候都会基于要使用库再进行一些封装,屏蔽一些无用的接口,这要也很大程度上可以减少SWIG的wrap复杂度,下面是<SWIG3.0 Manual 6.3>中贴出的支持的C++特性:
- Classes
- Constructors and destructors
- Virtual functions
- Public inheritance (including multiple inheritance)
- Static functions
- Function and method overloading
- Operator overloading for many standard operators
- References
- Templates (including specialization and member templates)
- Pointers to members
- Namespaces
- Default parameters
- Smart pointers
其中贴出来了目前不支持的C++特性:
- Overloaded versions of certain operators (new, delete, etc.)
当然还有一些其他特殊语法,SWIG也是不支持的,这里没有贴出来,<SWIG3.0 Manual 5.1.6 Parser Limitations>
3.3 SWIG对于C/C++参数类型的处理
C/C++中参数类型可以是值传递,指针,引用来区分,也可以按内置数据类型和自定义数据类型来区分,要想在脚本语言对这些类型种类进行一一区分,SWIG需要在生成的wrapper代码中和脚本语言接口中进行分装,以最终达到操作底层的C指针的方式。
3.3.1 对于整型和浮点类型的指针和引用参数的处理
下面看一个示例:我们希望调用一个add()的C函数,将x+y的结果通过result返回:
1 | void add(int x, int y, int *result); |
SWIG的<cpointer.i>库定义了pointer_functions的macro支持:希望(int, double)类型的指针(引用)做为函数参数,用来传入或则是传出结果使用。注意不支持char*哦
1 | // interface file |
我们可以看看pointer_functions的SWIG宏定义:
1 | //<cpointer.i> |
上述宏定义分别定义了intp在C中的实现,以及用于生成脚本访问接口和wrapper代码的声明。wrapper代码中又会对intp进行type*类型进行PyObject的封装:
1 | SWIGINTERN PyObject *_wrap_new_intp(PyObject *SWIGUNUSEDPARM(self), PyObject *args) { |
最终你会发现intp的所有操作都是在C层面进行实现的,每一步都需要在Python和C库之间进行交互:上述python执行过程如下:
- 当执行intobj = example.new_intp(),会在C层面new了一个int,返回给Python一个对象;
- 当在Python中调用example.add(3, 4, intobj)时,add的wrapper代码就会将传入的intobj转换成一个C层面的指针,这个指针在是在调用new_intp创建出来的,C层面的add函数会直接将结果保存在new出来的空间中(即intobj封装的);
- 执行完后通过example.intp_value(intobj)取出结果,intp_value会将C层面new出来的int中的数据返回到Python中;
- 最终执行example.delete_intp(intobj)来释放C层面new出来的int对象。
上面四个过程都是需要与C函数进行交互来实现,其实这里还有一个%pointer_class宏功能和pointer_function一样,不过pointer_class会将对应的类型封装成一个Python的类而不是很多函数接口:且它的好处就是不需要额外的调用delete接口来释放C层面new出来的对象:
1 | // 接口文件定义 |
我们可以看出pointer_class的接口根据简单清晰,符合面向对象的思想,且不需要额外手动的GC操作,所以建议使用pointer_class。
SWIG的<carrays.i>库定义的macros支持:希望函数参数是(int, double)数组的,不支持指针和其他复杂数据类型,也不支持char,和<cpointer.i>一样,有两个宏:
1 | %array_functions(type, name) # 定义了数组操作的一系列接口,不建议使用 |
我们知道C的数组参数同样是当做指针来处理的,<carrays.i>定义的宏的功能同样是将数组对象的创建,操作,释放通过调用C函数来进行实现,这里就不贴出wrapper中生成的代码了,基本和<cpointer.i>中的pointer_functions()和ponter_class()一样,只不过是new int 和new int[n]的差别,以及多了对数组操作的接口。下面是array_class的使用:
1 | // 接口文件 |
3.3.2 对于char指针参数的处理
对于char *参数的C接口,如果char*只是传入参数,表示一个字符串,那么完全不用担心,SWIG会自动封装好,在调用C接口时将Python的字符串转换成char*,但不允许在C中对该char*进行修改。对于Python来说,通过string来传入binary数据是能够透传到C接口中的char*的,不需要进行人工干预。如果想通过C接口中的char*透传binary数据到Python层面是需要进行干预的。
如果C接口中char*参数是希望能够进行写入数据的,就需要我们在IDL进行描述了。SWIG中的<cstring.i>库定义的macros用来处理:希望函数参数可以是char指针,以此写入字符串或者是二进制数据
1 | inline void get_name(char * _name) |
可以通过:
1 | // interface file |
这个宏的使用方式是依靠函数的参数名字进行替换,如果头文件中含有很多char *path的参数名,都会进行处理,生成的脚本语言中传出的name会通过返回值来实现,
1 | import example |
下面是get_name对应SWIG生成的wrapper代码的实现
1 | SWIGINTERN PyObject *_wrap_get_name(PyObject *SWIGUNUSEDPARM(self), PyObject *args) { |
如果想要通过char*透传出binary数据可以通过%cstring_chunk_output(parm, chunksize)宏,我们可以看一下两者生成的wrapper代码的差异,如下图:其实最终都是调用了Py扩展中的API,在处理char*的时候是按NULL来结束还是获取固定长度的串。
3.3.3 对于自定义类型参数的处理
对于前面两小节介绍了SWIG对于c内置数据类型指针,引用,数组的处理,对于内置数据类型,SWIG都会封装成一个对应指针类型PyObject参数, 当调用时,需要定义对应的wrapper 数据类型才可以。
但对于自定义的数据类型,SWIG对自定义数据类型进行了统一wrap, 能够让Python中的对象和C中的 Pointers,references, values, and arrays自动转换。如下:
1 | // C函数声明 |
3.4 SWIG对于C++的class生成的Proxy class分析
SWIG为了能够让C++的class和目标脚本语言之间很好的映射,SWIG为目标语言生成了一个基本一致的Proxy class,例如在Python中:
1 | // example.h |
生成的Python代码如下:
1 | class Bob(_object): |
是不是对使用者来说基本相当于调用C++类,其实通过之前的内容我们应该清楚了SWIG通过生成wrapper代码来调用对应的C功能代码,而脚本和wrapper代码的通信是直接通过Python约定的扩展API进行交互,我们可以看看生成的Bob类的wrapper代码的样子:
这里是_init_(self)中调用的_example.new_Bob()的代码,可以看出在wrapper代码中Python的Bob对象的构建最终会在C层面new一个对应的Bob对象。
1 | SWIGINTERN PyObject *_wrap_new_Bob(PyObject *SWIGUNUSEDPARM(self), PyObject *args) { |
Bob类中其他成员函数的wrapper代码基本都是一样的实现逻辑,这里就不贴出来了 。
3.4.1 tempaltes指令
C++中的template类和函数是一个相对C来说是很牛逼的特性,对于C++的template C++编译器会在编译期间根据调用代码实例化生成具体的类和函数代码。如果没有对于模版类型的调用,是不会生成template相关的实例化代码的。
所以要想SWIG对tempalte相关的的代码进行wrap,就需要告诉SWIG需要wrap 模版类或函数具体的实例化。
SWIG 1.3.7就支持了C++ template的包装,template的包装需要注意以下两点:
- 需要告知SWIG解析器template怎样实例化:vector<int> …etc
- template实例化的名字,例如vector<int>在很多脚本语言中不是合法的标识符,所以需要为Wrapper生成的proxy class定义一个更合理的实例化名,例如intvector等。
对于C++中的各种STL容器类,SWIG封装好了一个库,当要wrap的代码中有用到STL容器的时候引入这些库,就不需要对STL的头文件进行Wrapper了,那样会很麻烦,且生成很多额外且无用的wrapper代码.例如如使用到std::vector<int>,std::vector<double>, std::vector<std::string>就需要接口文件进行如下定义:
1 | // interface file |
下图是SWIG关于STL 基本容器的封装(<SWIG3.0 Manual 9.4 STL/C++ Library>)
下面是关于tempalte class的一个实例:
1 | // example.h |
生成的Python代码如下:
1 | class IntAlice(_object): |
3.4.2 struct中的数组成员
对于struct中或class的公有数组成员,这里单独拿出来说是因为和之前对于指针和数组类型的参数处理是类似的,需要我们在IDL中进行定义。
1 | // example.h |
通过%array_class定义一个intArray这样就可以set结构体中的数组成员了:
1 | // python |
但是你会发现无法在python中去get数组成员。因为你直接访问Bob对象的bob数组没有转换成intArray的接口,且bob是不可遍历的。那怎么办呢,如何访问结构体中的数组成员呢,这个时候就需要通过SWIG的typemap指令了(typemap具体含义和使用方式在下一节中阐述),接口文件修改如下:
1 | // example.i |
可以看到:wrapper代码中bob_get方法的对于类型转换的方法被重写如下:
这样就可以在Python中访问set之后的bob数据了:
1 | // python |
3.5 SWIG的Typemaps
typemap指令顾名思义是类型映射的意思,用来修改SWIG对脚本语言数据类型和C数据类型的转换行为。使用typemap的前提是需要对Python C API需要有一定的了解。其实在大多数情况下,SWIG的默认封装都是能够满足需求的,typemap是一个高度自定义化的功能扩展。
typemap指令实际是用来修改SWIG对特定C数据类型和脚本数据类型的默认转换规则。typemap语法如下:
1 | // typemap有method, typelist, code三部分组成 |
typemap的method主要有‘in’, ‘out’等,详见< SWIG3.0 11.5 Common typemap methods>一节。
1 | // in 方法用于修改Python的int转换成C int的方式 |
typemap会将匹配到的类型的默认wrapper中的Python和C关于该类型的转换逻辑都进行替换。前面**<struct中的数组成员>**中已经用typemap来处理int[]成员的get方法,这里再通过结构体中的字符串数组来阐述typemap的强大功能:
1 | struct Tom |
对于是否有一下typemap的定义,可以看到下图的差异:
1 | // interface file |
SWIG wrap的代码可以看到差异如下:
默认的情况下,是无法通过tom_name获取binary data的,因为有默认值通过strlen截断,加入要想通过tom_name返回binary data,就要修改其默认转换行为,是不是很厉害。
4. 参考
http://www.swig.org/Doc3.0
https://docs.python.org/2/extending/index.html
https://docs.python.org/2/c-api/index.html