pythonc++swigtypemapsswig-typemap

How to produce a Python dictionary from a C++ unordered map using SWIG?


I'm trying to wrap an unordered map in a python dictionary using swig:

// UsingAnUnorderedMap.h
#ifndef LEARNSWIG_USINGUNORDEREDMAP_H
#define LEARNSWIG_USINGUNORDEREDMAP_H

#include <unordered_map>
#include <string>


std::unordered_map<std::string, int> makeStringToIntMap(){
    return std::unordered_map<std::string, int>{
        {"first", 4},
        {"second", 5},
    };
}

#endif //LEARNSWIG_USINGUNORDEREDMAP_H
//UsingAnUnorderedMap.i
%module UsingUnorderedMap

%{
#include "UsingUnorderedMap.h"
#include <iostream>
#include <unordered_map>
%}

%include "std_string.i"
%include "std_pair.i"
%include "std_unordered_map.i"

%template(StringToIntMap) std::unordered_map<std::string,int>;


%include "UsingUnorderedMap.h"
%typemap(out) StringToIntMap {
    PyDictObject* dict = PyDict_New($input);
    for (auto &item: StringToIntMap){
        PyDict_SetItem(dict, PyUnicode_FromString(item.first), item.second);
    }
    $result = dict;
}
# testUsingAnUnorderedMap.py
import sys

sys.path += [
    r"D:\LearnSwig\install-msvc\UsingUnorderedMap"
]

import UsingUnorderedMap

print(type(UsingUnorderedMap.makeStringToIntMap())) 

This produces

<class 'UsingUnorderedMap.StringToIntMap'>

i.e. it just ignores the typemap. Technically the StringToIntMap behaves pretty much the same as a Python dict - at far as I can tell, but I think there's confort for Python users in Python dictionaries and so it would be better if this were a straight up dictionary. Does anybody have any pointers on how to achieve this?

For convenience, you can build this code using the following CMake code. Note that I build this using the command -DSWIG_EXECUTABLE=/path/to/swig.exe.

# CMakeLists.txt
set(Python_ROOT_DIR "C:/Miniconda3")
find_package(Python COMPONENTS Interpreter Development NumPy)
        message("Python_EXECUTABLE ${Python_EXECUTABLE}")

find_package(SWIG 4.0.0 REQUIRED
        COMPONENTS python
        )

include(UseSWIG)

set_property(SOURCE UsingUnorderedMap.i PROPERTY CPLUSPLUS ON)

swig_add_library(UsingUnorderedMap
        LANGUAGE python
        TYPE MODULE
        SOURCES UsingUnorderedMap.i UsingUnorderedMap)

set_target_properties(UsingUnorderedMap
        PROPERTIES LANGUAGE CXX)

target_include_directories(UsingUnorderedMap PUBLIC
        ${Python_INCLUDE_DIRS}
        ${CMAKE_CURRENT_SOURCE_DIR}
        )
target_link_libraries(UsingUnorderedMap PUBLIC ${Python_LIBRARIES})

install(TARGETS UsingUnorderedMap DESTINATION UsingUnorderedMap)
install(FILES
        ${CMAKE_CURRENT_BINARY_DIR}/UsingUnorderedMap.py
        ${CMAKE_CURRENT_SOURCE_DIR}/testUsingUnorderedMap.py
        DESTINATION UsingUnorderedMap)

Solution

  • Here goes a small example showing conversion of std::unordered_map to a python dictionary

    %module dictmap
    %{
      #include "test.h"
    %}
    
    %typemap(out) std::unordered_map<std::string, std::string> (PyObject* obj) %{
      obj = PyDict_New();
      for (const auto& n : $1) {
        PyObject* strA = PyUnicode_FromString(n.first.c_str());
        PyObject* strB = PyUnicode_FromString(n.second.c_str());
        PyDict_SetItem(obj, strA, strB);
        Py_XDECREF(strA);
        Py_XDECREF(strB);
      }
      $result = SWIG_Python_AppendOutput($result, obj);
    %}
    %include "test.h"
    

    Small inline function

    #pragma once
    #include <string>
    #include <unordered_map>
    
    std::unordered_map<std::string, std::string> makeStringMap() {
      return std::unordered_map<std::string, std::string> {
        {"first", "hello"},
        {"second", "world"},
      };
    }
    

    I hope that you can see what you have made wrong. You are returning an empty dictionary created using an empty $input. I am not sure of whether you need to defined a PyObject argument, but I do this always in case it is needed together with another typemap.