SWIG 使用经验
啦啦啦,先定个基调:2 青年欢乐多~
1.入正题,先讲一下为何要采用SWIG或者类似的工具来连接C++和python
C++的神奇之处就不用在此赘述了,只谈天使的另一面——魔鬼面孔。C++语法严谨,一不小心容易造成整个软件崩溃甚至系统崩溃(你可以姑且认为是“指针”这个小情人惹的祸),内存泄露有时候几乎无法避免。脚本语言(比如Python)解释执行,执行效率低,但是开发效率(完成同一个任务所花费的写代码的时间)完胜C++,语法也很不“规整”,容易编写,而且不必直接操作指针,安全。其实以上都可以看作是借口,因为采用C++和Python混合的方式进行编写究竟是比单一用C++开发的效率高还是低要取决于应用的特点,当我们费劲心机把C+模块都封装成python模块了,可能花费在接口编写以及数据格式转换上的时间要远比用C++独自开发花费的时间长。但是,如果老王都不夸自己的瓜好,怎能让他人信服?!因此,堂而皇之地推出了解决方案:Python处理项目中易变、非核心的部分,核心、非易变的部分封装成c++模块(比如静态库。。),从而达到开发效率、运行效率权衡意义上的最优。
2.为何选用SWIG
先说主流技术:CAPI,BOOST,CTYPES,SWIG。。。对前三个的非官方介绍如下:
CAPI:基于扩展和嵌入( Extending &Embedding)机制,python自身提供,比较完整。但需要大量的工作来封装接口、转换数据类型。
CTYPES: 是Python标准库提供的调用动态链接库的模块,使用这个模块可以直接在Python里加载动态链接库,调用其中的函数。对C的支持很好,但C++的改名机制使得python调用C++编译的动态链接库很麻烦。
BOOST: Boost.Python是Boost提供的一个C++的模板库,用以支持Python和C++的无缝互操作。相对SWIG来说,这个库的优势是功能通过 C++ API完成,不用学习写新的接口文件。对C++的支持更自然、完整。文档差,让人晕头转向;据称有外部环境依赖。
关于SWIG:
原理(Python):基于 Python 的扩展机制,根据用户编写的接口文件,自动生成包含各种C-API的 **_wrap.cxx 文件,再与相关c/c++代码或库文件编译成可以被 Python 调用的动态库扩展模块
同上,这些优点也只是借口而已,情人眼里出西施,不管它长得如何,叫神马名字,只要用起来顺手、能解决问题就OK了。
3.SWIG的一般工作流程
编写c/c++程序,编写接口文件(*.i),准备相应的库文件;
用SWIG读取编写描述文件 (*.i) ,生成接口文件 (*_wrap.cxx) ;
将接口文件、需要调用的代码编译为目标文件(.o) ;
将接口文件的目标文件和原代码段的目标文件(还有静态库文件,如果有的话)一起编译成动态库 ;
在python中调用相应的前面生成的python模块文件,此模块执行时会调用前面生成的动态库文件(.so)
4.测试SWIG方案(理论上的)
(1)入门必备:Hello Tristan
(2)测试简单STL:map
(3)测试常用容器:vector<int>
(4)测试vector<string>
(5)测试嵌套vector<vector<int> >
(6)测试结构体:struct
(7)测试结构体vector:vector<struct>
(8)测试结构体嵌套vector: vector<vector<struct> >
(9)测试void*作为输入参数
(10)测试void*作为返回值
(11)测试.a静态库文件
(12)测试正式组件
5.具体测试过程
实际测试中,vector<struct>不好使,于是将vector<struct>中的数据取出来放到了vector<vector<vector<string> > >里面;为了防止void*惹不必要的麻烦,于是将handler(void *类型的)设置成了全局变量,也就避免了调用接口文件时要传入handler的值。
这里主要讲一下测试用python调用正在项目中实际应用的C++组件(3个静态库文件,一个组内自己编写的组件,两个开源组件)的过程。
我手中拥有的文件包括libIndi.a(姑且叫这个名字吧)静态库,libhtmlcxx.a以及libpcreapi.a静态库,还有IndiAPI.h头文件(描述libIndi.a库中有哪些接口参数,以及相关的类、结构体声明)
先自己动手写一个wrapper文件IndiTest.cpp,主要用来实现对接口的改装(vector<struct>改为相应的vector<vector<vector<string> > >,去除void*类型的参数)以及返回值的修改。
然后编写SWIG接口文件test.i。将用到的stl类型对应的库文件包含进去(std_string.i std_vector.i),包含相应的头文件、接口函数(主要是之前的wrapper文件中的函数)。
按照我先前的博文中所提到的方式对接口文件test.i进行编译,然后g++编译生成相应的.o文件。所谓光阴似箭,真的一点也不错,因为才一转眼就说到了问题的重点——编译动态库。动态库是一个很神奇的东东,尤其是当生成它的原始文件中包含了几个实际应用中的静态库的时候。
g++ -shared IndiTest.o test_wrap.o libIndi.a libHtmlcxx.a libpcreapi.a -o _Indi1_0.so
Indi1_0就是之前test.i文件中定义的模块名字,此时会产生最重要的两个文件:Indi1_0.py和_Indi1_o.so!这俩东西就是我们奋斗了这么长时间的结晶。
莫慌,只是这样的话,用户用起来会相当的麻烦,所以我们有必要写一个python接口模块(姑且叫做PyIndiAPI.py),导入Indi1_0模块,专门供用户调用,用户调用哪个接口,我们就在相应的接口函数里面调用之前生成的Indi1_0.py里面的相应接口函数。而用户既然用python来调用,当然希望我们返回的结果都是正宗的python认可的数据类型,所以需要我们在PyIndiAPI.py的接口函数里面对从Indi1_0.py中函数返回的数据进行转换,比如我们可以将vector<string>,转换成list。
这样,用户就只需将Indi1_0.py,PyIndiAPI.py以及_Indi1_o.so文件放到当前目录下,导入PyIndiAPI.py,就可以用python自由的调用相关的接口函数了,从而实现了python与C++之间的相互调用。
6.一些错误改正以及注意事项
首先是接口文件(test.i)中的头文件包含顺序。先把stl中相关的库文件(std_vector.i等)包含进去,然后再去%include“某某.h”
然后是缺省构造函数。比如test类中的public test(){},如果没有必要就别写了,我写上了就报错了,还未查明是何原因,仅是非主流建议。
还有生成动态库时,如果原始静态库文件之间有相应的调用关系,一定要按照相应顺序(具体顺序是啥,自己搞清楚),不然会提示找不到相应的资源。
如果用到%template()时(比如
%template(StringVecVec) std::vector<std::vector<std::string> >; ) ,尤其是嵌套vector,如果在python中直接取元素值(比如strvecvec[0])将会得到一个python默认的元组类型(tuple)的值,将无法再使用string的相关函数(size()等),得先用相应的template(比如Test.StringVec(str))进行类型的强制转化。
在导入相关python模块时,尽量不要使用from Test import *的形式,这样很容易造成命名的冲突,可以使用import Test的形式,如果模块名字太长,可以简化,比如import TristanIsSoCute as Cute。
7.对自己的一点忠告
行百里者半九十。会做和做好之间差距甚大。希望大家有则改之,无则加勉