| |
|
|
||||
![]() |
||||||
| |
|
|||||
|
Double Indirection: The Catch-22
There have been competing native interface APIs proposed, most notably Microsoft's Raw Native Interface (RNI). The problem with RNI is that it exposes the underlying operating system and JVM implementation, which makes it impossible to port to another VM. In some ways, the problem with JNI is that it does not expose the VM implementation. JNI makes you go through pains to ensure that native code never gets to see how Java objects are laid out in memory. Consequently, the native code has to deal with indirections every step along the way, many of them ultimately leading to table and string lookups. This is not good for application performance. See the example in Listing 2, where we pass command line arguments from Java to C, which involves references, arrays, and conversion to UTF-8 (the canonical two-byte Unicode encoding used by Java). Listing
2. Passing Arguments from // Minimal Java control code wraps legacy engine. package somegame; class GameMain { protected static native int nativeMain( String[] args ); public static void main( String[] args ) { int ret = nativeMain( args ); ... } static { System.loadLibrary("Game"); } } // Handing over the arguments to native code. // This code will be put into the Game.DLL. #include <jni.h> extern int gameMain( int argc, char** argv ); JNIEXPORT jint JNICALL Java_somegame_GameMain_nativeMain ( JNIEnv* env, jclass cls, jobjectArray jargv ) { jint res; jint argc; jint i; jboolean isCopy; jstring jstr; jsize len; const char* cstr; jargc = env->GetArrayLength(jargv); for ( i = 0; i<argc; i++ ) { jstr = (jstring)(*env)->GetObjectArrayElement(env,jargv,i); cstr = (const char*)((*env)->GetStringUTFChars( env, jstr, &isCopy )); // We copy - we have to release, and we don't want to accumulate local references. argv[i] = (char*)malloc( strlen(cstr)+1 ); strcpy( argv[i], (const char*)cstr ); // Did the JVM copy as well? if ( isCopy == JNI_TRUE ) { (*env)->ReleaseStringUTFChars( env, jstr, cstr ); } // Clear local reference. (*env)->DeleteLocalRef(env,jstr); } // Call our main() now. res = (jint)gameMain( (int)argc, argv ); // Release allocated memory. for ( i=0; i<argc; i++ ) { free( argv[i] ); } // Return to Java. return res; } Tools such as javah generate C header files containing proper function prototypes (name and signatures) from a Java class. These tools are already Unicode-aware, thus using underscores and other special characters in Java method and class names can lead to surprising results. The code in Game.dll will be linked to the class by the JVM by calling java.lang.System.loadLibrary("Game") automatically. The example in Listing 2 implements a minimal Java wrapper around native legacy code. Given all the implicit and explicit copying, we somehow seem to have come full circle: to get rid off some portability-related Java overhead, we decided to use native code, only to find out that the JNI design hampers the interaction between Java and native code to ensure portability. Now what? Well, there are basically two ways left to increase performance: 1. Brute force. You could switch tools and compile to native code. If you pursue this option, make sure your Java compiler supports JNI as well, and that it doesn't just compile pure bytecode. 2. Smart design. You could accept the limitations of the JNI, and design your native and Java modules in a way that streamlines the interface between them. Mind you, your native code by itself will be as fast as it gets. It's only the transfer of parameters and results back and forth that, inside an inner loop, incurs significant performance penalties. |
|
|