Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing Java Map<String, String> to C++ method using SWIG

Tags:

java

c++

swig

I have a method defined in C++:

std::map<std::string, std::string> validate(
                                   std::map<std::string, std::string> key, 
                                   std::map<std::string, std::string> value
                                   );

I want to consume this method in Java. So, I have to write a wrapper using Swig through which I will be able to pass Java Map as STL map to the c++ method.

Please let me know how should I define the .i file for swig to make this work.

like image 995
AKG Avatar asked May 30 '12 08:05

AKG


2 Answers

In order to do this you'll need to tell SWIG to use java.util.Map for the input argument, using %typemap(jstype). You'll also need to supply some code to convert from the Java map type to the C++ std::map type, which SWIG will inject at appropriate points. I've put together a small (compiled, but untested) example to illustrate this:

%module test

%include <std_map.i>
%include <std_string.i>

%typemap(jstype) std::map<std::string, std::string> "java.util.Map<String,String>"
%typemap(javain,pre="    MapType temp$javainput = $javaclassname.convertMap($javainput);",pgcppname="temp$javainput") std::map<std::string, std::string> "$javaclassname.getCPtr(temp$javainput)"
%typemap(javacode) std::map<std::string, std::string> %{
  static $javaclassname convertMap(java.util.Map<String,String> in) {
    $javaclassname out = new $javaclassname();
    for (java.util.Map.Entry<String, String> entry : in.entrySet()) {
      out.set(entry.getKey(), entry.getValue());      
    }
    return out;
  }    
%}

%template(MapType) std::map<std::string, std::string>;

void foo(std::map<std::string, std::string>);

The pgcppname part is making sure that the std::map we pass in doesn't get garbage collected too early. See this example in the SWIG documentation for more details on how that works.

To support returning from std::map from C++ to Java takes quite a bit more work, but is possible. java.util.Map is an interface so we need to adapt the default wrapping of std::map to meet that interface. In practice it's easier to use java.util.AbstractMap and inherit from that although I ended up overriding most of the functions in that anyway. This whole solution is analogous to my answer for std::vector.

There are quite a few moving parts in my final version. I'll present it complete here, with annotated notes:

%module test
%{
#include <cassert>
#include <iostream>
%}

%include <std_map.i>

// 1.
%rename (size_impl) std::map<std::string,std::string>::size;
%rename (isEmpty) std::map<std::string,std::string>::empty;
%include <std_string.i>

%typemap(jstype) std::map<std::string, std::string> "java.util.Map<String,String>"
%typemap(javain,pre="    MapType temp$javainput = $javaclassname.convertMap($javainput);",pgcppname="temp$javainput") std::map<std::string, std::string> "$javaclassname.getCPtr(temp$javainput)"
%typemap(javacode) std::map<std::string, std::string> %{
  static $javaclassname convertMap(Map<String,String> in) {
    // 2.
    if (in instanceof $javaclassname) {
      return ($javaclassname)in;
    }

    $javaclassname out = new $javaclassname();
    for (Map.Entry<String, String> entry : in.entrySet()) {
      out.set(entry.getKey(), entry.getValue());
    }
    return out;
  }

  // 3.
  public Set<Map.Entry<String,String>> entrySet() {
    HashSet<Map.Entry<String,String>> ret = new HashSet<Map.Entry<String,String>>(size());
    String array[] = new String[size()];
    all_keys(array);
    for (String key: array) {
      ret.add(new MapTypeEntry(key,this));
    }
    return ret;
  }

  public Collection<String> values() {
    String array[] = new String[size()];
    all_values(array);
    return new ArrayList<String>(Arrays.asList(array));
  }

  public Set<String> keySet() {
    String array[] = new String[size()];
    all_keys(array);
    return new HashSet<String>(Arrays.asList(array));
  }

  // 4.
  public String remove(Object key) {
    final String ret = get(key);
    remove((String)key);
    return ret;
  }

  public String put(String key, String value) {
    final String ret = has_key(key) ? get(key) : null;
    set(key, value);
    return ret;
  }

  // 5.
  public int size() {
    return (int)size_impl();
  }
%}

// 6.
%typemap(javaimports) std::map<std::string, std::string> "import java.util.*;";
// 7.
%typemap(javabase) std::map<std::string, std::string> "AbstractMap<String, String>";

// 8.
%{
template <typename K, typename V>
struct map_entry {
  const K key;
  map_entry(const K& key, std::map<K,V> *owner) : key(key), m(owner) {
  }
  std::map<K,V> * const m;
};
%}

// 9.
template <typename K, typename V>
struct map_entry {
  const K key;
  %extend {
    V getValue() const {
      return (*$self->m)[$self->key];
    }

    V setValue(const V& n) const {
      const V old = (*$self->m)[$self->key];
      (*$self->m)[$self->key] = n;
      return old;
    }
  }
  map_entry(const K& key, std::map<K,V> *owner);
};

// 10.
%typemap(javainterfaces) map_entry<std::string, std::string> "java.util.Map.Entry<String,String>";
// 11.
%typemap(in,numinputs=0) JNIEnv * %{
  $1 = jenv;
%}

// 12.
%extend std::map<std::string, std::string> {
  void all_values(jobjectArray values, JNIEnv *jenv) const {
    assert((jsize)$self->size() == jenv->GetArrayLength(values));
    jsize pos = 0;
    for (std::map<std::string, std::string>::const_iterator it = $self->begin();
         it != $self->end();
         ++it) {
       jenv->SetObjectArrayElement(values, pos++, jenv->NewStringUTF(it->second.c_str()));
    }
  }

  void all_keys(jobjectArray keys, JNIEnv *jenv) const {
    assert((jsize)$self->size() == jenv->GetArrayLength(keys));
    jsize pos = 0;
    for (std::map<std::string, std::string>::const_iterator it = $self->begin();
         it != $self->end();
         ++it) {
       jenv->SetObjectArrayElement(keys, pos++, jenv->NewStringUTF(it->first.c_str()));
    }
  }
}

%template(MapType) std::map<std::string, std::string>;
%template(MapTypeEntry) map_entry<std::string, std::string>;

// 13.
%inline %{
  std::map<std::string, std::string> foo(std::map<std::string, std::string> in) {
    for (std::map<std::string, std::string>::const_iterator it = in.begin();
         it != in.end(); ++it) {
      std::cout << it->first << ": " << it->second << "\n";
    }

    return std::map<std::string, std::string>(in);
  }
%}
  1. std_map.i isn't meant to implement any interface/abstract class. We need to rename some of what exposes in order to do so.
  2. Since we make our type implement Map (via AbstractMap), it's silly to end up converting from MapType -> MapType when that's literally just a copy operation. The convertMap method now checks for this case as an optimisation.
  3. EntrySet is the main requirement of AbstractMap. We have defined (later on) MapTypeEntry to implement the Map.Entry interface for us. This uses some more code inside %extend later on to efficiently get enumerate all the keys as an array. Note that this is not thread-safe, if we change the map whilst this enumeration is in progress weird bad things will happen and possibly not get detected.
  4. remove is one of the methods we have to implement in order to be mutable. Both remove and put have to return the old value, so there's a bit of extra Java here to make that happen since C++ maps don't do that.
  5. Even size() isn't compatible because of the long/int conversion that's needed. Really we should detect the loss of precision somewhere for very large maps and do something sane for overflows.
  6. I got bored of typing java.util.Map everywhere so this makes the generated SWIG code have the import needed.
  7. This sets up the MapType to inherit from AbstractMap, so that we proxy and meet the requirements of a Java map rather than doing an extra copy to convert back.
  8. The C++ definition of the class that will act as our entry. This just has a key and then a pointer to the map it is owned by. The value isn't stored in the Entry object itself and is always referred back to the underlying map for. This type is immutable too, we can't change the owning map or the key ever.
  9. This is what SWIG sees. We supply an extra get/setValue function that calls back to the map it originates from. The pointer to the owning map isn't exposed since we're not required to do so and it's really just an implementation detail.
  10. The java.util.Map.Entry<String,String>.
  11. This is a trick that auto-populates the jenv argument of some code inside %extend that we need to make some JNI calls inside that code.
  12. These two methods inside %extend place all of the keys and values respectively into an output array. The array is expected to be the right size when passed in. There's an assert to validate this, but really it should be an exception. Both of these are internal implementation details that probably ought to be private anyway. They get used by all of the functions that require bulk access to keys/values.
  13. An actual implementation of foo to sanity check my code.

Memory management happens for free here since it remains owned by the C++ code. (So you've still got to decide how to manage memory for a C++ container, but that's nothing new). Since the object that gets returned to Java is just a wrapper around a C++ map the elements of the container don't have to outlive it. Here they're also Strings which are special in that they get returned as new objects, if they were smart pointers using SWIG's std::shared_ptr support then everything would work as expected. The only case that would be tricky is maps of pointers to objects. In that case it's the responsibility of the Java programmer to keep the map and its contents alive at least as long as any Java proxies that get returned.

Finally I wrote the following Java to test it:

import java.util.Map;

public class run {
  public static void main(String[] argv) {
    System.loadLibrary("test");

    Map<String,String> m = new MapType();
    m.put("key1", "value1");
    System.out.println(m);
    m = test.foo(m);
    System.out.println(m);
  }
}

Which I compiled and ran as:

swig2.0 -Wall -java -c++ test.i
gcc -Wall -Wextra -shared -o libtest.so -I/usr/lib/jvm/default-java/include -I/usr/lib/jvm/default-java/include/linux test_wrap.cxx
javac run.java
LD_LIBRARY_PATH=. java run
{key1=value1}
key1: value1
{key1=value1}
like image 92
Flexo Avatar answered Oct 18 '22 15:10

Flexo


Or we could do it entirely in Java (assuming your function declaration can be found in the header file MapTest.h), with the help of JavaCPP:

import com.googlecode.javacpp.*;
import com.googlecode.javacpp.annotation.*;

@Platform(include={"<string>", "<map>", "MapTest.h"})
public class MapTest {
    static { Loader.load(); }

    @Name("std::map<std::string, std::string>")
    public static class StringStringMap extends Pointer {
        static { Loader.load(); }
        public StringStringMap() { allocate(); }
        public StringStringMap(Pointer p) { super(p); }
        private native void allocate();

        @Index @ByRef public native String get(String x);
        public native StringStringMap put(String x, String y);
    }

    public static native @ByVal StringStringMap validate(
            @ByVal StringStringMap key, @ByVal StringStringMap value);

    public static void main(String[] args) {
        StringStringMap m = new StringStringMap();
        m.put("foo", "bar");
        System.out.println(m.get("foo"));
    }
}

I find this easier, clearer, than SWIG...

like image 26
Samuel Audet Avatar answered Oct 18 '22 14:10

Samuel Audet