如何使用SWIG在Python中包装一个接受函数指针的C++函数
这里有一个简化的例子,说明我想做的事情。假设我在test.h文件中有以下的C++代码:
double f(double x);
double myfun(double (*f)(double x));
现在这些函数具体做什么并不重要。重要的是,我的myfun函数需要一个函数指针作为输入。
在我的接口文件中包含了test.h文件后,我使用SWIG编译了一个名为"test"的Python模块。然后,在Python中,我运行了以下命令:
import test
f = test.f
这会创建一个正常工作的函数f,它接受一个双精度浮点数(double)。但是,当我尝试将"f"传递给myfun时,发生了以下情况:
myfun(f)
TypeError: in method 'myfun', argument 1 of type 'double (*)(double)'
我该如何解决这个问题呢?我想我需要在SWIG接口文件中添加一个类型映射声明,但我不确定正确的语法是什么,或者应该放在哪里。我试过:
%typemap double f(double);
但那并没有成功。有没有什么建议?
1 个回答
注意:这个回答有很长一段关于解决方法的内容。如果你只是想使用这个,可以直接跳到解决方案5。
问题
你发现了一个问题,那就是在Python中,一切都是对象。在我们开始解决问题之前,先来理解一下当前的情况。我创建了一个完整的示例来进行演示,包含一个头文件:
double f(double x) {
return x*x;
}
double myfun(double (*f)(double x)) {
fprintf(stdout, "%g\n", f(2.0));
return -1.0;
}
typedef double (*fptr_t)(double);
fptr_t make_fptr() {
return f;
}
到目前为止,我做的主要改动是为你的声明添加了定义,以便我可以测试它们,并且添加了一个make_fptr()
函数,它返回一个我们知道会被包装成函数指针的东西给Python。
这样,第一个SWIG模块可能看起来像这样:
%module test
%{
#include "test.h"
%}
%include "test.h"
我们可以用以下命令编译它:
swig2.0 -Wall -python test.i && gcc -Wall -Wextra -I/usr/include/python2.6 -std=gnu99 -shared -o _test.so test_wrap.c
现在我们可以运行这个,并询问Python我们拥有的类型——test.f
的类型,以及调用test.make_fptr()
的结果的类型:
Python 2.6.6 (r266:84292, Dec 27 2010, 00:02:40)
[GCC 4.4.5] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import test
>>> type(test.f)
<type 'builtin_function_or_method'>
>>> repr(test.f)
'<built-in function f>'
>>> type(test.make_fptr())
<type 'SwigPyObject'>
>>> repr(test.make_fptr())
"<Swig Object of type 'fptr_t' at 0xf7428530>"
所以目前的问题应该很清楚了——没有从内置函数到SWIG函数指针类型的转换,因此你调用myfun(test.f)
是行不通的。
解决方案
那么问题是我们该如何(以及在哪里)解决这个问题呢?实际上,我们可以选择至少四种可能的解决方案,这取决于你想要支持多少其他语言,以及你想要多“Pythonic”。
解决方案1:
第一个解决方案很简单。我们已经使用test.make_fptr()
为函数f
返回了一个Python的函数指针句柄。所以我们实际上可以调用:
f=test.make_fptr()
test.myfun(f)
个人来说,我不太喜欢这个解决方案,因为这不是Python程序员所期望的,也不是C程序员所期望的。唯一的优点就是实现简单。
解决方案2:
SWIG提供了一种机制,可以将函数指针暴露给目标语言,使用%constant
。 (通常这用于暴露编译时常量,但实际上函数指针在最简单的形式下本质上就是常量)。
所以我们可以修改我们的SWIG接口文件:
%module test
%{
#include "test.h"
%}
%constant double f(double);
%ignore f;
%include "test.h"
这个%constant
指令告诉SWIG将f
包装为一个函数指针,而不是一个函数。%ignore
是为了避免看到同一标识符的多个版本而产生的警告。
(注意:此时我还从头文件中移除了typedef
和make_fptr()
函数)
这现在让我们可以运行:
Python 2.6.6 (r266:84292, Dec 27 2010, 00:02:40)
[GCC 4.4.5] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import test
>>> type(test.f)
<type 'SwigPyObject'>
>>> repr(test.f)
"<Swig Object of type 'double (*)(double)' at 0xf7397650>"
太好了——它得到了函数指针。但这里有个问题:
>>> test.f(0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'SwigPyObject' object is not callable
现在我们无法从Python端调用test.f
。这就引出了下一个解决方案:
解决方案3:
为了解决这个问题,我们首先将test.f
同时暴露为函数指针和内置函数。我们可以通过简单地使用%rename
而不是%ignore
来做到这一点:
%module test
%{
#include "test.h"
%}
%constant double f(double);
%rename(f_call) f;
%include "test.h"
Python 2.6.6 (r266:84292, Dec 27 2010, 00:02:40)
[GCC 4.4.5] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import test
>>> repr(test.f)
"<Swig Object of type 'double (*)(double)' at 0xf73de650>"
>>> repr(test.f_call)
'<built-in function f_call>'
这算是一个进步,但我仍然不喜欢必须记住在不同的上下文中是写test.f_call
还是test.f
的想法。我们可以通过在SWIG接口中写一些Python代码来实现这一点:
%module test
%{
#include "test.h"
%}
%rename(_f_ptr) f;
%constant double f(double);
%rename(_f_call) f;
%feature("pythonprepend") myfun %{
args = f.modify(args)
%}
%include "test.h"
%pythoncode %{
class f_wrapper(object):
def __init__(self, fcall, fptr):
self.fptr = fptr
self.fcall = fcall
def __call__(self,*args):
return self.fcall(*args)
def modify(self, t):
return tuple([x.fptr if isinstance(x,self.__class__) else x for x in t])
f = f_wrapper(_f_call, _f_ptr)
%}
这里有几个功能部分。首先,我们创建一个新的纯Python类,将一个函数包装为可调用的和函数指针。它作为成员持有真正的SWIG包装(并重命名)的函数指针和函数。根据Python的约定,这些现在以一个下划线开头。其次,我们将test.f
设置为这个包装类的一个实例。当它被调用时,它会将调用传递下去。最后,我们在myfun
包装中插入一些额外的代码,以便在不改变其他参数的情况下,替换为真正的函数指针。
这确实按预期工作,例如:
import test
print "As a callable"
test.f(2.0)
print "As a function pointer"
test.myfun(test.f)
我们可以让这个更好,比如使用SWIG宏来避免重复%rename
、%constant
和包装实例的创建,但我们无法逃避在每次将这些包装传回SWIG时都使用%feature("pythonprepend")
的需要。(如果可以做到这一点而不显式使用,我的Python知识可能不够)。
解决方案4:
之前的解决方案有点整洁,它按预期透明地工作(作为C和Python用户),而且其机制完全由Python实现。
不过还有一个问题,除了需要在每次使用函数指针时都使用pythonprepend——如果你运行swig -python -builtin
,它根本无法工作,因为根本没有Python代码可以预先添加! (你需要将包装的构造更改为:f = f_wrapper(_test._f_call, _test._f_ptr)
,但那样是不够的)。
所以我们可以通过在SWIG接口中编写一些Python C API来解决这个问题:
%module test
%{
#include "test.h"
%}
%{
static __thread PyObject *callback;
static double dispatcher(double d) {
PyObject *result = PyObject_CallFunctionObjArgs(callback, PyFloat_FromDouble(d), NULL);
const double ret = PyFloat_AsDouble(result);
Py_DECREF(result);
return ret;
}
%}
%typemap(in) double(*)(double) {
if (!PyCallable_Check($input)) SWIG_fail;
$1 = dispatcher;
callback = $input;
}
%include "test.h"
这有点麻烦,主要有两个原因。首先,它使用一个(线程局部)全局变量来存储Python可调用对象。对于大多数实际的回调,这很容易解决,因为有一个void*
用户数据参数以及实际的回调输入。在那些情况下,“用户数据”可以是Python可调用对象。
第二个问题就有点棘手了——因为可调用对象是一个包装的C函数,所以调用序列现在涉及将所有内容包装为Python类型,并从Python解释器来回传递,这样做本该是简单的事情。这样会带来相当大的开销。
我们可以从给定的PyObject
向后推导,试图找出它是哪个函数(如果有的话)的包装:
%module test
%{
#include "test.h"
%}
%{
static __thread PyObject *callback;
static double dispatcher(double d) {
PyObject *result = PyObject_CallFunctionObjArgs(callback, PyFloat_FromDouble(d), NULL);
const double ret = PyFloat_AsDouble(result);
Py_DECREF(result);
return ret;
}
SWIGINTERN PyObject *_wrap_f(PyObject *self, PyObject *args);
double (*lookup_method(PyObject *m))(double) {
if (!PyCFunction_Check(m)) return NULL;
PyCFunctionObject *mo = (PyCFunctionObject*)m;
if (mo->m_ml->ml_meth == _wrap_f)
return f;
return NULL;
}
%}
%typemap(in) double(*)(double) {
if (!PyCallable_Check($input)) SWIG_fail;
$1 = lookup_method($input);
if (!$1) {
$1 = dispatcher;
callback = $input;
}
}
%include "test.h"
这确实需要为每个函数指针编写一些代码,但现在这只是一个优化,而不是一个要求,并且可以通过SWIG宏变得更通用。
解决方案5:
我正在研究一个更整洁的第五个解决方案,使用%typemap(constcode)
来允许%constant
同时作为方法和函数指针使用。不过,事实证明SWIG已经支持这样做,我在阅读一些SWIG源代码时发现了这一点。所以实际上我们只需要简单地:
%module test
%{
#include "test.h"
%}
%pythoncallback;
double f(double);
%nopythoncallback;
%ignore f;
%include "test.h"
这个%pythoncallback
启用了一些全局状态,使得后续的函数可以被包装为既可以用作函数指针又可以用作函数!%nopythoncallback
则禁用这一点。
这样就可以工作了(无论是否带-builtin
):
import test
test.f(2.0)
test.myfun(test.f)
这几乎一次性解决了所有问题。这在手册中也有文档说明,尽管似乎没有提到%pythoncallback
。所以之前的四个解决方案主要只是作为自定义SWIG接口的示例。
不过还有一种情况,解决方案4会有用——如果你想混合使用C和Python实现的回调,你需要实现这两者的混合。(理想情况下,你会尝试在你的typemap中进行SWIG函数指针类型转换,然后如果失败再回退到PyCallable方法)。