使用Boost预处理器的SWIG预处理宏

4 投票
1 回答
1146 浏览
提问于 2025-04-18 16:30

我正在使用这里推荐的带有 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 个回答

3

如果你想让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的伪枚举。

撰写回答