Nice JNI Exceptions
 
Support Ukraine

Nice JNI Exceptions

JNI, or Java Native Interface[a], is the way Java code calls non-Java code, and in particular C and C++ code. Writing C and C++ code usually means using error codes instead of exceptions, and unless we do something about it, we get code that looks like this:

public static final int RESULT_SOME_ERROR = -1;
            
public static native int nativeDoStuff ();
            
public static void doStuff () {
    int res = doStuff ();
    
    switch (res) {
    case RESULT_SOME_ERROR: throw new SomeError ();
    }
}

There is much that is wrong with that. First, it forces us to handle the error in two places - once in the native code, and then once in Java. Second, as Yossi Kreinin writes in Error codes vs exceptions: critical code vs typical code[b], we lose information about the error - the SomeError instance has no information beyond its type.

1. Introduction

What we will do is look at how to throw Java exceptions from C++ code and capture the line number and function name. The end result will look something like this:

Exception in thread "main" gphoto2.GPhoto2Exception: No camera auto detected. (-105: GP_ERROR_MODEL_NOT_FOUND: Model not found)
 at <native>.Java_gphoto2_GPhoto2_beginSession0(GPhoto2.cpp:70)
 at gphoto2.GPhoto2.beginSession0(Native Method)
 at gphoto2.GPhoto2.beginSession(GPhoto2.java:27)
 at gphoto2.test.GPhoto2Test.main(GPhoto2Test.java:8)

2. Exception Base Class

We start by creating a base class for Exceptions, with a method, __jni_setLocation that the native code can use to set the location in the native code where the exception was thrown:

package util;

/**
 * Base class for Exceptions that are throwable from code 
 * generated by the JNIThrowableGenerator. JNIRuntimeException
 * and JNIError base classes can be created the same way.
 */
public class JNIException extends Exception {
    
    public JNIException (String message) {
        super (message);
    }
    
    /**
     * Called by native code during construction to set the location. 
     */
    public void __jni_setLocation (String functionName, String file, int line) {
        JNIExceptions.addStackTraceElement (this, functionName, file, line);
    }
}

2.1. Common Code

Since we'll want not just a base class for Exceptions, but also for RuntimeExceptions and Errors, we factor out the code that pushes the native location onto the stack trace to a common helper class:

package util;

/**
 * Support routines for the JNI exception classes.
 */
public class JNIExceptions {
    
    /**
     * Pushes a stack trace element onto the existing stack trace of the throwable. 
     */
    public static void addStackTraceElement (
        Throwable t, 
        String functionName, 
        String file, 
        int line) {
        
        StackTraceElement[] currentStack = 
            t.getStackTrace ();
        StackTraceElement[] newStack = 
            new StackTraceElement[currentStack.length + 1];
        
        System.arraycopy (currentStack, 0, 
            newStack, 1, currentStack.length);
        
        file = file.replace ('\\', '/');
        if (file.lastIndexOf ('/') > -1) {
            file = file.substring (file.lastIndexOf ('/') + 1);
        }        
        
        newStack[0] = new StackTraceElement ("<native>", functionName, file, line);
        
        t.setStackTrace (newStack);
    }
}

3. The Exception Class

Then we can use the base class to create our GPhoto2 exception class:

package gphoto2;

import java.util.Map;
import java.util.HashMap;
import util.JNIException;

/**
 * GPhoto2 error code.
 */
public class GPhoto2Exception extends JNIException {
    
    private static final Map<Integer,String> errorCodes = new HashMap<> ();
    private static void addErrorCode (int code, String def, String msg) {
        errorCodes.put (code, def + ": " + msg);
    }
    static {
        addErrorCode (0, "GP_OK", "Everything is OK.");
        addErrorCode (-1, "GP_ERROR", "Generic Error.");
        /* ... */
    }
    
    public GPhoto2Exception (int code, String message) {
        super (message + " (" + code + ": " + errorCodes.get (code) + ")");
    }
}

4. C++ Exception Throw Code

The final step is to create the function that the naitve code will use to throw the exception. With an eye toward using a code generator to create this based on the Java Exception class, I'll try to indicate which parts are fixed and which would vary with the exception class. First, the implementation:

4.1. Implementation

void
throwGPhoto2Exception (
    // The first four parameters are common for all
    // throwables
    JNIEnv* env, 
    const char* functionName, 
    const char* file, 
    const int line,
    
    // These are specific to the GPhoto2Exception
    const int code,
    const char* message
    )
{
    // Find and instantiate a GPhoto2Exception
    jclass exClass = 
        env->FindClass ("gphoto2/GPhoto2Exception");
        
    jmethodID constructor = 
        env->GetMethodID (exClass, "<init>", 
            "(ILjava/lang/String;)V");
            
    jobject exception = env->NewObject (exClass, constructor,
        code,
        env->NewStringUTF (message));

        
    // Find the __jni_setLocation method and call it with 
    // the function name, file and line parameters
    jmethodID setLocation = 
        env->GetMethodID (exClass, "__jni_setLocation", 
            "(Ljava/lang/String;"
            "Ljava/lang/String;"
            "I)V");
            
    env->CallVoidMethod (exception, setLocation, 
        env->NewStringUTF (functionName),
        env->NewStringUTF (file),
        line);

    // Throw the exception. Since this is native code,
    // execution continues, and the execution will be abruptly
    // interrupted at the point in time when we return to the VM. 
    // The calling code will perform the early return back to Java code.
    env->Throw ((jthrowable) exception);
    
    // Clean up local reference
    env->DeleteLocalRef (exClass);
}

4.2. Header

Then we need a header file. Nothing strange here:

#ifndef gphoto2_GPhoto2Exception__JNIThrow
#define gphoto2_GPhoto2Exception__JNIThrow
/**
 * Throws a GPhoto2Exception. Use the macro 
 * ThrowGPhoto2Exception(code, message) to
 * call this function.
 */
void
throwGPhoto2Exception (
    JNIEnv* env, 
    const char* functionName, 
    const char* file, 
    const int line,
    const int code,
    const char* message
    );
    
/**
 * Throws a GPhoto2Exception. The macro assumes that the current scope has a
 * symbol named env that is of the type JNIEnv*.
 */
#define ThrowGPhoto2Exception(code, message) throwGPhoto2Exception (env, __func__, __FILE__, __LINE__, code, message)

#endif

You may want to replace the use of __func__ with __PRETTY_FUNCTION__ if you use GCC and thus have access to GCC's magic variables[c].

5. Usage

The usage in native code is quite straightforward:

JNIEXPORT jint JNICALL 
Java_gphoto2_GPhoto2_beginSession0 (
    JNIEnv* env,
    jclass
)
{
    // ...
    gp_camera_new (&camera);
    context = gp_context_new();
    
    // set callbacks for camera messages
    gp_context_set_error_func(context, error_func, NULL);
    gp_context_set_message_func(context, message_func, NULL);
    
    // This call will autodetect cameras, take the first one from the list and use it
    int ret = gp_camera_init(camera, context);
    if (ret != GP_OK) {
        // Clean up
        gp_camera_free(camera);
        
        // !!! Throw an exception and return !!!
        ThrowGPhoto2Exception (ret, "No camera auto detected.");
        // !!! Throw an exception and return !!!
        return ret;
    }
    
    return 0;
}

The corresponding Java code is also fairly straightforward:

protected static native int beginSession0 () throws GPhoto2Exception;
public synchronized static void beginSession () throws GPhoto2Exception {
    // ... some code omitted ...
    beginSession0 ();
}