2

Disclaimer: yes, I do know about boost::python::map_indexing_suite.

Task: I have a C++ class which I want to wrap with Boost.Python. Its constructor takes a std::map argument. Here is the C++ header:

// myclass.hh
typedef std::map<int, float> mymap_t;

class MyClass {
  public:
  explicit MyClass(const mymap_t& m);
  // ...
};
// ...

Here is the Boost.Python wrapper (essential parts only):

// myclasswrapper.cc
#include "mymap.hh"
#include "boost/python.hpp"
#include "boost/python/suite/indexing/map_indexing_suite.hpp"

namespace bpy = boost::python;

// wrapping mymap_t
bpy::class_<mymap_t>("MyMap")
     .def(bpy::map_indexing_suite<mymap_t>())
    ;

// wrapping MyClass
bpy::class_<MyClass>("MyClass", "My example class",
  bpy::init<mymap_t>()   // ??? what to put here?
)
  // .def(...method wrappers...)
;

This compiles. However, I cannot create the mapped MyClass object from the Python side because I don't know what to pass as argument to the constructor. Dictionaries do not get converted to std::map-s automatically:

# python test
myclass = MyClass({1:3.14, 5:42.03})

the interpreter complains (rightly so):

Boost.Python.ArgumentError: Python argument types in
    MyClass.__init__(MyClass, dict)
did not match C++ signature:
    __init__(_object*, std::__1::map<int, float, ...

and MyMap on the Python side cannot be initialised with a dictionary either.

Having googled away the best part of a day, I could find only examples for "normal" methods taking std::map arguments that are mapped with .def(...). And in .def(...) you do not have to specify explicitly the arguments of the mapped method, they are magically discovered. With constructors you must use boost::python::init<...>(), or at least that is what I understood from the docs.

Questions:

  1. Shall I add something to the MyMap wrapper to help map_indexing_suite convert from a Python dictionary?
  2. Shall I use a different template argument in boost::python::init<...> in the MyClass wrapper?
  3. Any other ideas...?

Note: I have also seen this accepted answer at SO, then I scrolled down and read the comment by @YvesgereY:

"For the record, map_indexing_suite solution doesn't work, since no implicit "dict->std::map" from_python converter will be applied."

And I lost faith :-)

Community
  • 1
  • 1
András Aszódi
  • 8,948
  • 5
  • 48
  • 51
  • Make a standalone function that will construct the object from a `boost::python::dict`. See [this answer](http://stackoverflow.com/a/18793953/3962537) for inspiration. – Dan Mašek Mar 22 '17 at 17:45
  • 1
    @DanMašek : many thanks, your comment was very useful, it pointed me towards converters. I now answered my own question, see below. – András Aszódi Mar 23 '17 at 16:33

1 Answers1

4

I have found a nice solution: added a template that can convert a Python dictionary to std::map. The logic is based on this extremely useful primer, with slight modifications mostly obtained from this source file and some additional comments.

Below is the template definition:

// dict2map.hh
#include "boost/python.hpp"
namespace bpy = boost::python;

/// This template encapsulates the conversion machinery.
template<typename key_t, typename val_t>
struct Dict2Map {

    /// The type of the map we convert the Python dict into
    typedef std::map<key_t, val_t> map_t;

    /// constructor
    /// registers the converter with the Boost.Python runtime
    Dict2Map() {
        bpy::converter::registry::push_back(
            &convertible,
            &construct,
            bpy::type_id<map_t>()
#ifdef BOOST_PYTHON_SUPPORTS_PY_SIGNATURES
            , &bpy::converter::wrap_pytype<&PyDict_Type>::get_pytype
#endif
        );
    }

    /// Check if conversion is possible
    static void* convertible(PyObject* objptr) {
        return PyDict_Check(objptr)? objptr: nullptr;
    }

    /// Perform the conversion
    static void construct(
        PyObject* objptr,
        bpy::converter::rvalue_from_python_stage1_data* data
    ) {
        // convert the PyObject pointed to by `objptr` to a bpy::dict
        bpy::handle<> objhandle{ bpy::borrowed(objptr) };   // "smart ptr"
        bpy::dict d{ objhandle };

        // get a pointer to memory into which we construct the map
        // this is provided by the Python runtime
        void* storage = 
            reinterpret_cast<
                bpy::converter::rvalue_from_python_storage<map_t>*
            >(data)->storage.bytes;

        // placement-new allocate the result
        new(storage) map_t{};

        // iterate over the dictionary `d`, fill up the map `m`
        map_t& m{ *(static_cast<map_t *>(storage)) };
        bpy::list keys{ d.keys() };
        int keycount{ static_cast<int>(bpy::len(keys)) };
        for (int i = 0; i < keycount; ++i) {
            // get the key
            bpy::object keyobj{ keys[i] };
            bpy::extract<key_t> keyproxy{ keyobj };
            if (! keyproxy.check()) {
                PyErr_SetString(PyExc_KeyError, "Bad key type");
                bpy::throw_error_already_set();
            }
            key_t key = keyproxy();

            // get the corresponding value
            bpy::object valobj{ d[keyobj] };
            bpy::extract<val_t> valproxy{ valobj };
            if (! valproxy.check()) {
                PyErr_SetString(PyExc_ValueError, "Bad value type");
                bpy::throw_error_already_set();
            }
            val_t val = valproxy();
            m[key] = val;
        }

        // remember the location for later
        data->convertible = storage;
    }
};

In order to use it, you must create a Dict2Map instance so that its constructor gets invoked. One possible way of doing it is to create a static Dict2Map<key_t, val_t> variable in the source file where you define the Python wrappers. Using my example:

// myclasswrapper.cc
#include "mymap.hh"
#include "dict2map.hh"

// register the converter at runtime
static Dict2Map<char, double> reg{};

#include "boost/python.hpp" // not really necessary
namespace bpy = boost::python;

// wrapping MyClass
bpy::class_<MyClass>("MyClass", "My example class",
  bpy::init<mymap_t>()
)
  // .def(...method wrappers...)
;

Now it is possible to create MyClass objects on the Python side like this:

myclass = MyClass({"foo":1, "bar":2})

Edit: Python lists can be converted to C++ std::vector-s in an analogous manner. Here is the corresponding template:

template<typename elem_t>
struct List2Vec {

    /// The type of the vector we convert the Python list into
    typedef std::vector<elem_t> vec_t;

    /// constructor
    /// registers the converter
    List2Vec() {
        bpy::converter::registry::push_back(
            &convertible,
            &construct,
            bpy::type_id<vec_t>()
#ifdef BOOST_PYTHON_SUPPORTS_PY_SIGNATURES
            , &bpy::converter::wrap_pytype<&PyList_Type>::get_pytype
#endif
        );
    }

    /// Check if conversion is possible
    static void* convertible(PyObject* objptr) {
        return PyList_Check(objptr)? objptr: nullptr;
    }

    /// Perform the conversion
    static void construct(
        PyObject* objptr,
        bpy::converter::rvalue_from_python_stage1_data* data
    ) {
        // convert the PyObject pointed to by `objptr` to a bpy::list
        bpy::handle<> objhandle{ bpy::borrowed(objptr) };   // "smart ptr"
        bpy::list lst{ objhandle };

        // get a pointer to memory into which we construct the vector
        // this is provided by the Python side somehow
        void* storage = 
            reinterpret_cast<
                bpy::converter::rvalue_from_python_storage<vec_t>*
            >(data)->storage.bytes;

        // placement-new allocate the result
        new(storage) vec_t{};

        // iterate over the list `lst`, fill up the vector `vec`
        int elemcount{ static_cast<int>(bpy::len(lst)) };
        vec_t& vec{ *(static_cast<vec_t *>(storage)) };
        for (int i = 0; i < elemcount; ++i) {
            // get the element
            bpy::object elemobj{ lst[i] };
            bpy::extract<elem_t> elemproxy{ elemobj };
            if (! elemproxy.check()) {
                PyErr_SetString(PyExc_ValueError, "Bad element type");
                bpy::throw_error_already_set();
            }
            elem_t elem = elemproxy();
            vec.push_back(elem);
        }

        // remember the location for later
        data->convertible = storage;
    }
};
András Aszódi
  • 8,948
  • 5
  • 48
  • 51