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);
}
%}
- 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.
- 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.
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.
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.
- 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.
- I got bored of typing
java.util.Map
everywhere so this makes the generated SWIG code have the import needed.
- 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.
- 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.
- 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.
- The
java.util.Map.Entry<String,String>
.
- 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.
- 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.
- 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}