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.
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);
}
%}
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.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.java.util.Map
everywhere so this makes the generated SWIG code have the import needed.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.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.java.util.Map.Entry<String,String>
.jenv
argument of some code inside %extend
that we need to make some JNI calls inside that code.%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.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}
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...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With