3

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?

Flexo
  • 87,323
  • 22
  • 191
  • 272
Jason
  • 11,709
  • 9
  • 66
  • 82
  • I think I answered basically the same question before here: http://stackoverflow.com/questions/9817516/swig-java-retaining-class-information-of-the-objects-bouncing-from-c - does that help? If not I can write something up that speaks to any differences. – Flexo Feb 20 '17 at 18:02
  • Indeed, that is the same problem and the solution would work for my question. Though I do not like the answer (even though it works). I would hope for something supported directly by Swig... in fact... I wish Swig, when creating the director instance, would store the Java object and detect and retrieve it when the C++ object is passed to Java. It's possible and it's simple. No hashes. But alas, it seems necessary at the moment. – Jason Feb 21 '17 at 15:48
  • 1
    Possible duplicate of [SWIG Java Retaining Class information of the objects bouncing from C++](http://stackoverflow.com/questions/9817516/swig-java-retaining-class-information-of-the-objects-bouncing-from-c) – Jason Feb 21 '17 at 15:50
  • Maybe we can do something more like you want - the tricky problem is that we either need to pass in a jobject, or a long because `Person` can be both a C++ only type and a Java director type. I'll have a play and see if I can do anything within your constraints. – Flexo Feb 21 '17 at 19:13
  • This is rapidly getting funky because I can't surgically match typemaps on "just the bits I care about". I'll persevere more later, but it's going to become much more intrusive than the 'do it in Java' approach and will always need to depend on both `instanceof` and `dynamic_cast` to work. – Flexo Feb 21 '17 at 19:52
  • I took the liberty of making an edit to express your desire to avoid maintaining the `HashMap` inside Java as part of the question. With that I think it's sufficiently different to justify not closing as a duplicate and make a distinct answer here sensible. – Flexo Feb 21 '17 at 21:26

1 Answers1

3

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:

  1. Some C++ code, via %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.
  2. Some Java code that will return either the result of our C++ code, or 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.
  3. Another trick in the form of a trivial typemap that passes the JNIEnv object into the %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.

Flexo
  • 87,323
  • 22
  • 191
  • 272
  • Nice, but I'd like to disagree with your sentiment around disabling RTTI. RTTI doesn't interact well with heavily template metaprogrammed code, as in all the instantions generate RTTI overhead, whereas without RTTI, they would vanish from the binary. This can easily amount to many megabytes of useless RTTI data. So there are valid reasons for disabling it. – enobayram Feb 22 '17 at 05:40
  • I think the case against rtti is often overstated based on folklore instead of actual benchmarks. Ram is cheap and "many MB" is still peanuts in most scenarios other than embedded, even on some mobile devices these days. If you only dynamic cast one or two types the real impact is likely to be only one or two extra pages in the working set. So assuming downloading over a slow link isn't on the critical path I'd wager a benchmark shows many other overheads that completely dwarf it. The thing I dislike is the assumption without evidence and that does seem to be diminishing in codebases I see. – Flexo Feb 22 '17 at 08:01
  • In terms of which solution actually performs better I wouldn't want to guess here - there's an extra call to native code in this solution which involves crossing the JNI boundary more, but there is no HashMap to maintain including O(n) memory, no locking on insert/remove which is instead traded for the dynamic_cast cost that's fixed based on type hierarchy depth and not number of instances. I wouldn't be surprised if there's a crossover point at which the two solutions switch around. – Flexo Feb 22 '17 at 08:09
  • The biggest trouble with RTTI in C++ is that it's against the "only pay for what you use" principle. I have no problem with using a few `dynamic_cast`s here and there in principle, if that really simplifies your code, but to enable it, you have to pay the price EVERYWHERE! It's not just folklore BTW, saying "heavily template metaprogrammed" I really mean it. Because of exceptions and RTTI, my zero-cost abstractions stop being zero-cost and I don't like it. – enobayram Feb 22 '17 at 10:40