使用SWIG Python动态绑定C++对象
SWIG(Simplified Wrapper and Interface Generator)是一个为C/C++库提供脚本调用支持的工具,支持Lua, Perl, Python, Go等多种脚本语言。如果不了解可以参考Interfacing C/C++ and Python with SWIG。本文主要关注在SWIG Python中如何实现绑定已有C++实例,想象一下,调试时如果可以不用重新编译C++程序,使用脚本动态调用C++,该是有多方便。SWIG文档 31.3.5 Pointers小节中明确提到
However, the inverse operation is not possible, i.e., you can't build a Swig pointer object from a raw integer value
官方是不提供支持的,但是仔细看一下SWIG代码会发现并不难实现。
首先,我们来看一下SWIG是如何对C++类进行封装的,定义一个简单的测试类:
#ifndef FOO_H__ #define FOO_H__ class Foo { public: Foo(); int GetNum(); void SetNum(int num); private: int m_num; }; #endif // FOO_H__
Foo.cpp
#include "Foo.h" Foo::Foo() : m_num(-1) { } int Foo::GetNum() { return m_num; } void Foo::SetNum( int num ) { m_num = num; }
接口文件:
%module foo %{ #include "Foo.h" %} class Foo { public: Foo(); int GetNum(); void SetNum(int num); private: int m_num; };
调用命令:swig -c++ –python Foo.i生成wrapper文件,然后使用distutils编译成_foo.so:
from distutils.core import setup, Extension setup(name = "foo", version = "1.0", ext_modules = [Extension("_foo", ["Foo_wrap.cxx", "Foo.cpp"], extra_compile_args = ['-g'])])
测试一下:
>>> from foo import * >>> f = Foo() >>> f <foo.Foo; proxy of <Swig Object of type 'Foo *' at 0x56db40> > >>> f.SetNum(17) >>> f.GetNum() 17
Ok, 工作正常。SWIG为Foo生成了两个封装文件foo.py和Foo_wrap.cxx,Foo_wrap.cxx使用Python C-API导出一些普通函数,foo.py用Python定义了Foo类,使得我们可以OO式使用封装。看一下SWIG生成的两个文件:
foo.py
class Foo(_object): def __init__(self, *args): this = _foo.new_Foo(*args) try: self.this.append(this) except: self.this = this def GetNum(*args): return _foo.Foo_GetNum(*args) def SetNum(*args): return _foo.Foo_SetNum(*args) ...
Foo_wrap.cxx
PyObject *_wrap_new_Foo(PyObject *self, PyObject *args) { Foo *result = (Foo *)new Foo(); resultobj = SWIG_NewPointerObj(SWIG_as_voidptr(result), SWIGTYPE_p_Foo, SWIG_POINTER_NEW | 0 ); return resultobj; } PyObject *_wrap_Foo_GetNum(PyObject *self, PyObject *args) { PyObject *resultobj = 0; Foo *arg1 = (Foo *) 0 ; int result; void *argp1 = 0 ; int res1 = 0 ; PyObject * obj0 = 0 ; if (!PyArg_ParseTuple(args,(char *)"O:Foo_GetNum",&obj0)) SWIG_fail; res1 = SWIG_ConvertPtr(obj0, &argp1,SWIGTYPE_p_Foo, 0 | 0 ); if (!SWIG_IsOK(res1)) { SWIG_exception_fail(SWIG_ArgError(res1), "in method '" "Foo_GetNum" "', expected argument " "1"" of type '" "Foo *""'"); } arg1 = reinterpret_cast< Foo * >(argp1); result = (int)(arg1)->GetNum(); resultobj = SWIG_From_int(static_cast< int >(result)); return resultobj; }
可以看到SWIG首先把C++类成员函数导出成普通函数,其第一个参数是self,即foo.py中的类Foo实例(为了区别,Python封装类记为py.Foo,C++类记为C++.Foo)。py.Foo的__init__函数调用_wrap_new_Foo构造C++实例,并使用SWIG_NewPointerObj将指针封装为一个PySwigObject,返回。py.Foo将其保存在self.this中。调用py.Foo.GetNum()时,使用SWIG_ConvertPtr再将py.Foo转换成C++.Foo指针,然后调用C++.Foo.GetNum()。
通过以上分析,发现_wrap_new_Foo函数中调用的SWIG_NewPointerObj可以使用C++指针构造一个PySwigObject对象,我们是不是可以手动调用该函数为已有C++实例构造一个PySwigObject呢,如果可以,将结果赋给一个py.Foo.this,理论上该py.Foo就绑定到了该C++实例。话不多说,尝试一下:
添加一个函数创建C++.Foo,并把指针打印出来:
Foo* CreateFoo() { Foo* f = new Foo(); f->SetNum(7); cout.unsetf(std::ios_base::basefield); cout << f << endl; return f; }
使用Python C-API建一个模块,命名为hook,如下:
#include <Python.h> #include <cstdio> #include <iostream> using namespace std; // 从Foo_wrap.cpp中复制出来的定义 typedef void *(*swig_converter_func)(void *, int *); typedef struct swig_type_info *(*swig_dycast_func)(void **); struct swig_type_info; extern "C" swig_type_info *swig_types[3]; extern "C" PyObject *SWIG_Python_NewPointerObj(PyObject *self, void *ptr, swig_type_info *type, int flags); #define SWIGTYPE_p_Foo swig_types[0] static PyObject* AttachFooObject(PyObject *self, PyObject *args) { const char *addr_str; if (!PyArg_ParseTuple(args, "s", &addr_str)) return NULL; unsigned long addr; sscanf(addr_str, "%lx", &addr); return SWIG_Python_NewPointerObj(NULL, reinterpret_cast<void*>(addr), SWIGTYPE_p_Foo, 0); } static PyMethodDef HookMethods[] = { { "Attach", AttachFooObject, METH_VARARGS, "Attach C++ object" }, { NULL, NULL, 0, NULL } }; PyMODINIT_FUNC inithook(void) { PyObject *m; m = Py_InitModule("hook", HookMethods); if (m == NULL) return; }
distuitls脚本:
from distutils.core import setup, Extension setup(name = "hook", version = "1.0", ext_modules = [Extension("hook", ["hook.cpp"], extra_compile_args = ['-g'], extra_link_args=['_foo.so'])])
先看一下效果:
>>> from foo import * >>> CreateFoo() 0x60aba0 <foo.Foo; proxy of <Swig Object of type 'Foo *' at 0x2a987b1b70> > >>> import hook >>> def empty_init(self): ... pass ... >>> Foo.__init__ = empty_init // 禁止Foo.__init__再创建C++.Foo >>> f = Foo() >>> f.this = hook.Attach('0x60aba0') >>> f.GetNum() 7
可以看到,f已经成功绑定到CreateFoo()创建的C++实例上。
但是,hook.cpp中从Foo_wrap.cpp复制出来的那段符号定义中,实际上swig_type_info是没有导出的,可以nm –D _foo.so | grep swig_type_info查看一下,上面我是直接手动修改了SWIG生成的Foo_wrap.cpp文件:
还好SWIG是开源的,我们可以给SWIG打patch,在SWIG中找到输出这段代码的部分:
swig/Source/Swig/typesys.c (line 2148 ~ line 2154)
Printf(f_forward, "static swig_type_info *swig_types[%d];\n", i + 1); Printf(f_forward, "static swig_module_info swig_module = {swig_types, %d, 0, 0, 0, 0};\n", i); Printf(f_forward, "#define SWIG_TypeQuery(name) SWIG_TypeQueryModule(&swig_module, &swig_module, name)\n"); Printf(f_forward, "#define SWIG_MangledTypeQuery(name) SWIG_MangledTypeQueryModule(&swig_module, &swig_module, name)\n"); Printf(f_forward, "\n/* -------- TYPES TABLE (END) -------- */\n\n");
我们手动修改一下:
Printf(f_forward, "extern "C" swig_type_info *swig_types[%d];\n", i + 1); Printf(f_forward, "swig_type_info *swig_types[%d];\n", i + 1); Printf(f_forward, "static swig_module_info swig_module = {swig_types, %d, 0, 0, 0, 0};\n", i); Printf(f_forward, "#define SWIG_TypeQuery(name) SWIG_TypeQueryModule(&swig_module, &swig_module, name)\n"); Printf(f_forward, "#define SWIG_MangledTypeQuery(name) SWIG_MangledTypeQueryModule(&swig_module, &swig_module, name)\n"); Printf(f_forward, "\n/* -------- TYPES TABLE (END) -------- */\n\n");
diff一下,制作一个patch就可以让SWIG支持了。
总结:
本文只是提供了实现思路,具体应用还需要考虑很多问题,例如swig_type_info的查询等。本文中所用的SWIG是2.0.10版本,有些版本中SWIG_Python_NewPointerObject是没有导出的,不过也可以用打patch的方法解决。