如何使用SWIG在Python中包装一个接受函数指针的C++函数

13 投票
1 回答
7178 浏览
提问于 2025-04-18 01:46

这里有一个简化的例子,说明我想做的事情。假设我在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 个回答

21

注意:这个回答有很长一段关于解决方法的内容。如果你只是想使用这个,可以直接跳到解决方案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是为了避免看到同一标识符的多个版本而产生的警告。

(注意:此时我还从头文件中移除了typedefmake_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方法)。

撰写回答