Reputation: 12303
When using Java, C++, Swig, and Swig's directors I can pass a Java object that inherits a C++ class to C++. This works great.
Now, when I pass that same Java object back to Java from the C++ code, Swig creates a new Java object to wrap the C++ pointer. The problem with this is that the new object does not have the same type as the old object. I inherited the C++ class in Java and I need that Java object back.
Why do I want to do this? I have a pool of resources in Java and the C++ code is checking out these resources then returning them to the pool.
Follows is the SSCE:
Here's the C++ code that checks out the resource and returns it:
// c_backend.cpp
#include "c_backend.h"
#include <stdio.h>
void Server::doSomething( JobPool *jp ) {
printf("In doSomthing\n");
Person *person = jp->hireSomeone();
person->doSomeWorkForMe(3);
jp->returnToJobPool(person);
printf("exiting doSomthing\n");
}
Here's the Java code that overrides the C++ classes:
//JavaFrontend.java
import java.util.List;
import java.util.ArrayList;
public class JavaFrontend {
static {
System.loadLibrary("CBackend");
}
public static void main( String[] args ) {
JobPool jobPool = new JobPoolImpl();
new Server().doSomething(jobPool);
}
public static class JobPoolImpl extends JobPool {
private List<PersonImpl> people = new ArrayList<>();
public Person hireSomeone() {
if ( people.size() > 0 ) {
Person person = people.get(0);
people.remove(person);
return person;
} else {
System.out.println("returning new PersonImpl");
return new PersonImpl();
}
}
public void returnToJobPool(Person person) {
people.add((PersonImpl)person);
}
}
public static class PersonImpl extends Person {
public void doSomeWorkForMe(int i) {
System.out.println("Java working for me: "+i);
}
}
}
Here's the Swig interface file:
//c_backend.i
%module(directors="1") c_backend
%{
#include "c_backend.h"
%}
%feature("director") Person;
%feature("director") JobPool;
%include "c_backend.h"
And finally, the C++ header file with the base classes and then a Makefile that compiles it all:
// c_backend.h
#ifndef C_BACKEND_H
#define C_BACKEND_H
#include <stdio.h>
class Person {
public:
virtual ~Person() {}
virtual void doSomeWorkForMe(int i) {
printf("in C++ doSomeWorkForMe %i\n",i);
}
};
class JobPool {
public:
virtual ~JobPool() {}
virtual Person *hireSomeone() {
printf("in C++ hireSomeone\n");
return NULL;
}
virtual void returnToJobPool(Person *person) {
printf("in C++ returnToJobPool\n");
}
};
class Server {
public:
void doSomething( JobPool * );
};
#endif
The Makefile:
# Makefile
JAVA_INCLUDE=-I/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/include/darwin
all:
c++ -c c_backend.cpp
swig -java -c++ $(JAVA_INCLUDE) c_backend.i
c++ $(JAVA_INCLUDE) -c c_backend_wrap.cxx
c++ -dynamiclib -o libCBackend.jnilib *.o -framework JavaVM
javac *.java
clean:
rm -rf *.class *.o *_wrap.cxx *_wrap.h Server.java SWIGTYPE*.java c_backend*.java JobPool.java Person.java
Here's a snippet from the swig code that creates the new Java object replacing my original Java object:
public static void SwigDirector_JobPool_returnToJobPool(JobPool jself, long person) {
jself.returnToJobPool((person == 0) ? null : new Person(person, false));
}
How can I make this work without relying on maintaining a HashMap
inside Java?
Upvotes: 3
Views: 2133
Reputation: 88711
You can do this, withing the constraints you favour (i.e. not maintaining a map of weak references) with a little work. It turns out in fact to be less work than I originally expected too. I'll talk through the solution first and then add some discussion on the way I first tried to do this that became too unwieldy to complete.
The high level view of the working solution is that we have three things added:
%extend
inside person that tries a dynamic cast to Director*
(i.e. one base of the SWIG director heirarchy). This holds a jobject reference to the original Java class, if one exists. So we can trivially return either that jboject, or NULL if the cast fails.this
if not appropriate. We can then inject call that from witihin our javadirectorin typemap to allow an "upgrade" from new proxy to original object to occur.%extend
method of #1 automatically because it isn't normally accessible there directly, even though it could be exposed like this.So your interface file then becomes:
%module(directors="1") c_backend
%{
#include "c_backend.h"
#include <iostream>
%}
%feature("director") Person;
%feature("director") JobPool;
// Call our extra Java code to figure out if this was really a Java object to begin with
%typemap(javadirectorin) Person * "$jniinput == 0 ? null : new $*javaclassname($jniinput, false).swigFindRealImpl()"
// Pass jenv into our %extend code
%typemap(in,numinputs=0) JNIEnv *jenv "$1 = jenv;"
%extend Person {
// return the underlying Java object if this is a Director, or null otherwise
jobject swigOriginalObject(JNIEnv *jenv) {
Swig::Director *dir = dynamic_cast<Swig::Director*>($self);
std::cerr << "Dynamic_cast: " << dir << "\n";
if (dir) {
return dir->swig_get_self(jenv);
}
return NULL;
}
}
%typemap(javacode) Person %{
// check if the C++ code finds an object and just return ourselves if it doesn't
public Person swigFindRealImpl() {
Object o = swigOriginalObject();
return o != null ? ($javaclassname)o : this;
}
%}
%include "c_backend.h"
I threw in a message to stderr just to prove that it really had worked.
In real code you'd probably want to add a javaout typemap that mirrors what the javadirectorin typemap does as well. You could probably dress it all up neatly inside a macro too because all the code is written to avoid assuming an fixed type names.
If I had to guess as to why SWIG doesn't do something like that by default it's almost certainly because that would mandate use of RTTI, but it used to be trendy to pass -fno-rtti
into your compiler "for performance", so lots of code bases try to avoid assuming dynamic casts can be relied upon.
If all you care about is a solution stop reading now. However included here by way of reference is my original approach to this which I ultimately abandoned. It started out like this:
//c_backend.i
%module(directors="1") c_backend
%{
#include "c_backend.h"
%}
%feature("director") Person;
%feature("director") JobPool;
%typemap(jtype) Person * "Object"
%typemap(jnitype) Person * "jobject"
%typemap(javadirectorin) Person * "$jniinput instanceof $*javaclassname ? ($*javaclassname)$jniinput : new $*javaclassname((Long)$jniinput), false)"
%typemap(directorin,descriptor="L/java/lang/Object;") Person * {
SwigDirector_$1_basetype *dir = dynamic_cast<SwigDirector_$1_basetype*>($1);
if (!dir) {
jclass cls = JCALL1(FindClass, jenv, "java/lang/Long");
jmid ctor = JCALL3(GetMethodID, jenv, cls, "<init>", "J(V)");
$input = JCALL3(NewObject, jenv, cls, ctor, reinterpret_cast<jlong>($1));
}
else {
$input = dir->swig_get_self(jenv);
}
}
%include "c_backend.h"
Which changed the Person
types to return an Object
/jobject
all the way through from the wrapper code. My plan was that it would either be an instance of Person
or java.lang.Long
and we'd dynamically decide what to construct based on the instanceof comparison.
The problem with this though is that jnitype and jtype tyemaps make no distinction between the context they get used in. So any other usage of Person
(e.g. constructors, function inputs, director out, other bits of the director code) all needed to be changed to work with a Long
object instead of a long
primitive type. Even by matching typemaps on the variable names it still didn't avoid the overmatching. (Try it and note the places where long becomes Person inside c_backendJNI.java). So it would have been ugly in terms of requiring very explicit naming of typemaps and still have overmatched what I wanted and thus required more intrusive changes to other typemaps.
Upvotes: 4