Development

registering jni methods when using proguard

When writing Java software, it’s sometimes needed to call some native code, and mostly this has been achieved by writing a JNI adapter, and relying on the magic of the java runtime to link the methods in the library to the abstract native java methods.

Here we show another way to do this, applicable mostly when using Java in an embedded device, but perhaps in other settings too, where the java code can be ProGuard’ed and all symbols removed from the C objects.

Quick JNI Recap

Usually, you’d write a java class a bit like this:

package bob;
public class YoJni {
    static {
        System.loadLibrary("yo");
    }
    private native void yo();
    
    // some other code that calls this method...
}

with a corresponding C program

JNIEXPORT void JNICALL Java_YoJni_yo(JNIEnv *env, jobject this) {
    // do some C stuff...
    return;
}

Which is great, but it relies on a few things behind the scenes. Because the JNI loader introspects the class and the shared object symbol table to link up the native java method with the correctly named C function, the two things have to match up.

This isn’t going to happen if the class has been renamed with proguard, or if you want to remove the symbol table from the library.

Running on a normal JVM

In a non-obscured mode, the library can simply be loaded with System.loadLibrary(), but when the C code needs to be stripped and the Java bytecode run through ProGuard, you may have to jump through an extra library to register the natives, loading a library with a single entry point first, which when called registers all the rest of the obscured libraries.

##Runing on embedded JVM

An alternative is to statically link in the library into the java executable. How you do this will vary from platform to platform - but embedded java vendors often supply a .a file that implements the JVM.

What needs to be registered?

There are two types of method that need to be registered with the JVM - the C implementation of an abstract native method, and the targets of C-to-Java callbacks.

By tagging each callback method with an annotation, we can inspect the compiled bytecode using ASM to find all tagged methods, and all native methods, and write a C registration function.

An Example

Lets set up a simple example, with a single native method, and a single method that is called back from C, which we’ll compile and put in a .jar file - jnirn-example.jar


package example;

public class Native {
    private native void bob();
}

@Retention(RetentionPolicy.CLASS)
public @interface CalledBack {
}


public class Java {
    @CalledBack
    public static void callback(String string, int bob) {
    }
}

Generate C bindings

We can then run the C generator

java -jar jnirn.jar -C example.CalledBack -H c/example.h -o c/example.c out/jnirn-example.jar

The C output

All this code is generated by jnirn for you - you don’t have to type anything!

Firstly we build a table of all the native methods, by class, using the JNI specification name for the C function - which is the JNI method you’d have to write…

Here ‘example_Native’ is the Java class example.Native.

static const JNINativeMethod method_table_example_Native[] = {
   {"bob", "()V", Java_example_Native_bob}
};

struct registration { const char * class_name; const JNINativeMethod *methods; int method_count; };
static const struct registration registrations[] = {
    {"example/Native", method_table_example_Native, 1},
};

Then build a table of all the callbacks

jclass Natives_class_example_Java;

jmethodID Natives_method_example_Java__callback;

struct method_ref { const char *name; const char *signature; jmethodID *method_id_p; };
struct class_ref { const char *name; jobject *global_ref_p; int method_count; const struct method_ref* methods; };

static const struct class_ref referenced_classes[] = {
    { "example/Java", &Natives_class_example_Java, 1,
        (struct method_ref[]) {
            { "callback", "(Ljava/lang/String;I)V", &Natives_method_example_Java__callback },
        },
    },
};

Finally, build a registration function that can be called at JVM initialisation time

int NativesInit(JNIEnv *env, NativesErrorCallback error_callback, void *client_context) {
    jclass the_class;
    jint status;
    int i, j;

    for (i = 0; i < 1; i++) {
        the_class = (*env)->FindClass(env, registrations[i].class_name);
        if (the_class == NULL) {
            error_callback(client_context, registrations[i].class_name, NULL, NULL);
            return -1;
        }
        for (j = 0; j < registrations[i].method_count; j++) {
            const JNINativeMethod *method = &registrations[i].methods[j];
            status = (*env)->RegisterNatives(env, the_class, method, 1);
            if (status < 0) {
                error_callback(client_context, registrations[i].class_name, method->name, method->signature);
                return -1;
            }
        }
    }

    for (i = 0; i < 1; i++) {
        the_class = (*env)->FindClass(env, referenced_classes[i].name);
        if (the_class == NULL) return -1;
        *(referenced_classes[i].global_ref_p) = (*env)->NewGlobalRef(env, the_class);
        if (*(referenced_classes[i].global_ref_p) == NULL) return -1;
        for (j = 0; j < referenced_classes[i].method_count; j++) {
            *(referenced_classes[i].methods[j].method_id_p) = (*env)->GetStaticMethodID(env, the_class, referenced_classes[i].methods[j].name, referenced_classes[i].methods[j].signature);
            if(*(referenced_classes[i].methods[j].method_id_p) == NULL) return -1;
        }
    }

    return 0;
}

Stripped Library

As the registration functions are now compiled into the C code, no symbols need to be exposed externally, so the library / executable can be stripped completely.

Proguard

If the .jar file is run through proguard before calling jnirn, then it can read the mapping file, generating registration calls, that map the original C names to the mangled Java code.

You’ll need to make sure that Proguard is configured to only mangle the names of these classes, not remove them or flatten them, but otherwise, you’re all set.

Limitations

Currently, callbacks have to be static, and the JNI method references are kept for the lifetime of the JVM, which may not work in your environment.

Credit

jnirn was written by James Richardson & Nat Pryce in 2014

Code

The code is on github at: https://github.com/npryce/jnirn

comments powered by Disqus