Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SWIG Java Retaining Class information of the objects bouncing from C++

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 CollisionListeners and when the collision engine detects a collision event, it calls the collidedWith(GameObject & collidee) method of all registered collisionListeners 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.

like image 862
enobayram Avatar asked Mar 22 '12 06:03

enobayram


1 Answers

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).

like image 176
Flexo Avatar answered Nov 01 '22 20:11

Flexo