How to create a swig typemap to convert a unordered map of string to std::unique_ptr<Foo>'s to a Python dictionary?

189 views Asked by At

I have a map type in C++ that I'm wrapping and exposing to Python using swig. The map is of type std::unordered_map<std::string, std::unique_ptr<Foo>>, where Foo is another one of my objects already exposed to Python with swig. I need help creating a typemap so that this custom map type becomes a Python dictionary on the Python end. The complication of course is the unique_ptr.

The code for this example is avialable here and is built using cmake (see commands below). You will of course need to replace -DPython_ROOT_DIR=C:\Miniconda3\envs\py39 with the corresponding location to your own Python and -DSWIG_EXECUTABLE=D:/swigwin-4.0.2/swig.exe with the corresponding location to the swig executable on your system.

git clone https://github.com/CiaranWelsh/SwigExample.git
cd SwigExample
mkdir build
cd build
cmake -DPython_ROOT_DIR=C:\Miniconda3\envs\py39 -DSWIG_EXECUTABLE=D:/swigwin-4.0.2/swig.exe -DCMAKE_BUILD_TYPE="Release" ..
cmake --build . --target swig_example --config Release

The C++ library code that will be wrapped with swig is:

// src/SwigExample.h
#ifndef SWIGEXAMPLE_SWIGEXAMPLE_H
#define SWIGEXAMPLE_SWIGEXAMPLE_H

#include <vector>
#include <string>
#include <unordered_map>
#include <stdexcept>
#include <memory>

class Foo {
public:
    Foo(int x_)
        : x(x_){}
    int x;
};


class SpecialFooMap{
public:
    SpecialFooMap() = default;

    SpecialFooMap(const std::vector<std::string>& labels, const std::vector<int>& values){
        insertFoos(labels, values);
    }

    void insertFoos(const std::vector<std::string>& labels, const std::vector<int>& values){
        if (labels.size() != values.size())
            throw std::invalid_argument("labels and values are not the same size");
        for (int i=0; i<labels.size(); i++){
            map_[labels[i]] = std::make_unique<Foo>(values[i]);
        }
    }
    std::unordered_map<std::string, std::unique_ptr<Foo>> map_;
};

#endif //SWIGEXAMPLE_SWIGEXAMPLE_H

And the swig interface file

// SwigExample.i
%module swig_example

%{

#define SWIG_FILE_WITH_INIT
#include "SwigExample.h"

%}

%include "std_string.i"
%include "std_unordered_map.i"
%include "std_vector.i"

%template(StringVector) std::vector<std::string>;
%template(IntVector) std::vector<int>;

%typemap(out) SpecialFooMap* {
    // marker for SpecialFooMap
    $result = PyDict_New();
    if (!$result){
        std::string err = "Could not create dict object";
        PyErr_SetString(PyExc_ValueError, err.c_str());
    }
    for (auto& [fooName, fooUniquePtr]: $1->map_){
        // release ownership from the unique_ptr.
        Foo* fooPtr = fooUniquePtr.release();
        // give it to swig
        PyObject* swigOwnedPtr = SWIG_NewPointerObj(
                SWIG_as_voidptr(fooPtr),
                $descriptor(Foo*),
                /*SWIG_POINTER_NEW |  SWIG_POINTER_NOSHADOW*/
                SWIG_POINTER_NEW |  0
        );
        if (!swigOwnedPtr){
            PyErr_SetString(PyExc_ValueError, "Could not create a swig wrapped RoadRunner object from "
                                        "a std::unique_ptr<RoadRunner>");
        }
        int failed = PyDict_SetItem($result, PyUnicode_FromString(fooName.c_str()), swigOwnedPtr);
        if (failed){
            PyErr_SetString(PyExc_ValueError, "Could not create item in dict");
        }
    }
}

%ignore SpecialFooMap::map_;
%include "SwigExample.h"

%extend Foo {
    %pythoncode %{
        def __str__(self):
            return f"<class '{type(self)}'>"
     %}
}

As you can see, my strategy is for every item in the hash map, to release the pointer from the std::unique_ptr<Foo> and "give" it to swig.

Running the code:

f = swig_example.Foo(3)
print(f) # outputs the expected: <class '<class 'swig_example.Foo'>'>
print(type(f)) # outputs the expected: <class 'swig_example.Foo'>

But

m = swig_example.SpecialFooMap(["first", "second"], [1, 2])
print(m) 

Outputs:

<swig_example.SpecialFooMap; proxy of {'first': <Swig Object of type 'Foo *' at 0x00000272A12E7C90>, 'second': <Swig Object of type 'Foo *' at 0x00000272A12E7B70>} >

and

print(type(m))

Outputs

<class 'swig_example.SpecialFooMap'>

What I want is for type(m) to be a Python dict so that isinstance(m, dict) evaluates to true.

0

There are 0 answers