Ok, there's a keyword that I've intentionally kept away from the tags and the title. That's "Android", but that's because even though the project is in Android, I don't think my question has anything to do with it, and I don't want to scare people without experience in Android.
So, the usual problem with swig. I've a virtual method in a C++ class, which I've made it overloadable in Java by adding the director
feature to the class and it works. The problem is that the method receives a polymorphic argument which is also extended on the java side, and during the virtual method call in Java, the object comes with all polymorphic information stripped.
To present the exact situation; I'm writing a game engine in C++, and I want to use it happily in Java. The game engine has a GameObject
class, which registers CollisionListener
s and when the collision engine detects a collision event, it calls the collidedWith(GameObject & collidee)
method of all registered collisionListener
s passing them with what object they've collided.
class CollisionListener {
public:
virtual bool collidedWith(GameObject &){};
~CollisionListener(){} // I know this needs to be virtual but let's forget about that now
};
I'm exposing this class, along with the GameObject
class to java using the following interface file Bridge.i
%module(directors="1") Bridge
%feature("director") CollisionListener;
%include "CollisionListener";
%feature("director") GameObject;
%include "GameObject.h"
Now, when I inherit from CollisionListener
in java and overload collidedWith
, it get's called with a java side GameObject
object. For instance, if I inherit from the java side GameObject
class and define a Bullet
class, when this bullet collides with another object with a listener, in the collidedWith
method call, all I receive is a bare GameObject
, so that (object instanceof Bullet)
does not work. No surprise, I've dug into the swig generated BridgeJNI.java
and found this:
public static boolean SwigDirector_CollisionListener_collidedWith(CollisionListener self, long arg0) {
return self.collidedWith(new GameObject(arg0, false));
}
So it wraps a new object around the pointer before calling the java overloads.
So, the main question is how to receive a Bullet
object when there's a collision?
I've come up with a way to easily achieve that but I need to modify the auto-generated files, which is a bad idea. So I'm hoping some swig master could help me inject the modifications to the swig generated files.
My little hack is to keep a jobject * self
in every C++ side GameObject
object, and assign the address of the real java object during the construction of the real java side GameObject
(and not the one that merely wraps the pointer). This way, I could define a polymorphic getSelf
method in C++ side GameObject
and use the result happily in java. Is there a way to inject the necessary code to the swig generated files?
Thanks
Note: If you tried directors on Android and they haven't worked, it's because the current stable version does not support it. Download the Bleeding Edge from the swig website. But I'm writing this in 22/03/2012 and this note will soon be unnecessary. The reason why the destructor isn't virtual is that the Bleeding Edge version makes the program crash in the destructor, and making it non-virtual seems to keep it under control for now.
I've put together a solution to this problem. It's not quite the solution you suggested in your question though, it's more code on the Java side and no extra on the JNI/C++ side. (I found doing it the way you suggested quite tricky to get correct in all the possible cases).
I simplified your classes down to a single header file:
class GameObject {
};
class CollisionListener {
public:
virtual bool collidedWith(GameObject &) { return false; }
virtual ~CollisionListener() {}
};
inline void makeCall(GameObject& o, CollisionListener& c) {
c.collidedWith(o);
}
which also added makeCall
to actually make the problem obvious.
The trick I used is to register all Java derived instances of GameObject
in a HashMap
automatically at creation time. Then when dispatching the director call it's just a question of looking it up in the HashMap.
Then the module file:
%module(directors="1") Test
%{
#include "test.hh"
%}
%pragma(java) jniclasscode=%{
static {
try {
System.loadLibrary("test");
} catch (UnsatisfiedLinkError e) {
System.err.println("Native code library failed to load. \n" + e);
System.exit(1);
}
}
%}
/* Pretty standard so far, loading the shared object
automatically, enabling directors and giving the module a name. */
// An import for the hashmap type
%typemap(javaimports) GameObject %{
import java.util.HashMap;
import java.lang.ref.WeakReference;
%}
// Provide a static hashmap,
// replace the constructor to add to it for derived Java types
%typemap(javabody) GameObject %{
private static HashMap<Long, WeakReference<$javaclassname>> instances
= new HashMap<Long, WeakReference<$javaclassname>>();
private long swigCPtr;
protected boolean swigCMemOwn;
public $javaclassname(long cPtr, boolean cMemoryOwn) {
swigCMemOwn = cMemoryOwn;
swigCPtr = cPtr;
// If derived add it.
if (getClass() != $javaclassname.class) {
instances.put(swigCPtr, new WeakReference<$javaclassname>(this));
}
}
// Just the default one
public static long getCPtr($javaclassname obj) {
return (obj == null) ? 0 : obj.swigCPtr;
}
// Helper function that looks up given a pointer and
// either creates or returns it
static $javaclassname createOrLookup(long arg) {
if (instances.containsKey(arg)) {
return instances.get(arg).get();
}
return new $javaclassname(arg,false);
}
%}
// Remove from the map when we release the C++ memory
%typemap(javadestruct, methodname="delete",
methodmodifiers="public synchronized") GameObject {
if (swigCPtr != 0) {
// Unregister instance
instances.remove(swigCPtr);
if (swigCMemOwn) {
swigCMemOwn = false;
$imclassname.delete_GameObject(swigCPtr);
}
swigCPtr = 0;
}
}
// Tell SWIG to use the createOrLookup function in director calls.
%typemap(javadirectorin) GameObject& %{
$javaclassname.createOrLookup($jniinput)
%}
%feature("director") GameObject;
// Finally enable director for CollisionListener and include the header
%feature("director") CollisionListener;
%include "test.hh"
Note that since all Java instances are being stored in a HashMap
we need to use a WeakReference
to be sure that we aren't prolonging their lives and preventing garbage collection from happening. If you care about threads then add synchronisation as appropriate.
I tested this with:
public class main {
public static void main(String[] argv) {
JCollisionListener c = new JCollisionListener();
JGameObject o = new JGameObject();
c.collidedWith(o);
Test.makeCall(o,c);
}
}
Where JCollisionListener
is:
public class JCollisionListener extends CollisionListener {
public boolean collidedWith(GameObject i) {
System.out.println("In collide");
if (i instanceof JGameObject) {
System.out.println("Is J");
}
else {
System.out.println("Not j");
}
JGameObject o = (JGameObject)i;
return false;
}
}
and JGameObject
is:
public class JGameObject extends GameObject {
}
(For reference if you wanted to do the other approach you would be looking at writing a directorin
typemap).
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