10

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.

enobayram
  • 4,650
  • 23
  • 36
  • So the short version of your question is you want to be able to (for example) derive `GameObject` in Java and still be able to cast within Java when that derived type gets passed to a Java implementation of `collidedWith`? Pretty sure your little hack can be wrapped in a typemap if that's the case. – Flexo Mar 23 '12 at 18:22
  • Precisely! I thought swig would have a way of injecting code, but I'm quite new to swig. I'll check typemaps. – enobayram Mar 23 '12 at 18:47
  • I'll write up an answer tomorrow hopefully then. – Flexo Mar 23 '12 at 18:50

1 Answers1

10

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

Flexo
  • 87,323
  • 22
  • 191
  • 272
  • Wow! So much effort and a great reply. This seems to solve my problem, as well as teaching me a lot about swig. Looks like we sometimes need to override half of swig to get what we want. Thanks a lot. – enobayram Mar 27 '12 at 22:11
  • The nice thing about SWIG is it lets you do things like this when you want to, but encapsulates it all so that you only have to work on the "odd" bits and all the grunt work is done for you. – Flexo Mar 28 '12 at 09:10
  • The never ending decision making of software engineers: "Where should I invest my time in?". SWIG seems to be a good deal since its approach to the cross-language problem appears to be as general as it gets. I mean, as an external code generator, it's free of most limitations that purely library based solutions (like boost python) suffer. – enobayram Mar 28 '12 at 09:21
  • 5 years later I revisited this and realised there's a arguably neater solution that doesn't require extra storage: http://stackoverflow.com/a/42378259/168175 – Flexo Feb 21 '17 at 22:39