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 Exception
s, 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 Exception
s, but also for RuntimeException
s and Error
s, 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 ();
}