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.