That which does not kill us makes us stronger.Disclaimer: I have very minimal experience with Java. There is a good chance that information relating to Java, the JVM, and/or best-practices are incorrect or dated. To borrow from DHS: if you see something, say something! Unlike DHS, I'm not just being paranoid. Any input is greatly appreciated!
- Friedrich Nietzsche
Everyone I've talked to who has used the Java Native Interface (JNI) with C++ almost immediately recoils when you ask about it. This is usually followed by a rant and recommendations for SWIG or JNA. Both of these tools can help with integrating native code into the Java language run-time. Considering their utility, I'll give you a quick rundown of what each does before diving into the JNI.
SWIG
The Simplified Wrapper and Interface Generator uses a custom interface description language to define how to generate code for the interface boundary. When your working with larger code bases, especially with custom containers, etc., this can require a fair amount of custom code to manage the interface boundary (managing resources, etc). This custom code usually comes in the form of proxy (or other intermediary classes) that are used to manage garbage collection or scope issues. If you're building a system from scratch that seeks to provide multiple language interfaces, SWIG is definitely the way to go; read the docs upfront to minimize design complications that might arise, and build your SWIG module definitions early and rebuild often. Because SWIG can generate interface code for more than just Java, having a thorough understanding of the tool and its limitations will pay in spades later on. Check out the tutorial for more info.
JNA
The Java Native Access is effectively a Java FFI (Foreign Function Interface). Scripting languages with robust FFI support (e.g., Python 'ctypes' module, Javascript 'ffi' module) provide methods for defining and calling C compatible functions without having to write a target-language specific native library. The JNA provides this for Java. I only had minor experience working with JNA, but it seemed straightforward for common use cases, including calling Java callbacks from C.
Other Mechanisms
- JNR-FFI
Java Native Runtime - Another FFI for Java. ymmv. - JEP 191
Proposal for a built in FFI. Doesn't seem to be implemented yet. A lot of people are apparently looking forward to it. Personally, I'm surprised the JDK didn't define/implement one a long time ago...
One problem with this approach, given a library written in C++, is that you are forced to either a) provide a subset of overall library functionality, or b) manually write all the C run-time interfaces. There are tools out there that will generate C library interfaces for C++ classes (the Chromium Embedded Framework uses one, but I haven't done much research into it). Considering Java is oop(s) and C++ is O.O.P., I wanted to play around with the JVM directly to see how well a modern C++ wrapper for the JNI would counter its complexity.
Note: Two words that describe Java's biggest flaw in the OO department: extends and implements. The "diamond problem" has been solved. Separating interfaces from implementation inheritance (especially with Java now allowing default methods for interfaces) adds to the language complexity...
I have to admit that I only researched a little into existing modern C++ JNI wrappers and didn't find anything compelling. My goal was, honestly, more so to explore the JNI and modern C++. Every native interface directly exposes you, as the module developer, to the mode of thinking and implementation of the target language (it doesn't have to, necessarily, but I'm not sure there's a benefit to adding extra complexity just to shield native developers from the underlying implementation). You are typically using the same methods that the interpreter or VM is using, and if your software takes a different design philosophy, these two worlds can be hard to reconcile. Lucky for me, I was just exploring!
JNI Code
The JNI as implemented seems to be at best C++98 as far as I can tell. The absence of RAII (resource acquisition is initialization) in their C++ API is painful. Rant: One of the core reasons why I think garbage collected languages suck is the lack of complete RAII. Being tied to object destruction prevents full RAII since the GC usually doesn't make guarantees about when an object will be destructed. Instead of RAII, Java uses a try, finally block pair to help the developer handle any local scope objects requiring cleanup... Stack local C++ objects are cleaned up when the code exits the scope in which they are constructed. Java's approach requires you explicitly manage the "interactive" lifetime of objects since there is no concept of a stack local object (other than some scalar types). boo
JNIEXPORT void JNICALL
Java_com_marakana_jniexamples_InstanceAccess_propertyAccess(
JNIEnv *env, jobject object)
{
jfieldID fieldId;
jstring jstr;
const char *cString;
/* Getting a reference to object class */
jclass class = (*env)->GetObjectClass(env, object); /* 1 */
/* Getting the field id in the class */
fieldId = (*env)->GetFieldID(env, class, "name", "Ljava/lang/String;"); /* 2 */
if (fieldId == NULL) {
return; /* Error while getting field id */
}
/* Getting a jstring */
jstr = (*env)->GetObjectField(env, object, fieldId); /* 3 */
/* From that jstring we are getting a C string: char* */
cString = (*env)->GetStringUTFChars(env, jstr, NULL); /* 4 */
if (cString == NULL) {
return; /* Out of memory */
}
printf("C: value of name before property modification = \"%s\"\n", cString);
(*env)->ReleaseStringUTFChars(env, jstr, cString);
/* Creating a new string containing the new name */
jstr = (*env)->NewStringUTF(env, "Brian"); /* 5 */
if (jstr == NULL) {
return; /* Out of memory */
}
/* Overwrite the value of the name property */
(*env)->SetObjectField(env, object, fieldId, jstr); /* 6 */
}
This example (from this tutorial) shows the baggage needed to interact with the JVM. Honestly, it isn't that much different from other native language APIs. For example, Javascript with the V8 engine is similar.
Handle global = context->Global();
Handle value = global->Get(String::New("Name"));
v8::String::Utf8Value str(value->ToString());
std::cout << "Name: " << std::string(*str) << '\n';
global->Set( String::New("Name"), String::New("Brian") );
You often have to use specialized native types that represent the language objects. In my experience, this is the biggest frustration with native interfaces. Rant: For one reason or another, the language developers have adopted their own String, Integer, Array, etc. helper classes that must be used to interact with the higher level language objects. The biggest reason I've come across for this is that custom classes make garbage collection easier. I'm not familiar enough with the problem of garbage collection and current techniques to really have an opinion on the matter, but (I have one anyway, and) I find it hard to believe a GC couldn't be implemented using reference wrappers or other techniques.
In the JNI example above, the method name is absurdly long. If you replace the underscores with dots, you end up with the fully qualified package path and method name that the method implements. In the case above, the Java class that houses this native class looks like this:
package com.marakana.jniexamples;
public class InstanceAccess {
public String name; //1
public void setName(String name) { //2
this.name = name;
}
//Native method
public native void propertyAccess(); //3
...
//Load library
static {
System.loadLibrary("instanceaccess");
}
}
The JVM looks for the functions by name in the provided library (dlopen, dlsym), generating the name using the method, class, and package name.
Java_com_poseidonsfury_coderage_JNISucks_please_god_no would represent the class JNISucks with a native method please_god_no in the package com.poseidonsfury.coderage. This is substantially easier than how it's managed in V8 (hint: it's not - that I'm aware. Comment if I'm wrong!). Granted V8 is a bit more flexible with where you can shove your native interfaces (giggity). The language is your playground. I imagine the JNI has a way to do the same... even if it's hacky af. Take a look at Java reflection libraries for more Java fun. Note: Just because you can do something doesn't mean you should!In the example above, the first parameter is this (i.e., the object instance being called). The method gets the class type of this, gets the field identifier, and then uses the field identifier to get the value of the field from the instance (this). After printing information, a new jstring object is created and the value of the field is set to the new jstring. Simple enough.
JNIEXPORT void JNICALL
Java_com_marakana_jniexamples_InstanceAccess_methodAccess(
JNIEnv *env, jobject object)
{
jclass class = (*env)->GetObjectClass(env, object); /* 7 */
jmethodID methodId =
(*env)->GetMethodID(env, class,
"setName",
"(Ljava/lang/String;)V"); /* 8 */
jstring jstr;
if (methodId == NULL) {
return; /* method not found */
}
/* Creating a new string containing the new name */
jstr = (*env)->NewStringUTF(env, "Nick"); /* 9 */
(*env)->CallVoidMethod(env, object, methodId, jstr); /* 10 */
}
This last example shows how to call a method from C++. You must look up the method using the class object, a name, and the function signature (lines 8-10). All of this for the most part is reasonable, given Java is a statically typed language. It's still a bit brutal to read and write.
Overall, the JNI seems like a mostly standard native language interface. Unfortunately, its failings are a bit more pernicious than others I've seen...
Sugar, meet Gas Tank.
The first thing that should smack you in the face is line 23 and 28:
/* From that jstring we are getting a C string: char* */
cString = (*env)->GetStringUTFChars(env, jstr, NULL); /* 4 */
if (cString == NULL) {
return; /* Out of memory */
}
printf("C: value of name before property modification = \"%s\"\n", cString);
(*env)->ReleaseStringUTFChars(env, jstr, cString);
Remember how I mentioned the JNI doesn't even attempt to use RAII... If that isn't the JNI pouring sugar into your gas tank, I don't know what is. Now, you might be saying:
Don't you think you're being a little too hyperbolic here? This looks like standard C to me.And I would agree, it looks like standard C. However, if you take a look in jni.h, you'll find that both C and C++ interface definitions exist, and having a separate C++ interface, it would stand to reason that they would update it to use more modern C++ techniques like RAII.
Wait. Waht? RAII was developed when? The late 1980's for exception handling in C++, you say?
Java programmers...
Up Next: Toward a More Modern Interface Wrapper, JNI Part 2
In the next blog post, I'll attempt to explain what I implemented to simplify the JNI interface. Hindsight is 20/20, so no need for glasses!
No comments:
Post a Comment