Alright, here for future readers how I managed to do it. There's a few points which don't seem perfectly clean to me, if anybody has an idea on how to do it more cleanly, I'll be very interested in this.
So, I wrote a simple Java class Bar in package foo, which is to be called from C++, passing a reference to a function (more on that below) and calling the function with some hardcoded parameters.
package foo;
import foo.Functor;
//this is just what we want to call from C++
//for demonstration, expect return type int
public class Bar
{
public static void run(long addr) {
Functor F = new Functor(addr);
//synchronously here, just to prove the concept
F.run(1,2);
}
}
As you see, I also wrote a class Functor, which also straightforward
package foo;
//we need to write this for every signature of a callback function
//we'll do this as a void foo(int,int), just to demonstrate
//if someone knows how to write this in a general (yet JNI-compatible) way,
//keeping in mind what we are doing in the non-Java part, feel free to tell me
public class Functor
{
static {
System.loadLibrary("functors");
}
public native void runFunctor(long addr,int a,int b);
long address;
public Functor(long addr)
{
address = addr;
}
public void run(int a, int b) {
runFunctor(address,a,b);
}
}
This depends on a shared library I called functors. Implemented very straightforward. The idea is to keep the actual logic separate, and just provide the interface in the shared object. The main drawback, as mentioned before, is that I have to write it for every signature, I see no way to template this.
Just for completeness, here's the implementation of the shared object:
#include <functional>
#include "include/foo_Functor.h"
JNIEXPORT void JNICALL Java_foo_Functor_runFunctor
(JNIEnv *env, jobject obj, jlong address, jint a, jint b)
{
//make sure long is the right size
static_assert(sizeof(jlong)==sizeof(std::function<void(int,int)>*),"Pointer size doesn't match");
//this is ugly, if someone has a better idea...
(*reinterpret_cast<std::function<void(int,int)>*>(address))(static_cast<int>(a),static_cast<int>(b));
}
And finally, here's how I call it in C++, defining the callback function during runtime, outside the shared object:
#include <iostream>
#include <string>
#include <jni.h>
#include <functional>
int main()
{
//this is from some tutorial, nothing special
JavaVM *jvm;
JNIEnv *env;
JavaVMInitArgs vm_args;
JavaVMOption* options = new JavaVMOption[1];
options[0].optionString = "-Djava.class.path=."; //this is actually annoying, JNI has this as char* without const, resulting in a warning since this is illegal in C++ (from C++11)
vm_args.version = JNI_VERSION_1_6;
vm_args.nOptions = 1;
vm_args.options = options;
vm_args.ignoreUnrecognized = false;
jint rc = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
delete[] options;
if (rc != JNI_OK)
return EXIT_FAILURE;
jclass cls = env->FindClass("foo/Bar");
jmethodID mid = env->GetStaticMethodID(cls, "run", "(J)V");
//the main drawback of this approach is that this std::function object must never go out of scope as long as the callback could be fired
std::function<void(int,int)> F([](int a, int b){std::cout << a+b << std::endl;});
//this is a brutal cast, is there any better option?
long address = reinterpret_cast<long>(&F);
env->CallStaticVoidMethod(cls,mid,static_cast<jlong>(address));
if (env->ExceptionOccurred())
env->ExceptionDescribe();
jvm->DestroyJavaVM();
return EXIT_SUCCESS;
}
This works perfectly fine, and I can work with this. Though, a few things are still bothering me:
- I have to write the interface (both a Java class and the corresponding C++ implementation) for each signature of functions I want to pass. Can this be done in a more general way? My feeling is Java (and especially JNI) isn't flexible enough for this.
- The
reinterpret_casts for converting the pointer to std::function to an integer and back are not something I like doing. But it was the best way I could think of to pass a reference to a function (which possibly exists only during run time) to Java...
- In the initialization of the Java VM in C++, I'm setting an option which in the JVM interface is defined as
char * (there should be a const here). This looks like a very innocent line, but gives a compiler warning, since it is illegal in C++ (it is legal in C, that's why JNI developers likely didn't care). I found no elegant way around this. I know ways how to make it legal, but I really don't want to write several lines of code just for this (or throw around const_casts), so I decided, for this, just to live with the warning.