SWIG и Boost :: вариант - PullRequest
       15

SWIG и Boost :: вариант

2 голосов
/ 16 октября 2019

Я пытаюсь обернуть проект c ++ в API Python, используя SWIG, и столкнулся с проблемой кода, имеющего следующий формат.

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 \ option \ option.hpp. (148): error: синтаксическая ошибка во входных данных (3).

Итак, я посмотрю на ошибку и ее строку, в которой есть макрос, который определен внутри другого заголовочного файла, в частности, «boost /mpl / aux_ / value_wknd.hpp ", поэтому я добавляю это в файл интерфейса с помощью% include, и теперь кажется, что SWIG.exe вылетает с ошибкой, сообщая

Нарушение доступа

Короче говоря, есть ли способ обернуть тип шаблона наддува :: варианта? К сожалению, это определение шаблона встроено в ядро ​​нашей библиотеки, и я не могу сейчас его изменить. Также, если это имеет значение, я нахожусь на компиляторе MSVC 2013.

Если невозможно обернуть тип шаблона напрямую, можно ли обойти это? Я читаю документацию SWIG, чтобы увидеть, есть ли какая-то магия шрифтов, которую можно применить, но я довольно новичок в SWIG в целом.

Ответы [ 2 ]

1 голос
/ 24 октября 2019

Если вы выбрали SWIG (что мне не ясно из вашего поста, поскольку вы сказали, что он довольно новичок в SWIG, поэтому я предполагаю, что это новый проект), тогда прекратите читать иигнорируйте этот ответ.

Но в случае, если используемая технология привязок еще не исправлена, и вам нужно только связать Python, никаких других языков, альтернативой является использование cppyy (http://cppyy.org, и fullОтказ от ответственности: я главный автор). При этом тип boost :: variable непосредственно доступен в Python, а затем вы можете сделать его более похожим на Python, написав код Python, а не SWI-код .i.

Пример (обратите внимание, что в cppyy есть колеса дляWindows на PyPI, но построенная с 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>
0 голосов
/ 23 октября 2019

Вы можете сделать это. Я долго думал о том, что на самом деле является самым красивым интерфейсом Python для boost::variant. Я пришел к выводу, что 99% времени пользователь Python даже не должен осознавать, что используется тип варианта - объединения и варианты - это в основном просто ограниченная типизация утки для 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())

Так как это на самом деле работает?

  • Мы по существу дублируем поддержку %template SWIG здесь. (Это задокументировано здесь как опция )
  • Большая часть тяжелой работы в моем файле выполняется с помощью макроса FOR_EACH variadic. Во многом это то же самое, что и мой предыдущий ответ на std::function, который сам был получен из нескольких старых ответов Stack Overflow и адаптирован для работы с препроцессором SWIG.
  • Используя макрос FOR_EACH, мы сообщаем SWIG обернуть один конструктор для каждого типа, который может содержать вариант. Это позволяет нам явно создавать варианты из кода Python с добавлением двух дополнительных конструкторов
  • Используя такие конструкторы, мы можем в значительной степени опираться на поддержку разрешения перегрузки SWIG. Поэтому, учитывая объект Python, мы можем просто положиться на SWIG, чтобы определить, как создать вариант из него. Это экономит нам кучу дополнительной работы и использует существующие карты типов для каждого типа в варианте.
  • Карта типов in в основном просто делегирует конструктору через слегка запутанный маршрут, потому что найти его на удивление сложнодругие функции в том же модуле программно. После того, как это делегирование произошло, мы используем обычное преобразование аргумента функции, чтобы просто передать временный вариант в функцию, как если бы это было то, что нам дали.
  • Мы также синтезируем набор дополнительных функций-членов, convert_typeN внутри которого просто вызывается boost::get<TYPE>(*this), где N и TYPE - это позиция каждого типа в списке типов вариантов.
  • В рамках таблицы типов out это позволяет нам искать функцию Python, используя which()чтобы определить, что вариант в настоящее время имеет. Затем мы получили в основном SWIG-сгенерированный код, используя существующие карты типов, чтобы превратить данный вариант в объект Python базового типа. Опять же, это экономит нам много усилий и делает все подключи и играй.
...