Search code examples
java-native-interfaceswigdowncast

How to implement a generic interface between C++ and Java using SWIG and Downcasts?


I´m writing an application which is intended to run on different platforms. My base library is written in C++ and I´d like to use SWIG for generating platform-specific code (Java/Android, C#/Windows, Objective C/iOS).

Here I´m using Message-Objects which I´d like to pass from Java -> C++ and from C++ -> Java. The idea is, to have a base type "CoreMessage" and derived messages "StatusMessage", "DebugMessage"...

For me it seems natural to keep the interface generic by doing something like this:

C++:

class CoreMessage {

}

class StatusMessage : public CoreMessage {
public:
     string getStatusMessage();
}

class DebugMessage : public CoreMessage {
public:
     ... some stuff
}

The (bidirectional) interface code in C++ should look like this:

CoreMessage waitForNextMessage(); // Java calls this and blocks until new message is available in a message queue
void processMessage(CoreMessage m); // Java calls this and puts a new message in an eventhandling mechanism

My Swig file is very basic, it simply includes the class-definitions and the interface - nothing else.

Now Swig generates all the classes and interfaces for Java, including the inheritance for the messages in Java:

public class CoreMessage {
    ...
}
public class StatusMessage extends CoreMessage {
    ...
}
public class DebugMessage extends CoreMessage {
    ...
}

The main Java-Module now looks like this:

public native void processMessage(CoreMessage);
public native CoreMessage waitForNextMessage();

When I try to call this code from Java like that:

StatusMessage m = new StatusMessage();
processMessage(m);

this code in C++ will be executed and causes an error:

void processMessage(CoreMessage in) {
    StatusMessage* statusPtr = dynamic_case<StatusMessage*>(&in); // works
    StatusMessage status = *(statusPtr); // This will cause a runtime error
}

Same problem in the other direction: C++ Code

TsCoreMessage waitForMessage() {
    StatusMessage m = StatusMessage();
    return m;
}

with this Java Code, calling C++ via JNI and SWIG-generated wrapper:

CoreMessage msg = waitForMessage();
// msg instanceof StatusMessage returns false
StatusMessage s = (StatusMessage) msg; // Causes java.lang.ClassCaseException

So for me it seems JNI looses the type information when passing the object from one language to another...

I did read some threads and articles but I did not find a solution for this - I need a bidirectional solution.

I´m not sure "director" is not what I´m looking for? As far as I understood directors, they are made for classes, extended in java by hand and allow to UP-cast to the corresponding C++ BaseClass... But I´m not sure with my understanding of directors ;-)


Solution

  • well, I finally found a way to solve this:

    The key is: I MUST use pointers (in my case boost::shared:ptr).

    So my Messages (messages.h) now look like this:

    typedef boost::shared_ptr<CoreMessage> CoreMessagePtr;
    public class CoreMessage {
        ...
    }
    typedef boost::shared_ptr<StatusMessage> StatusMessagePtr;
    public class StatusMessage extends CoreMessage {
        ...
    }
    typedef boost::shared_ptr<DebugMessage> DebugMessagePtr;
    public class DebugMessage extends CoreMessage {
        ...
    }
    

    And the functions also need to be changed to these Pointers:

    CoreMessagePtr waitForNextMessage();
    void processMessage(CoreMessagePtr m);
    

    Then, I had to tell Swig about the shared_ptr using the included template for boost smart-pointers:

    %module myapi
    
    %include <boost_shared_ptr.i> // Tells Swig what to do with boost::shared_ptr
    
    %header %{ // Tells Swig to add includes to the header of the wrapper
        #include <boost/shared_ptr.hpp>
        #include "messages.h"
    %}
    
    // Now here is the magic: Tell Swig that we want to use smart-Pointers for
    // these classes. It makes Swig generate all necessary functions and wrap 
    // away all the de-referencing.
    %shared_ptr(CoreMessage) 
    %shared_ptr(StatusMessage)
    %shared_ptr(DebugMessage)
    
    %include "message.h" // Finally: Tell Swig which interfaces it should create
    

    Well, what we now get: Swig generates Java classes which look very similar to the ones we had before. But there is one small thing which makes the difference: The constructors now look like this:

    protected StatusMessage(long cPtr, boolean cMemoryOwn) {
        super(tscoreapiJNI.StatusMessage_SWIGSmartPtrUpcast(cPtr), true);
        swigCMemOwnDerived = cMemoryOwn;
        swigCPtr = cPtr;
    }
    

    and the magic line is super(tscoreapiJNI.StatusMessage_SWIGSmartPtrUpcast(cPtr), true); This line wraps an dynamic cast of the pointer which enables casting your object from Java.

    But: It is not as easy as it might be (perhaps I just didn´t find a solution?!)

    You cannot use Java casting to do the work like this [Java Code]:

    CoreMessage m = waitForMessage(); // Returns a StatusMessage
    StatusMessage mm = (StatusMessage) m; // Throws ClassCaseException
    

    This is because java cannot do the dynamic cast which is necessary but needs the C++ code for that. My solution looks like this and uses a function template:

    I added this template to messages.h

    template <class T>
    static boost::shared_ptr<T> cast(TsCoreMessagePtr coreMsg) {
        return boost::dynamic_pointer_cast<T>(coreMsg);
    }
    

    Now tell Swig how to handle this template [added to the end of interface.i]:

    %template(castStatusMessage) cast<StatusMessage>;
    %template(castDebugMessage) cast<DebugMessage>;
    

    What happens is: Swig adds these two functions to myapi.java:

    public static StatusMessage castStatusMessage(CoreMessage coreMsg) {
        long cPtr = myapiJNI.castStatusMessage(CoreMessage.getCPtr(coreMsg), coreMsg);
        return (cPtr == 0) ? null : new StatusMessage(cPtr, true);
    }
    
    public static DebugMessage castDebugMessage(CoreMessage coreMsg) {
      long cPtr = myapiJNI.castDebugMessage(CoreMessage.getCPtr(coreMsg), coreMsg);
      return (cPtr == 0) ? null : new DebugMessage(cPtr, true);
    }
    

    And finally you can use it like this in your java code:

    CoreMessage m = waitForMessage(); // Returns a StatusMessage
    StatusMessage mm = myapi.cast(m);