dhilst

JNI Callback

Hi everybody. Today I’ll show you how to call Java from C code and how to pass callbacks to native code.

Let’s start simple. By calling an static method from native code. Our class would look like this:

public class Callback {
	public static void javaSaysHello() {
		System.out.println("Hello from java");
	}

	static native void nativeCallJavaMethod();

	public static void main(String []args) {
		System.loadLibrary("callback");
		nativeCallJavaMethod();
	}
}

nativeCallJavaMethod will call javaSaysHello from native code. To achieve this we need to implement nativeCallJavaMethod. The libcallback.c code looks like this:

#include "Callback.h"

JNIEXPORT void JNICALL Java_Callback_nativeCallJavaMethod
(JNIEnv *env, jclass cls)
{
	jmethodID mid = (*env)->GetStaticMethodID(env, cls, 
				"javaSaysHello", "()V"); 
	if (!mid)
		return;

	(*env)->CallStaticVoidMethod(env, cls, mid);
}

As you can see, to call a static method from native code we need the method ID and the class. To retrieve method ID you should use the GetStaticMethodID or GetMethodID for static and instance methods respectively. That function receives four arguments being the last the most crypt. The last parameter is the signature of the method. In Java you can have multiple methods with the same name but distinct signature, so to find an specific method you need to supply its signature. To do that you use the javap tool with the -s flag and the class name. The class should be at class path. You may invoke it like this:

javap -s Callback

And it will output something like this:

$ javap -s Callback
Compiled from "Callback.java"
public class Callback {
  public Callback();
    descriptor: ()V

  public static void javaSaysHello();
    descriptor: ()V

  static native void nativeCallJavaMethod();
    descriptor: ()V

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
}
$ 

See the descriptor: bellow the javaSaysHello line? There is from where the “()V” comes. So this brings our first headache while dealing with JNI code. Since the method lookup is made at runtime, changing the method signature will silently break native code, and you will only know at execution time. Here is the Makefile to compile the whole stuff:

JAVA_HOME := /usr/lib/jvm/java-8-openjdk-amd64
CFLAGS += -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/linux

all: libcallback.so

libcallback.so: libcallback.c Callback.h 
Callback.h: Callback.class
Callback.class: Callback.java

%.class: %.java
	javac $<

%.h: %.class
	javah $(<:.class=)

%.so: %.c
	$(CC) $(CFLAGS) $(LDFLAGS) -fPIC -shared -o $@ $^

Executing the code will generate the exepcted output.

$ java -Djava.library.path=. Callback
Hello from java

Okay. This show how to call Java methods from C. But this is not realy a callback. Callbacks should be implemented by user. So let’s do some real callback.

Prior to Java 8 passing a method as argument isn’t possible so to make possible to user implement the callback we use an interface.

interface CallbackInterface {
	public void callback();
}

public class Callback {

	static native void nativeCallJavaMethod(CallbackInterface cb);

	public static void main(String []args) {
		System.loadLibrary("callback");

		/* Passing code as argument */
		nativeCallJavaMethod(new CallbackInterface() {
			public void callback() {
				System.out.println("Hello from CallbackInterface");
			}
		});
	}
}

You can see that nativeCallJavaMethod now receives an CallbackInterface implementation. This way we can ensure the method name and signature. Instead of explicity implement the inteface I’m instantiating it directly at nativeCallJavaMethod argument list. IMHO this syntax is very nice to pass code as argument.

Our libcallback.c has to change too. Here is how it is now:

#include "Callback.h"

JNIEXPORT void JNICALL Java_Callback_nativeCallJavaMethod
(JNIEnv *env, jclass cls, jobject impl_obj) 
{
	jclass impl_cls;
	jmethodID impl_cb_mid;

	impl_cls = (*env)->GetObjectClass(env, impl_obj);
	if (!impl_cls)
		return;
	
	impl_cb_mid = (*env)->GetMethodID(env, impl_cls, "callback", "()V"); 
	if (!impl_cb_mid)
		return;

	(*env)->CallVoidMethod(env, impl_obj, impl_cb_mid);
}

We first grab the class of the object being passed, then use that class to get the method id and finaly is this one to call the implemented method. The output should be:

$ java -Djava.library.path=. Callback
Hello from CallbackInterface

You may notice that if (!...) statements. There I’m checking if the prior call have failed and if this is the case I silently return. This is possible since the native method is of void but this is not good since user can’t tell what goes wrong. An better aproach is to raise some exception, but this is subject for another post.

The code can be found here

If you are running Java 8 you can replace the interface instantiation by a lamda expression. In this case only the call to the nativeCallJavaMethod changes:

		/* Passing code as argument */
		nativeCallJavaMethod(() -> System.out.println("Hello from CallbackInterface"));

But, if you are using Java 8 you may want to take avantage of java.util.function package that declare lots of types of interfaces to be used with method references. Because of this I’m posting a last example using that package. Take a look

import java.util.function.Consumer;

public class Callback {

	static native void nativeCallJavaMethod(Consumer<String> s, String msg);

	public static void main(String []args) {
		System.loadLibrary("callback");
		nativeCallJavaMethod(System.out::println, "Hello World!");
	}
}
#include "Callback.h"

JNIEXPORT void JNICALL Java_Callback_nativeCallJavaMethod
(JNIEnv *env, jclass cls, jobject impl_obj, jstring msg) 
{
	jclass impl_cls;
	jmethodID impl_cb_mid;

	impl_cls = (*env)->GetObjectClass(env, impl_obj);
	if (!impl_cls)
		return;
	
	impl_cb_mid = (*env)->GetMethodID(env, impl_cls, "accept", "(Ljava/lang/Object;)V"); 
	if (!impl_cb_mid)
		return;

	(*env)->CallVoidMethod(env, impl_obj, impl_cb_mid, msg);
}

This time we have used an inteface provided by Java infrastructure. This should save you from duplicating code and creating a lot of interfaces. This also make the native code better since no custom interface (which are candidate to changes) is being used.

Thats it! Regards!