We had a really nasty bug in Terminator this week. It only showed itself on Linux 2.4, presumably because of SIGCHLD-related changes between 2.4 and 2.6 (which most of us run). What had happened was that we'd factored out the
ThrowNewcode into a global function called
throwJavaException. The word 'throw' in Java-related C++ was obviously enough to fool all three of us who'd read the code that an exception really was being thrown, and that the call stack would be unwound rather than the rest of the calling function being executed.
It got worse, though. It turns out that
GetFieldIDfails if called with a pending exception. Worse still, we were using the resulting
jfieldIDon the assumption that it was good. This was causing the Java VM to crash later, claiming a VM error (and not pointing at our code at all).
(This is one reason why Joel Spolsky is wrong to dislike exceptions. With exceptions, this couldn't have happened.
GetFieldIDwould have thrown an exception, and we'd have stopped. At the right time to see what was wrong.)
It took hours to find this, and I didn't want any of us to have to waste more time on similar problems in future. We can't well fix the Java VM (though if anyone's looking for ways to make Free VMs better, having them go to greater lengths to protect themselves against incorrect JNI code would be one place to start), but we can improve our code.
My idea was to make use of C++ exceptions throughout the implementation, but catch them and convert them to Java exceptions in the crufty
extern "C"global functions that JNI forces on us. Two problems: I didn't want to have to write that code out myself in every JNI function I write, and I also needed a place for the real code (the code that throws exceptions for these stubs to catch) to live.
My answer was javahpp, which is to C++ what Sun's javah is to C. It generates stub JNI functions that instantiate a C++ class corresponding to the Java class, and invoke the matching member function on that instance. The C++ class also has a data member for each field in the Java class. These data members proxy for the Java fields, and let you use assignment to set them, and offer a
getmethod to get their current value.
Rather than invent bogus return values for the JNI C functions when an exception is thrown, javahpp insists that your native methods return void. This is safe and simple and not a limitation in practice because you're doing as much work as possible in Java anyway (right?).
Any of the C++ can throw a C++ exception and go straight back to the JNI C function, where a Java exception is "thrown". There's no clever translation from C++ exception classes to Java exception classes, but there could easily be. If I had more JNI, I might be tempted. At the moment, we always throw
java.lang.RuntimeException. (Exceptions thrown by native code are inherently unchecked anyway, so even if we were checked-exception people, which we're not, this would still have been a good choice.)
We don't use C++ namespaces because it's not easy to see what use they'd be, and it would have been slightly more work to use them than it was not to.
You can see a real-life example of this in Terminator, and javahpp is in salma-hayek. It only supports what we've needed so far, so it's not yet a complete solution to writing JNI code in C++, but if you look at Terminator's native code I'm sure you'll agree that it's useful as it stands. If you'd like to use it but need support for, say, static fields (which aren't supported at the time of writing), let me know. Time permitting, I'm interested in extending this, even if I don't have an awful lot of use for JNI personally.
I don't claim that any of the ideas presented here aren't obvious, but I do claim that they're useful, and better than I've seen elsewhere.