Error handling in JNI

In this second part of the JNI tutorial series, we attack error handling, to get that annoying subject out of the way as soon and as much as possible. There’s no complete example for this part in the tutorial example zip file (because you can’t write a program that only does error handling), but you can see the techniques put to use in the examples for the two following parts.

Often when I caused something to go wrong with JNI, eventually crashing my program, it was difficult to figure out exactly what the problem was, because the effect showed itself in a different part of the code from the cause, and not where the hs_err file (the JVM crash report) indicated that the crash happened. This has taught me to always, always, immediately check for error conditions relating to JNI, never assume that something worked, no matter how trivial, and react on errors immediately.

Since we are in Java, the best reaction to an error is usually to throw an exception. But throwing in JNI C code is a bit different than throwing in Java. When you call a JNI exception function to throw an exception, the function returns (except FatalError, which terminates the process immediately) and execution continues in a “thrown” state, until the native function returns, and only then does the exception take effect in the normal Java way. It’s possible to throw again while already in thrown state, which may override the first exception and hide the original source of the problem. Therefore, these are my rules:

  • When an error condition arises, first use ExceptionCheck to check if we’re already in a thrown state (maybe because an exception was thrown inside some JNI function), and if not, immediately throw something suitable. Here’s an example, where env is the JNIEnv * that is the first parameter of every native method implementation, throwClass is a suitable exception class, and message is a C string:

    if (!(*env)->ExceptionCheck(env)) {
        (*env)->ThrowNew(env, throwClass, message);
    }
    
  • Then return to the caller as soon as possible without doing anything more.

While reacting to an error, you don’t want to risk obscuring the original problem by doing something else that can go wrong, like look up a specific exception class for throwing, or compose an elaborate error message. Therefore, it’s a good idea to prepare by looking up some exception classes in advance and keep them ready for use. My convention is to use my Library static initialization class to set up the exception classes I want to use. I prefer not to use FindClass to look up the class objects in a native method, but instead pass them as parameters from the Java side, so that I get a compile error rather than a runtime error if I get the class name wrong. Say that the throwable classes we want to use are Error, IOException, and FileNotFoundException. Then Library can have a static native method for passing the three class objects to the C side:

private static native void init(Class errClass, Class ioexClass, Class fnofClass);

Note that if you use the generic makefile from the last post, Library should now be included on the JNI_CLASSES line, since it has a native method.

The private init method can be called in a static block, with the class objects as arguments:

static {
    init(Error.class, IOException.class, FileNotFoundException.class, ByteBuffer.class);
}

In the C implementation part of Library, which I put in src/jni/Library.c, the exception classes are kept as static variables:

static jobject errClass, ioexClass, fnotfClass;

And these are initialized in the Library.init implementation.

But you must not simply assign the static C variables from pointers passed to the native method! In general, an object pointer passed to a native method is only valid until the method returns. The next time JNI C code is called, the object may have been moved by the garbage collector, and then the old pointer value is wrong. Therefore, whenever we retain a pointer to a Java object on the C side between native calls, we must use NewGlobalRef to get a global reference to it, and then keep the pointer to that instead.

To make sure that execution doesn’t continue after a failed attempt to get a global reference (a serious condition that it’s probably not worth trying to recover from), I use the following convenience function that calls FatalError on failure.

static jobject init_global(JNIEnv *env, jobject local) {
    jobject global = (*env)->NewGlobalRef(env, local);
    if (global == NULL) { (*env)->FatalError(env, "failed to get global reference"); }
    return global;
}

The implementation of the native Library.init can then look like this:

void METHOD(init)(JNIEnv *env,
                  jclass thisClass,
                  jclass jerrClass,
                  jclass jioexClass,
                  jclass jfnotfClass)
{
    errClass = init_global(env, jerrClass);
    ioexClass = init_global(env, jioexClass);
    fnotfClass = init_global(env, jfnotfClass);
}

When a global reference is no longer used, it should be released with DeleteGlobalRef, but we want these exception class references to stay around until the end of the program, and never release them.

The following convenience functions for throwing also go in my src/jni/Library.c file:

void raiseThrowable(JNIEnv *env, const jclass throwClass, const char *message) {
    if (!(*env)->ExceptionCheck(env)) {
        (*env)->ThrowNew(env, throwClass, message);
    }
}
void raiseError(JNIEnv *env, const char *message) {
    raiseThrowable(env, errClass, message);
}
void raiseIOException(JNIEnv *env, const char *message) {
    raiseThrowable(env, ioexClass, message);
}
void raiseFileNotFound(JNIEnv *env, const char *message) {
    raiseThrowable(env, fnotfClass, message);
}

I put declarations of these functions in a file src/jn/Library.h, which other JNI C files can include, and then use the raise functions, like this for example (ferror, strerror, and errno, used there are all part of standard C):

if (ferror(f)) { raiseIOException(env, strerror(errno)); return; }

That line is an excerpt from the code presented in the next part of this tutorial, which focuses on transferring blocks of data in JNI.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.