我正在尝试使用 SWIG 将 c++ 项目包装到 python api 中,但我遇到了具有以下格式的代码的问题。
class A
{
//constructors and such.
};
class B
{
//constructors and such.
};
class C
{
//constructors and such.
};
typedef boost::variant<A,B,C> VariantType;
typedef std::vector<boost::variant<A,B,C>> VariantTypeList;
A、B 和 C 类都在 python 包装器中毫无问题地出现并且似乎可用。但是,当我尝试将以下行添加到接口(interface)文件时
%template(VariantType) boost::variant<A,B,C>;
%template(VariantTypeList) std::vector<boost::variant<A,B,C>>;
我收到一条错误消息
Boost\x64\include\boost\variant\variant.hpp(148): error : Syntax error in input(3).
所以我去查看错误及其一行,其中有一个宏定义在另一个头文件中,特别是“boost/mpl/aux_/value_wknd.hpp”,所以我使用 %include 将其添加到接口(interface)文件中,现在看来 SWIG.exe 崩溃时出现了一个有用的错误提示
Access Violation
长话短说,有没有办法包装 boost::variant 模板类型?不幸的是,这个模板定义已经嵌入到我们库的核心中,我现在无法更改它。另外,如果重要的话,我正在使用 MSVC 2013 编译器。
如果不能直接包装模板类型,是否可以解决这个问题?我正在阅读 SWIG 文档,看看是否有一些可以应用的类型映射魔法,但我对 SWIG 总体来说还很陌生。
最佳答案
你可以做到这一点。我想了很久 boost::variant
的最简洁的 Python 接口(interface)是什么实际上是。我的结论是,在 99% 的情况下,Python 用户甚至不应该意识到正在使用变体类型 - union 和变体基本上只是 C++ 的某种约束鸭子类型。
所以我的目标是:
- 尽可能从现有类型映射中获益——我们不想自己编写
std::string
,int
, 从头开始打字。 - C++ 函数接受
boost::variant
的任何地方我们应该透明地接受变体可以为该函数参数保留的任何类型。 - C++ 函数在任何地方返回
boost::variant
当我们将它返回到 Python 时,我们应该透明地将它作为变体所持有的类型返回。 - 允许 Python 用户显式创建变体对象,例如一个空的,但不要指望它会真正发生。 (也许这对引用输出参数有用,但目前我还没有走那么远)。
- 我没有这样做,但是使用 SWIG 的导向器功能从该界面当前所在的位置添加访问者会相当简单。
在不添加任何机械的情况下完成所有这些工作是相当繁琐的。我将所有内容都打包到一个可重复使用的文件中,这是我的 boost_variant.i 的最终工作版本:
%{
#include <boost/variant.hpp>
static PyObject *this_module = NULL;
%}
%init %{
// We need to "borrow" a reference to this for our typemaps to be able to look up the right functions
this_module = m; // borrow should be fine since we can only get called when our module is loaded right?
// Wouldn't it be nice if $module worked *anywhere*
%}
#define FE_0(...)
#define FE_1(action,a1) action(0,a1)
#define FE_2(action,a1,a2) action(0,a1); action(1,a2)
#define FE_3(action,a1,a2,a3) action(0,a1); action(1,a2); action(2,a3)
#define FE_4(action,a1,a2,a3,a4) action(0,a1); action(1,a2); action(2,a3); action(3,a4)
#define FE_5(action,a1,a2,a3,a4,a5) action(0,a1); action(1,a2); action(2,a3); action(3,a4); action(4,a5)
#define GET_MACRO(_1,_2,_3,_4,_5,NAME,...) NAME
%define FOR_EACH(action,...)
GET_MACRO(__VA_ARGS__, FE_5, FE_4, FE_3, FE_2, FE_1, FE_0)(action,__VA_ARGS__)
%enddef
#define in_helper(num,type) const type & convert_type ## num () { return boost::get<type>(*$self); }
#define constructor_helper(num,type) variant(const type&)
%define %boost_variant(Name, ...)
%rename(Name) boost::variant<__VA_ARGS__>;
namespace boost {
struct variant<__VA_ARGS__> {
variant();
variant(const boost::variant<__VA_ARGS__>&);
FOR_EACH(constructor_helper, __VA_ARGS__);
int which();
bool empty();
%extend {
FOR_EACH(in_helper, __VA_ARGS__);
}
};
}
%typemap(out) boost::variant<__VA_ARGS__> {
// Make our function output into a PyObject
PyObject *tmp = SWIG_NewPointerObj(&$1, $&1_descriptor, 0); // Python does not own this object...
// Pass that temporary PyObject into the helper function and get another PyObject back in exchange
const std::string func_name = "convert_type" + std::to_string($1.which());
$result = PyObject_CallMethod(tmp, func_name.c_str(), "");
Py_DECREF(tmp);
}
%typemap(in) const boost::variant<__VA_ARGS__>& (PyObject *tmp=NULL) {
// I don't much like having to "guess" the name of the make_variant we want to use here like this...
// But it's hard to support both -builtin and regular modes and generically find the right code.
PyObject *helper_func = PyObject_GetAttrString(this_module, "new_" #Name );
assert(helper_func);
// TODO: is O right, or should it be N?
tmp = PyObject_CallFunction(helper_func, "O", $input);
Py_DECREF(helper_func);
if (!tmp) SWIG_fail; // An exception is already pending
// TODO: if we cared, we chould short-circuit things a lot for the case where our input really was a variant object
const int res = SWIG_ConvertPtr(tmp, (void**)&$1, $1_descriptor, 0);
if (!SWIG_IsOK(res)) {
SWIG_exception_fail(SWIG_ArgError(res), "Variant typemap failed, not sure if this can actually happen");
}
}
%typemap(freearg) const boost::variant<__VA_ARGS__>& %{
Py_DECREF(tmp$argnum);
%}
%enddef
这为我们提供了一个可以在 SWIG 中使用的宏,%boost_variant
.然后,您可以在您的界面文件中使用它,如下所示:
%module test
%include "boost_variant.i"
%inline %{
struct A {};
struct B {};
%}
%include <std_string.i>
%boost_variant(TestVariant, A, B, std::string);
%inline %{
void idea(const boost::variant<A, B, std::string>&) {
}
boost::variant<A,B,std::string> make_me_a_thing() {
struct A a;
return a;
}
boost::variant<A,B,std::string> make_me_a_string() {
return "HELLO";
}
%}
哪里%boost_variant
宏将第一个参数作为类型的名称(很像 %template
),其余参数作为变体中所有类型的列表。
这足以让我们运行以下 Python:
import test
a = test.A();
b = test.B();
test.idea(a)
test.idea(b)
print(test.make_me_a_thing())
print(test.make_me_a_string())
那么这实际上是如何工作的呢?
- 我们基本上复制了 SWIG 的
%template
在这里支持。 (这是 documented here as an option ) - 我文件中的大部分繁重工作都是使用
FOR_EACH
完成的可变宏。这在很大程度上与我的 previous answer onstd::function
相同,它本身源自几个较旧的 Stack Overflow 答案,并适用于 SWIG 的预处理器。 - 使用
FOR_EACH
我们告诉 SWIG 为变体可以容纳的每种类型包装一个构造函数。这让我们可以从 Python 代码显式构造变体,并添加两个额外的构造函数 - 通过使用这样的构造函数,我们可以极大地依赖 SWIG 的重载解析支持。因此,给定一个 Python 对象,我们可以简单地依靠 SWIG 来确定如何从中构造一个变体。这为我们节省了大量额外的工作,并为变体中的每种类型使用现有的类型映射。
in
typemap 基本上只是通过稍微复杂的路线委托(delegate)给构造函数,因为以编程方式在同一模块中找到其他函数非常困难。一旦委托(delegate)发生,我们就使用函数参数的正常转换,将临时变体传递给函数,就好像它是我们得到的一样。- 我们还综合了一组额外的成员函数,
convert_typeN
在内部只调用boost::get<TYPE>(*this)
,其中 N 和 TYPE 是每种类型在变体类型列表中的位置。 - 在 out 类型映射中,这允许我们使用
which()
查找 Python 函数。以确定变体当前持有什么。然后,我们获得了大部分 SWIG 生成的代码,使用现有的类型映射将给定的变体转换为底层类型的 Python 对象。这再次为我们节省了很多精力,让一切都即插即用。
关于python - SWIG 和 Boost::变体,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58415839/