Вы можете сделать это. Я долго думал о том, что на самом деле является самым красивым интерфейсом 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 базового типа. Опять же, это экономит нам много усилий и делает все подключи и играй.