使用Boost预处理器的SWIG预处理宏
我正在使用这里推荐的带有 ToString 实现的枚举:如何将枚举类型变量转换为字符串?。这个方法很好用,效果也不错。
不过,我遇到的问题是,当我尝试将这个宏包装并导出到一个用 SWIG 封装的 Python 库时,就出现了麻烦。类似的问题可以参考:因为预处理指令导致的 SWIG 错误。在那个问题中,解决办法是向 SWIG 接口添加头文件和声明。但到目前为止,我还没有成功,可能是我不知道该添加什么。
我尝试过:
%include <boost/preprocessor/config/config.hpp>
%include <boost/preprocessor/stringize.hpp>
%include <boost/preprocessor/seq/for_each.hpp>
%include <boost/preprocessor/seq/enum.hpp>
最小示例:
minimal.h
#ifndef MINIMAL_H
#define MINIMAL_H
#include <boost/preprocessor.hpp>
//Found this here: https://stackoverflow.com/questions/5093460/how-to-convert-an-enum-type-variable-to-a-string
#define X_DEFINE_ENUM_WITH_STRING_CONVERSIONS_TOSTRING_CASE(r, data, elem) \
case elem : return BOOST_PP_STRINGIZE(elem);
#define DEFINE_ENUM_WITH_STRING_CONVERSIONS(name, enumerators) \
enum name { \
BOOST_PP_SEQ_ENUM(enumerators) \
}; \
\
inline const char* ToString(name v) \
{ \
switch (v) \
{ \
BOOST_PP_SEQ_FOR_EACH( \
X_DEFINE_ENUM_WITH_STRING_CONVERSIONS_TOSTRING_CASE, \
name, \
enumerators \
) \
default: return "[Unknown " BOOST_PP_STRINGIZE(name) "]"; \
} \
}
DEFINE_ENUM_WITH_STRING_CONVERSIONS(my_enum, (A)(B))
#endif
minimal.cpp
#include <iostream>
#include "minimal.h"
int main(){
using namespace std;
cout << A << ": " << ToString(A) << endl;
cout << B << ": " << ToString(B) << endl;
}
minimal.i
%module minimal
%{
#include "minimal.h"
%}
%include "minimal.h"
错误信息不是很明确。第29行是我定义 my_enum 的地方。
matthias@rp3deb:~/dvl/swig_boost_minimal$ swig minimal.i
minimal.h:29: Error: Syntax error in input(1).
有没有什么建议可以帮助我完成这个包装?
1 个回答
如果你想让SWIG读取boost/preprocessor.hpp,你可以这样做:
%module minimal
%{
#include "minimal.h"
%}
%include <boost/preprocessor.hpp>
%include "minimal.h"
因为默认情况下,SWIG不会跟随#include
指令。(你也可以使用-includeall
来让它跟随这些指令)。不过在这种情况下,我觉得让SWIG的预处理器理解Boost预处理器库的那些复杂魔法是没希望的。
不过,我们可以尝试用一种同样不错但更“Python风格”的语法来实现。实际上,我们要做的是为SWIG的包装器写一个完全不同版本的DEFINE_ENUM_WITH_STRING_CONVERSIONS
。不过它会与C++中的定义兼容。
为此,我将从把你的文件minimal.h拆分成两个文件开始。一个是宏定义,另一个是使用这个宏的文件。(我们可以用不同的方法来做,比如用#ifndef DEFINE_ENUM_WITH_STRING_CONVERSIONS
或#ifndef SWIG
来包裹宏定义,这也是有效的解决方案)。
因此,我们现在有了enum.hh:
#ifndef ENUM_H
#define ENUM_H
#include <boost/preprocessor.hpp>
//Found this here: https://stackoverflow.com/questions/5093460/how-to-convert-an-enum-type-variable-to-a-string
#define X_DEFINE_ENUM_WITH_STRING_CONVERSIONS_TOSTRING_CASE(r, data, elem) \
case elem : return BOOST_PP_STRINGIZE(elem);
#define DEFINE_ENUM_WITH_STRING_CONVERSIONS(name, enumerators) \
enum name { \
BOOST_PP_SEQ_ENUM(enumerators) \
}; \
\
inline const char* ToString(name v) \
{ \
switch (v) \
{ \
BOOST_PP_SEQ_FOR_EACH( \
X_DEFINE_ENUM_WITH_STRING_CONVERSIONS_TOSTRING_CASE, \
name, \
enumerators \
) \
default: return "[Unknown " BOOST_PP_STRINGIZE(name) "]"; \
} \
}
#endif
还有minimal.h:
#ifndef MINIMAL_H
#define MINIMAL_H
#include "enum.h"
DEFINE_ENUM_WITH_STRING_CONVERSIONS(my_enum, (A)(B))
#endif
所以你的minimal.cpp依然可以正常工作,但现在我们可以写一个SWIG模块,至少可以编译,尽管它现在还没有任何实际用处:
%module minimal
%{
#include "minimal.h"
%}
%define DEFINE_ENUM_WITH_STRING_CONVERSIONS(name,enumerators)
%enddef
%include "minimal.h"
目前这个模块有一个占位符,SWIG特定的宏,我们将填充它。我这样做有点丑陋,因为我尽量避免改变现有宏的定义和使用方式。
我作为起点产生了另一个文件,enum.i:
%include <std_vector.i>
%include <std_string.i>
%{
#include <vector>
#include <string>
#include <tuple>
%}
%define DEFINE_ENUM_WITH_STRING_CONVERSIONS(name,enumerators)
%{
typedef std::tuple<name,std::string> name ## _entry;
struct name ## _helper {
std::vector<name ## _entry> list;
name ## _helper(const name value) {
list.push_back(std::make_tuple(value,ToString(value)));
}
name ## _helper operator()(const name value) {
list.push_back(std::make_tuple(value,ToString(value)));
return *this;
}
};
static const std::vector<name ## _entry> name ## _list = name ## _helper enumerators . list;
%}
struct name ## _entry {
%extend {
const unsigned long value {
return std::get<0>(*$self);
}
const std::string& label {
return std::get<1>(*$self);
}
}
};
%template(name ## vec) std::vector<name ## _entry>;
const std::vector<name ## _entry> name ## _list;
%enddef
这样minimal.i只需要变成:
%module minimal
%{
#include "minimal.h"
%}
%include "enum.i"
%include "minimal.h"
这个宏的作用是获取enumerators
的值,像(A)(B)
这样的,然后生成一些完全标准(虽然有点奇怪)的C++代码,把它扩展成std::vector<std::tuple<my_enum,std::string>>
。这是通过将第一个枚举成员映射到构造函数调用,其他的映射到重载的operator()
来实现的。我们使用enum.h提供的ToString()
来找到字符串表示。最后,我们的宏有足够的信息来以一种在Python中有意义的方式包装这个元组的向量。
有了这个,我们可以做一些像这样的事情:
import minimal
print ", ".join(("%s(%d)" % (x.label,x.value) for x in minimal.my_enum_list))
编译并运行后会得到:
A(0), B(1)
也就是说,足够让我们开始编写能够识别C++枚举的标签和值的Python代码。
但我们不要止步于此!我为什么故意把生成的向量叫做my_enum_list
而不是my_enum
?因为我们现在可以做更多的事情。
Python 2.7没有任何默认的“枚举风格”,但这并不妨碍我们把它包装成既符合Python风格又对了解枚举的人自然的东西。我是通过阅读这个其他答案来实现我的Python 2.7枚举支持的。首先,我在文件中使用%pythoncode
添加了一些通用的枚举支持例程(在最终源代码中标记为#1),但在SWIG宏之外,因为没有必要改变它。我还在SWIG宏内部添加了一个%pythoncode
(标记为#2),每个实际的枚举调用一次。为了使其工作,我不得不将之前版本中的const std::vector
转换为一个函数,以便在生成的Python的正确部分中可以访问。最后,我必须向SWIG展示真实枚举的前向声明,以说服它接受这个作为函数的参数。最终结果是:
%include <std_vector.i>
%include <std_string.i>
%{
#include <vector>
#include <string>
#include <tuple>
%}
// #1
%pythoncode %{
class EnumValue(int):
def __new__(cls,v,l):
result = super(EnumValue,cls).__new__(cls,v)
result._value = l
return result
def __str__(self):
return self._value
def make_enum(name,enums):
return type(name, (), enums)
%}
%define DEFINE_ENUM_WITH_STRING_CONVERSIONS(name,enumerators)
%{
typedef std::tuple<name,std::string> name ## _entry;
struct name ## _helper {
std::vector<name ## _entry> list;
name ## _helper(const name value) {
list.push_back(std::make_tuple(value,ToString(value)));
}
name ## _helper operator()(const name value) {
list.push_back(std::make_tuple(value,ToString(value)));
return *this;
}
};
static const std::vector<name ## _entry> name ## _list() {
return name ## _helper enumerators . list;
}
%}
struct name ## _entry {
%extend {
const unsigned long value {
return std::get<0>(*$self);
}
const std::string& label {
return std::get<1>(*$self);
}
}
};
%template(name ## vec) std::vector<name ## _entry>;
const std::vector<name ## _entry> name ## _list();
// #2
%pythoncode %{
name = make_enum('name', {x.label: EnumValue(x.value, x.label) for x in name ## _list()})
%}
enum name;
%enddef
我在minimal.i中添加了一个函数来证明它确实有效:
%module minimal
%{
#include "minimal.h"
%}
%include "enum.i"
%include "minimal.h"
%inline %{
void foo(const my_enum& v) {
std::cerr << "GOT: " << v << "\n";
}
%}
最后用以下代码测试它:
import minimal
print minimal.my_enum
print minimal.my_enum.A
print minimal.my_enum.B
minimal.foo(minimal.my_enum.B)
你会高兴地看到它成功了,结果是:
<class 'minimal.my_enum'>
A
B
GOT: 1
如果你使用Python 3,还有一种可能更好的方式来表示枚举,但我暂时把这个留给读者作为练习。显然,你也可以根据自己的喜好调整Python 2.7的伪枚举。