python在测试和自动化中应用逐渐广泛和深入,与此同时先前用c/c++开发的大量实用工具并存.为了通过c/c++程序在诸如性能和可操作范围等方面的优势提高python的应用支撑功能,同时又节省重复开发的成本,将已有的c/c++二进制工具及公用库融入python类库是一件比较有意义的工作.
有关python的c/c++扩展一些技术论坛文章中已经先后出现了不少可参考文字,多种扩展手段中综合而言使用swig -python是一种比较高效的方式,但swig对c的指针类型尤其是多重指针,c++的面向对象诸多特性支持并不完善,需要我们自己编写额外的代码来弥补这些尚在进行中的feature,包括继承派生,(常)引用参数传递及返回,函数重载(运算符重载也是其一种)等等.
swig及python官方文档中不易找到个别生僻疑难问题的解答,借助搜索引擎通常也不太好得到相关性极高的对策文章,因此将目前作过的两项用swig进行c++类库python扩展工作中遇到的一些疑难问题和试验得出的解决方法进行了初步整理汇总.为在后面的借助现有c/c++库及工具的直接python扩展来极大丰富基础类库的工作中提供参考.
疑难问题与对策
Tips 1:
swig能自然向python转化的基本数据类型有int, char, long, double等最基本的c类型,其中对于GNU C通常还支持的int32, uint64等与符号性,长度相关的类型是不支持的,当c/c++函数中出现这些类型时,不会被自动转化,从而在python中调用时出现类型不匹配的错误.这种情况下,需要写typemaps,尽管看上去是一些trivial的代码.示例如:
%typemaps (in) int32_t, uint32_t, int16_t, uint16_t
{
$1 = PyInt_AsInt($input);
}
%typemaps (out) int64_t, uint64_t
{
$result = PyLong_FromLong($input);
}
小结:通过输入类型的typemaps映射转换,可以让swig生成的python模块得以兼容一些非基础数据类型。
tips 2:
由于python语言本身只有一种integer object,无法在python语言层面区分次级的integer types:int, long, long long, unsigned long, unsigned int……等等.所以如果在C++设计了通过这些次级的类型区分实现的重载.SWIG是无法正确转化到python去的, 需要想办法避免这种设计.
如C++中有如下重载:
void convert_digit(unsigned int a);
void convert_digit(unsigned long a);
void convert_digit(long long a);
那么通过SWIG作python扩展转化后,python中无法正确调用任何一个版本!解决方法就是每个函数分别取三个不同的函数名.
小结:通过接口扩充,让不同接口处理不同的次级参数类型,可弥补swig对重载特性扩展支持的不足。
tips 3:
在c++中编写了有继承依赖关系的类库,用SWIG转化扩展至Python中并作为独立的类来引用:
1. 对当前类的直接父类的扩展模块需要包含(include)而不仅仅是导入(import), 导入只能满足编译通过,当在python中进行子类实例化时,就会发生与父类相关的underfined reference错误. 如在c++中B.h中声明类为:
#include “A.h”
class B : public A
{
…..
};
并且A类的扩展模块描述文件为A.i, 对应的目标文件已经编译成A.po 则B类的扩展模块描述B.i中至少要包含如下语句:
%include “A.i”
同时在链接生成_B.so时,A.po需要联编在内,否则仍然无法在python中实例化B类.
当然,如果在B类的实现中引用了C类(而不是直接继承依赖于C类),而在python中使用B类时又不需要直接去引用C类的数据或成员,那么在B.i中只需要引入C类对应的模块扩展即可,也就是 %import “C.i” 不过,正如你所想到的,生成_B.so时联编C类的模块扩展之目标代码C.po还是不能缺少的,否则B类无法单独在python中引用,即使你写了 import C import B 当B类的某个需要引用C类代码的函数被调用时,一样出现末定义符号错误,这个道理与C/C++本质一样.
2. SWIG对c++的Inheritance特性扩展至python做得还是有一些欠缺,需要注意,其中一点便是成员访问控制上末能完整实现c++的特性.还是用实例说明比较明白~
如果在C++中声明RWBase类如下:
class RWBase {
protected:
void rw_seek(size_t offset);
…
};
同时有Reader类从它派生,如下:
class Reader : public RWBase {
……
}
然后在python中从C++类进行派生, 如下:
import RWBase
import Reader
class PyReader(Reader.Reader):
def seek_for_pyreader(self, offset):
…
self.rw_seek(offset)
….
那么python解释器在遇到self.rw_seek调用时抛出AttributeError说这个函数找不到.
简言之,SWIG没有做到对C++类派生中access control的传递关系的完整扩展转化.在上面C++设计中, 其实原意是RWBase只是一个接口原型抽象的基类,它本身实例化没有意义,因此C++中阻止它被实例化(因为连构造函数也不在public域). 但为了方便地用SWIG扩展至python中进行应用,只能让这种设计变得糟糕一点了,即把RWBase的rw_seek暴露于public域.
不过,说回来,SWIG能够让你轻松地复用C++类设计,可以自由从C++类派生出Python类,已经是很感谢它了,否则直接用Python C/C++ Extending API来写必定相当繁琐。
小结:1. Swig扩展一个模块时需要完整的目标代码,编译依赖不可忽视。
2.swig尚未能精确地实现access control这一面向对象特性的完整扩展,但可以适放松源类中的相关设计来折衷。
tips 4:
如果使用boost python, 那么与STL相关的typemap,引用类型的特殊处理等或许都可以轻松搞定.不可否认,boost python对于c++向python扩展支持得更加完整和到位.所以下面的经验适用于由于某种原因仍然选择用SWIG的方式扩展的场合,比如说你不愿意总是把一个额外的boost_python.so带在你的扩展模块一起显得比较臃肿 。况且,boostpython也不能解决所有个例需求。
如果希望用现有版本SWIG将引用了STL的C++代码顺利地扩展到python,有两个问题值得注意:
1. STL中定义的类型不能自动地像其它C++类型一样被顺利扩展,而是被当作一种新的类型而在python层应用时给出无法识别类型的错误.
此时,如前面所说的处理uint32_t, int64_t这类swig末内建支持的方法一样,需要为这些stl定义的类型编写需要的typemaps
至少在swig1.3版本中,已经连同安装包一起提供了一些现成的typemaps,涵盖std::string/vector/list/map/pair/deque/complex这些常用容器和utility的大部分typemap需求.只需要在模块的.i文件中包含这些已经定义好的typemaps就可以了.
列出swig1.3中提供的现成stl类型的typemaps定义文件如下:
/usr/lib64/swig1.3/python/std_common.i
/usr/lib64/swig1.3/python/std_complex.i
/usr/lib64/swig1.3/python/std_deque.i
/usr/lib64/swig1.3/python/std_list.i
/usr/lib64/swig1.3/python/std_map.i
/usr/lib64/swig1.3/python/std_pair.i
/usr/lib64/swig1.3/python/std_string.i
/usr/lib64/swig1.3/python/std_vector.i
如在自己扩展的引用了std::string的C++模块cpp_common的python扩展描述文件cpp_common.i中加入:
%module cpp_common
%include “std_string.i”
using namespace std;
就可以解决c++代码中参数类型和返回类型为string和const string&的函数向python扩展时的typemap问题了,这时在python中调用这些函数时,对应于string类型或const string&类型的形参,只需要传入python字符串即可,而对应于这两种类型的返回值, 也会在python中返回为python的string类型.
原因很简单,就是因为string.i中已经为这两种类型作为入参和返回值的情况编写好了对应的typemaps:
namespace std {
%typemap(in) string {
if (PyString_Check($input))
$1 = std::string(PyString_AsString($input),
PyString_Size($input));
else
SWIG_exception(SWIG_TypeError, “string expected”);
}
%typemap(in) const string & (std::string temp) {
if (PyString_Check($input)) {
temp = std::string(PyString_AsString($input),
PyString_Size($input));
$1 = &temp;
} else {
SWIG_exception(SWIG_TypeError, “string expected”);
}
}
%typemap(out) string {
$result = PyString_FromStringAndSize($1.data(),$1.size());
}
%typemap(out) const string & {
$result = PyString_FromStringAndSize($1->data(),$1->size());
}
2. 对于string.i中没有包含的typemaps需求,需要自己来补充!
如尽管你包含了string.i,但对于以下c++函数:
int make_a_random_string(string& target, int len_floor, int len_ceiling);
扩展后在python中调用时,你就不知道怎么向第一个参数传递值了, 现有的string.i中并没有为这种需求编写typemaps.这时就需要自己动手了.
首先要解决输入的问题,因为在python层代码中构造出一个string的对象,然后转递指针给这个函数,是比较麻烦的事情(还得把string的实现代码扩展成python模块, 然后还要用类如pointer_class这样的映射生成一个string pointer类型….):
%typemap(in, numinputs=0) std::string& (std::string temp)
{
$1 = &temp;
}
这样python中调用时第一个参数可以忽略,即当作没有这个参数.
然后再解决如何把这个c++函数调用后得到的结果返回至python代码中:
%typemap(argout) std::string&
{
std::string* output = static_cast($1);
Py_DECREF($result);
$result = PyString_FromString(output->c_str());
}
这样问题就得到了解决.不过也由此了一个问题:把原本通过引用传出的结果转换为返回值的方式来得到,那原来这个c++函数本身的返回值岂不是就这样丢失了,python代码中确实想两者兼得,怎么办?
其实也是很简单的事情,用python的tuple, dict, list任一各聚合类型,都可以轻松解决这个返回多个值的问题. 用tuple的方式示例如下:
%typemap(argout) std::string & {
std::string* output = static_cast($1);
Py_DECREF($result);
$result = PyTuple_Pack(2, $result, PyString_FromString(output->c_str()));
}
注意:上面typemap代码书写时都带上了string所在命名空间std,当然也可以不带,但那样就得把上面的typemap像1中一样放到std这个命名空间内.否则,这些typemap代码将不会生效!!
小结:1. 扩展STL代码,要充分复用swig自带的typemaps;个例情况则可以自己来扩展typemaps,归结到底,用typemaps显式地告诉迷惘的swig到底怎么办。
2. 如果为了“骗过”swig忽略某个Value-Result(即in-out型的)参数,可以借助python通过list/tuple甚至dict等容器类型来解决这个value-result上希望返回数据的问题。
tips 5:
参照tips 4中的方法,可以解决stl类型的python扩展与应用问题,尤其第2点中提供的typemap, 也解决了c++引用类型入参在python代码中传递实参的问题.
可以看出make_a_random_string这个函数扩展的方法是,在python代码对其调用过程中忽略第一个入参,同时把函数执行后通过这个引用传出的结果间接转移到返回值中,从而在python代码中得到其结果.
但此时可能又有类似这样的函数也同时需要扩展:
const string& produce_string_by_regex(const string& regex, …..);
这个函数通过传入的正则式产生一个匹配的字符串返回.问题出现了:SWIG发现这这里需要传递一个const string&的入参,查找已有的typemap后发现有一个与之匹配度最高的typemap,即:
%typemap(in,numinputs=0) std::string& (std::string temp)
{
$1 = &temp;
}
于是应用之.很显然,这样就把入参给忽略了,对应的c++函数调用时就缺少这个必要的值了,结果肯定不对(默认为空串),因些需要写一个严格类型对应的typemap,如下:
%typemap(in) const std::string& (std::string temp)
{
temp = PyString_AsString($input);
$1 = &temp;
}
但是,还有问题! 由于const string&中包含有string&, SWIG会自作聪明地把下面的typemap也应用到扩展中去:
%typemap(argout) std::string & {
std::string* output = static_cast($1);
Py_DECREF($result);
$result = PyTuple_Pack(2, $result, PyString_FromString(output->c_str()));
}
这样的后果是python中调用返回的返回结果成了传入的第一个参数值,而不是C++函数的实际返回结果,从而函数调用相当于什么都没做!
解决的方法再写一个严格匹配的argout型typemap,以避免错误地应用非预期的其它typemaps:
%typemap(argout) const std::string &
{
}
这样就两者兼顾了!这个typemap与前面string&的argout型typemap的顺序是不重要的,SWIG只要发现有最精确匹配的typemap就会优先应用它.
小结:当有多个类似的,尤其同时存在互相存在“is-a”关系的多个typemaps时,swig可能又晕了。方法就是让这些“貌似”多个转移更加显著地区分开来。
tips 6:
c/c++代码中的全局常量和变量SWIG也会帮你扩展到python模块中去,包装于cvar这个对象当中,你可以通过它来访问c/c++代码中定义的全局常量和变量.
不过,是全局变量和常量,不包括宏定义的值.
对于宏定义的值,直接扩展成了模块的属性,因此直接访问即可.
小结:如果有全局数据(聚合类型也包含)需要扩展到python中,swig能胜任-用cvar代理。
tips 7:
如果确实需要在python中以指针方式来应用从c/c++扩展过来的函数, SWIG也提供了一定的支持.
如有c++函数定义如下:
#include “CSystem.h”
class CFile
{
…
bool associate_with_sys(CSystem* psys);
…
};
为了在python层传入一个CSystem的对象指针, 可以在CFile类的扩展模块描述中加入:
%include “cpointer.i”
%pointer_class(CSystem, ptrCSystem);
然后在python代码中就可以生成CSystem*这一类型的对象来调用上面的函数, 以下是应用扩展模拟的python示例代码:
ptrobj_csys = ptrCSystem() // 构造一个CSystem*对象
sys_obj = CSystem()
ptrobj_csys.assign(sys_obj) // 指定指针指向的对象
pp_sys_obj = ptrCSystem_frompointer(ptrobj_csys) // 可以得到二重指针对象,其类型为CSystem**
another_sys_obj = ptrobj_csys.value() //返回指针所指向的对象
通过以上稍显别扭的方式, 基本模拟了声明, 赋值,取值,取地址得到二重指针等等常用操作, 可以满足多数的指针应用场合之需求.
注: cpointer.i中实际是定义了一个pointer类型相关转化的typemap, 因此需要包含它.
小结: 尽量回避一定要在python层应用指针的源类设计与实现。但如果实在无法回避,swig也还是有一招能帮你 – pointer_class。
转载请注明:爱开源 » Swig之cpp完整python扩展疑难对策