Отправка функции Python в качестве аргумента Boost.Function

Все усложняется в моем мире попыток соединить код Python с моим C++.

По сути, я хочу иметь возможность назначать функцию обратного вызова, которая будет использоваться после того, как HTTP-вызов получит ответ, и я хочу иметь возможность сделать это либо из C++, либо из Python.

Другими словами, я хочу иметь возможность вызывать это из C++:

http.get_asyc("www.google.ca", [&](int a) { std::cout << "response recieved: " << a << std::endl; });

и это из Python:

def f(r):
    print str.format('response recieved: {}', r)

http.get_async('www.google.ca', f)

Я установил демонстрацию на Coliru, которая точно показывает, чего я пытаюсь достичь . Вот код и ошибка, которую я получаю:

C++

#include <boost/python.hpp>
#include <boost/function.hpp>

struct http_manager
{
    void get_async(std::string url, boost::function<void(int)> on_response)
    {
        if (on_response)
        {
            on_response(42);
        }
    }
} http;

BOOST_PYTHON_MODULE(example)
{
    boost::python::class_<http_manager>("HttpManager", boost::python::no_init)
        .def("get_async", &http_manager::get_async);

    boost::python::scope().attr("http") = boost::ref(http);
}

Питон

import example
def f(r):
    print r
example.http.get_async('www.google.ca', f)

Ошибка

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
Boost.Python.ArgumentError: Python argument types in
    HttpManager.get_async(HttpManager, str, function)
did not match C++ signature:
    get_async(http_manager {lvalue}, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, boost::function<void (int)>)

Я не уверен, почему function не преобразуется в boost::function автоматически.

Я уже задавал смутно похожий вопрос на SO и получил потрясающий ответ. Мне также интересно, можно ли применить аналогичный метод в приведенном там ответе и к этому варианту использования.

Большое спасибо за любую поддержку!


person Colin Basnett    schedule 17.03.2016    source источник


Ответы (2)


Когда вызывается функция, которая была предоставлена ​​через Boost.Python, Boost.Python запрашивает свой реестр, чтобы найти подходящий преобразователь from-Python для каждого из аргументов вызывающего объекта на основе желаемого типа C++. Если найден преобразователь, который знает, как преобразовать объект Python в объект C++, то он будет использовать преобразователь для создания объекта C++. Если подходящих конвертеров не найдено, Boost.Python вызовет исключение ArgumentError.

Преобразователи from-Python зарегистрированы:

  • автоматически для типов, поддерживаемых Boost.Python, таких как int и std::string
  • неявно для типов, предоставляемых boost::python::class<T>. По умолчанию результирующий класс Python будет содержать встроенный экземпляр объекта T C++ и зарегистрировать преобразователи в Python и из Python для класса Python и типа T, используя встроенный экземпляр.
  • явно через boost::python::converter::registry::push_back()

Этапы проверки конвертируемости и создания объекта состоят из двух отдельных этапов. Поскольку конвертер from-Python не был зарегистрирован для boost::function<void(int)>, Boost.Python вызовет исключение ArgumentError. Boost.Python не будет пытаться создать объект boost::function<void(int)>, несмотря на то, что boost::function<void(int)> можно построить из объекта boost::python::object.


Чтобы решить эту проблему, рассмотрите возможность использования функции прокладки, чтобы отложить построение boost::function<void(int)> до тех пор, пока boost::python::object не пройдет через слой Boost.Python:

void http_manager_get_async_aux(
  http_manager& self, std::string url, boost::python::object on_response)
{
  return self.get_async(url, on_response);
}

...

BOOST_PYTHON_MODULE(example)
{
  namespace python = boost::python;
  python::class_<http_manager>("HttpManager", python::no_init)
    .def("get_async", &http_manager_get_async_aux);

  ...
}

Вот полный пример, демонстрирующий этот подход:

#include <boost/python.hpp>
#include <boost/function.hpp>

struct http_manager
{
  void get_async(std::string url, boost::function<void(int)> on_response)
  {
    if (on_response)
    {
      on_response(42);
    }
  }
} http;

void http_manager_get_async_aux(
  http_manager& self, std::string url, boost::python::object on_response)
{
  return self.get_async(url, on_response);
}

BOOST_PYTHON_MODULE(example)
{
  namespace python = boost::python;
  python::class_<http_manager>("HttpManager", python::no_init)
    .def("get_async", &http_manager_get_async_aux);

  python::scope().attr("http") = boost::ref(http);
}

Интерактивное использование:

>>> import example
>>> result = 0
>>> def f(r):
...     global result
...     result = r
...
>>> assert(result == 0)
>>> example.http.get_async('www.google.com', f)
>>> assert(result == 42)
>>> try:
...     example.http.get_async('www.google.com', 42)
...     assert(False)
... except TypeError:
...    pass
...

Альтернативный подход — явно зарегистрировать конвертер from-Python для boost::function<void(int)>. Преимущество этого заключается в том, что все функции, доступные через Boost.Python, могут использовать преобразователь (например, не нужно писать прокладку для каждой функции). Однако преобразование должно быть зарегистрировано для каждого типа C++. Вот пример, демонстрирующий явную регистрацию пользовательского преобразователя для boost::function<void(int)> и boost::function<void(std::string)>:

#include <boost/python.hpp>
#include <boost/function.hpp>

struct http_manager
{
  void get_async(std::string url, boost::function<void(int)> on_response)
  {
    if (on_response)
    {
      on_response(42);
    }
  }
} http;

/// @brief Type that allows for registration of conversions from
///        python iterable types.
struct function_converter
{
  /// @note Registers converter from a python callable type to the
  ///       provided type.
  template <typename FunctionSig>
  function_converter&
  from_python()
  {
    boost::python::converter::registry::push_back(
      &function_converter::convertible,
      &function_converter::construct<FunctionSig>,
      boost::python::type_id<boost::function<FunctionSig>>());

    // Support chaining.
    return *this;
  }

  /// @brief Check if PyObject is callable.
  static void* convertible(PyObject* object)
  {
    return PyCallable_Check(object) ? object : NULL;
  }

  /// @brief Convert callable PyObject to a C++ boost::function.
  template <typename FunctionSig>
  static void construct(
    PyObject* object,
    boost::python::converter::rvalue_from_python_stage1_data* data)
  {
    namespace python = boost::python;
    // Object is a borrowed reference, so create a handle indicting it is
    // borrowed for proper reference counting.
    python::handle<> handle(python::borrowed(object));

    // Obtain a handle to the memory block that the converter has allocated
    // for the C++ type.
    typedef boost::function<FunctionSig> functor_type;
    typedef python::converter::rvalue_from_python_storage<functor_type>
                                                                storage_type;
    void* storage = reinterpret_cast<storage_type*>(data)->storage.bytes;

    // Allocate the C++ type into the converter's memory block, and assign
    // its handle to the converter's convertible variable.
    new (storage) functor_type(python::object(handle));
    data->convertible = storage;
  }
};

BOOST_PYTHON_MODULE(example)
{
  namespace python = boost::python;
  python::class_<http_manager>("HttpManager", python::no_init)
    .def("get_async", &http_manager::get_async);

  python::scope().attr("http") = boost::ref(http);

  // Enable conversions for boost::function.
  function_converter()
    .from_python<void(int)>()
    // Chaining is supported, so the following would enable
    // another conversion.
    .from_python<void(std::string)>()
    ;
}
person Tanner Sansbury    schedule 17.03.2016

Одним из решений является добавление функции перегрузки:

void get_async(std::string url, boost::python::object obj)
{
    if (PyCallable_Check(obj.ptr()))
        get_async(url, static_cast<boost::function<void(int)>>(obj));
}

Затем выставьте только эту конкретную перегрузку:

.def("get_async", static_cast<void (http_manager::*)(std::string, boost::python::object)>(&http_manager::get_async))

Или, если вы не хотите загрязнять свой основной класс материалами Python, вы можете создать класс-оболочку. Тогда все выглядит намного чище:

struct http_manager_wrapper : http_manager
{
    void get_async(std::string url, boost::python::object obj)
    {
        if (PyCallable_Check(obj.ptr()))
            http_manager::get_async(url, obj);
    }

} http_wrapper;

BOOST_PYTHON_MODULE(example)
{
    boost::python::class_<http_manager_wrapper>("HttpManager", boost::python::no_init)
        .def("get_async", &http_manager_wrapper::get_async);

    boost::python::scope().attr("http") = boost::ref(http_wrapper);
}

Обновление: Другой вариант — использовать конвертер функций python, вызываемый для повышения. Это решит проблему синглтона и не потребует изменений в основном классе.

struct http_manager
{
    void get_async(std::string url, boost::function<void(int)> on_response)
    {
        if (on_response)
        {
            on_response(42);
        }
    }
} http;

struct BoostFunc_from_Python_Callable
{
    BoostFunc_from_Python_Callable()
    {
        boost::python::converter::registry::push_back(&convertible, &construct, boost::python::type_id< boost::function< void(int) > >());
    }

    static void* convertible(PyObject* obj_ptr)
    {
        if (!PyCallable_Check(obj_ptr)) 
            return 0;
        return obj_ptr;
    }

    static void construct(PyObject* obj_ptr, boost::python::converter::rvalue_from_python_stage1_data* data)
    {
        boost::python::object callable(boost::python::handle<>(boost::python::borrowed(obj_ptr)));
        void* storage = ((boost::python::converter::rvalue_from_python_storage< boost::function< void(int) > >*) data)->storage.bytes;
        new (storage)boost::function< void(int) >(callable);
        data->convertible = storage;
    }
};

BOOST_PYTHON_MODULE(example)
{
    // Register function converter
    BoostFunc_from_Python_Callable();

    boost::python::class_<http_manager>("HttpManager", boost::python::no_init)
        .def("get_async", &http_manager::get_async);

    boost::python::scope().attr("http") = boost::ref(http);
}
person doqtor    schedule 17.03.2016
comment
К сожалению, в этом случае объект http доступен глобально и, по сути, является синглтоном, поэтому создание оболочки не сработает, и я хотел бы избежать загрязнения класса деталями реализации Python. - person Colin Basnett; 17.03.2016
comment
@ColinBasnett, мое последнее обновление должно решить проблемы, о которых вы упомянули. - person doqtor; 18.03.2016