SWIG and Boost::variant

SWIG and Boost::variant

本文关键字:variant Boost and SWIG      更新时间:2023-10-16

我正在尝试使用 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包装器中,并且似乎可以使用。但是,当我尝试将以下行添加到界面文件中时

%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 : input(3) 中的语法错误。

所以我去看看错误,它是一行,它有一个宏在另一个头文件中定义,特别是"boost/mpl/aux_/value_wknd.hpp",所以我用 %include 将其添加到接口文件中,现在看来 SWIG.exe 崩溃并显示一个错误,有助于说明

访问冲突

长话短说,有没有办法包装一个 boost::variant 模板类型?不幸的是,这个模板定义被烘焙到我们库的核心中,我现在无法更改它。另外,如果这很重要,我在MSVC 2013编译器上。

如果无法直接包装模板类型,是否可以解决此问题?我正在通读 SWIG 文档,看看是否有一些可以应用的字体图魔法,但我对 SWIG 总体上相当陌生。

你可以这样做。我想了很长一段时间,boost::variant最整洁的Python接口实际上是什么。我的结论是,99%的情况下,Python用户甚至不应该意识到正在使用变体类型 - 联合和变体基本上只是C++的受约束的鸭子类型。

所以我的目标是这样的:

  • 尽可能从现有的排版图中受益 - 我们不想从头开始编写自己的std::stringint,打字图。
  • 无论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支持。(此处作为选项记录
  • )
  • 我的文件中的大部分繁重工作都是使用FOR_EACH可变参数宏完成的。这在很大程度上与我之前对std::function的回答相同,它本身是从几个较旧的堆栈溢出答案派生出来的,并适应与SWIG的预处理器一起使用。
  • 使用FOR_EACH宏,我们告诉 SWIG 为每个变体可以容纳的类型包装一个构造函数。这让我们可以从 Python 代码显式构造变体,并添加两个额外的构造函数
  • 通过使用这样的构造函数,我们可以严重依赖SWIG的重载分辨率支持。因此,给定一个 Python 对象,我们可以简单地依靠 SWIG 来确定如何从中构造一个变体。这为我们节省了大量额外的工作,并为变体中的每种类型使用现有的类型图。
  • intypemap 基本上只是通过稍微复杂的路由委托给构造函数,因为以编程方式在同一模块中找到其他函数非常困难。一旦委托发生,我们就使用函数参数的正常转换来将临时变量传递到函数中,就好像它是我们得到的一样。
  • 我们还合成了一组额外的成员函数,convert_typeN内部只调用boost::get<TYPE>(*this),其中 N 和 TYPE 是变体类型列表中每种类型的位置。
  • 在out类型图中,这允许我们查找Python函数,使用which()来确定变体当前包含的内容。然后,我们得到了大部分SWIG生成的代码,使用现有的类型图将给定的变体制作成底层类型的Python对象。同样,这为我们节省了很多精力,并使一切都即插即用。

如果你决定使用 SWIG(我从你的帖子中不清楚,因为你说这对 SWIG 来说是相当新的,所以我假设这是一个新项目),那么停止阅读并忽略这个答案。

但是,如果要使用的绑定技术尚未修复,并且您只需要绑定Python,而不需要其他语言,则另一种方法是使用cppyy(http://cppyy.org,并且完全免责声明:我是主要作者)。有了它,boost::variant 类型可以直接在 Python 中使用,然后你可以通过编写 Python 代码而不是 SWIG .i 代码来使其看起来/行为更像 Pythonistic。

示例(请注意,cppyy 在 PyPI 上为 Windows 提供了轮子,但使用 MSVC2017 而不是 MSVC2013 构建,所以我会保留关于MSVC2013是否足够现代来构建代码的警告,因为我还没有尝试过):

import cppyy
cppyy.include("boost/variant/variant.hpp")
cppyy.include("boost/variant/get.hpp")
cpp   = cppyy.gbl
std   = cpp.std
boost = cpp.boost
cppyy.cppdef("""
class A
{
//constructors and such.
};
class B
{
//constructors and such.
};
class C
{
//constructors and such.
};
""")
VariantType = boost.variant['A, B, C']
VariantTypeList = std.vector[VariantType]
v = VariantTypeList()
v.push_back(VariantType(cpp.A()))
print(v.back().which())
v.push_back(VariantType(cpp.B()))
print(v.back().which())
v.push_back(VariantType(cpp.C()))
print(v.back().which())
print(boost.get['A'](v[0]))
try:
print(boost.get['B'](v[0]))
except Exception as e:
print(e)   # b/c of type-index mismatch above
print(boost.get['B'](v[1]))  # now corrected
print(boost.get['C'](v[2]))

生成以下预期输出:

$ python variant.py
0
1
2
<cppyy.gbl.A object at 0x5053704>
Could not instantiate get<B>:
B& boost::get(boost::variant<A,B,C>& operand) =>
Exception: boost::bad_get: failed value get using boost::get (C++ exception)
<cppyy.gbl.B object at 0x505370c>
<cppyy.gbl.C object at 0x5053714>