如何使用 SWIG 包装一个在 python 中接收函数指针的 c++ 函数

Posted

技术标签:

【中文标题】如何使用 SWIG 包装一个在 python 中接收函数指针的 c++ 函数【英文标题】:How to wrap a c++ function which takes in a function pointer in python using SWIG 【发布时间】:2014-04-07 21:43:27 【问题描述】:

这是我想做的简化示例。假设我在 test.h 中有以下 c++ 代码

double f(double x);
double myfun(double (*f)(double x));

现在这些函数的作用并不重要。重要的是 myfun 接受一个函数指针。

在我的接口文件中包含 test.h 文件后,我使用 SWIG 编译了一个 python 模块“test”。现在,在 Python 中,我运行以下命令:

import test
f = test.f

这会创建一个正常工作的函数 f,它接受一个双精度数。但是,当我尝试在 python 中将“f”传递给 myfun 时,会发生这种情况:

myfun(f)
TypeError: in method 'myfun', argument 1 of type 'double (*)(double)'

我该如何解决这个问题?我想我需要在我的 SWIG 接口文件中声明类型映射,但我不确定正确的语法是什么或放在哪里。我试过了

%typemap double f(double);

但这没有用。有什么想法吗?

【问题讨论】:

你也可以显示你的 .i 吗?您所说的“正常工作的函数 f”是什么意思?这是一个 Python 函数吗?还是您通过 SWIG 导出到 Python 的 C 函数指针? 【参考方案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”的程度。

解决方案一:

第一个解决方案是微不足道的。我们已经使用test.make_fptr() 将Python 句柄返回给函数f 的函数指针。所以我们实际上可以调用:

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 来做到这一点:

%模块测试

%
#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,这取决于我当时想用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 和包装器实例,但我们无法真正摆脱在任何地方使用 %feature("pythonprepend") 的需要将这些包装器传递回 SWIG。 (如果可以透明地做到这一点,那远远超出了我的 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:

我正在研究一个更简洁的第 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)

一口气解决了几乎所有的问题。这甚至是手册中的documented,尽管似乎没有提到%pythoncallback。因此,前面的四个解决方案大多只是用作自定义 SWIG 界面的示例。

但在一种情况下,解决方案 4 仍然有用 - 如果您想混合和匹配 C 和 Python 实现的回调,您需要实现这两者的混合。 (理想情况下,您会尝试在类型映射中进行 SWIG 函数指针类型转换,然后如果失败则回退到 PyCallable 方法)。

【讨论】:

哇,这是很棒的柔印。我需要一点时间来吸收所有这些,但谢谢! @user1589038 小脸瞬间:我打算修补 SWIG 以添加更简洁的第 5 个解决方案,结果它已经存在! 我们可以使用 Python 函数作为回调,即将f = lambda x: return x*5 传递给test.myfun() 吗? @kawing-chiu 我在这里有一个例子:***.com/a/11522655/168175 你可以将它与 ffi 结合起来,就像我在这里展示的 Java 一样:***.com/a/40273233/168175 @Flexo 谢谢!会去看看。

以上是关于如何使用 SWIG 包装一个在 python 中接收函数指针的 c++ 函数的主要内容,如果未能解决你的问题,请参考以下文章

为静态库编译 SWIG Python 包装器?

使用python包装的c ++ SWIG模拟输入

将 python 函数传递给 SWIG 包装的 C++ 代码

使用 SWIG 围绕 C++ 的 Python 包装器。参数类型无法识别

使用 SWIG 为 Python 包装 C++。 “向量”未声明

SWIG 如何在 Python 中包装 map<string,string>?