|
IntroductionThe JNI is a low-level mechanism for communication between Java and C-based code. The most common use of the technology is accessing system functionality that does not easily match up with cross-platform Java features or APIs. In the case of Mac OS X, this could mean accessing data from the Mac OS X Address Book, or even displaying Quartz Composer compositions. The JNI can also be used to facilitate incremental migration of a C codebase to Java, or vice-versa. Instead of migrating the entire project at once, conversion can occur across multiple releases, using the JNI to maintain integration between, for example, the new Java code and remaining C code. Finally, the JNI provides an invocation interface for creating and communicating with a JVM from a native application. This technote discusses the specific details of working with the JNI on Mac OS X. It begins with simple translation of data and object references, and moves on to the more advanced topic of integrating native Cocoa GUI objects within a Java GUI. Threading concerns and techniques for addressing and avoiding problems are also discussed. Finally, it explains how to correctly create a JVM from within a Core Foundation or Cocoa application. The information and techniques described in this technote are specific to Java 1.4 and later on Mac OS X. Basic knowledge of the JNI is assumed. Developers unfamiliar with the JNI should first read The Java Native Interface: Programmer's Guide and Specification. Building JNI Applications in XcodeThe Xcode IDE can be a very powerful tool for building JNI applications, as both Java and native code can be built and bundled from a single project. However, this technote focuses mainly on JNI programming. The numerous samples cited below all include Xcode projects as starting points. For information on building a JNI project in Xcode from scratch, see Building a Universal JNI Project With Xcode. Data Transfer Between Java and Native CodeWorking With StringsAccessing or creating Java Creating Java Strings From Native StringsOn Mac OS X, the Listing 1: Creating a Java String from an NSString NSString *myNSString = @"This is an NSString"; // Note that length returns the number of UTF-16 characters, // which is not necessarily the number of printed/composed characters jsize buflength = [myNSString length]; unichar buffer[buflength]; [myNSString getCharacters:buffer]; jstring javaStr = (*env)->NewString(env, (jchar *)buffer, buflength); Listing 2 demonstrates creating a Listing 2: Creating a Java String from a CFString CFStringRef myCFString = CFSTR("This is a CFString"); CFRange range; range.location = 0; // Note that CFStringGetLength returns the number of UTF-16 characters, // which is not necessarily the number of printed/composed characters range.length = CFStringGetLength(myCFString); UniChar charBuf[range.length]; CFStringGetCharacters(myCFString, range, charBuf); jstring javaStr = (*env)->NewString(env, (jchar *)charBuf, (jsize)range.length); Creating Native Strings From Java StringsCreating native strings ( Listing 3: Creating an NSString from a Java String const jchar *chars = (*env)->GetStringChars(env, my_jstring, NULL); NSString *myNSString = [NSString stringWithCharacters:(UniChar *)chars length:(*env)->GetStringLength(env, my_jstring)]; (*env)->ReleaseStringChars(env, my_jstring, chars); Listing 4: Creating a CFString from a Java String const jchar *chars = (*env)->GetStringChars(env, my_jstring, NULL); CFStringRef myCFString = CFStringCreateWithCharacters (kCFAllocatorDefault, chars, (*env)->GetStringLength(env, my_jstring)); (*env)->ReleaseStringChars(env, my_jstring, chars); IMPORTANT: Both of these examples finish with a necessary call to Handling Unicode Supplementary CharactersThe previous examples demonstrate conversion of full strings between environments. Accessing single characters or substrings has repercussions when dealing with Unicode supplemental characters (from
Working With Object ReferencesThe rules for using Java object references in native code, as well as C pointers in Java code, are no different on Mac OS X from other platforms. However, the key points in sharing object references across the JNI are worth a brief discussion. References to nearly every type of Java object are represented in the JNI by the generic JNI functions which return pointers to Java code should have a return type of Java-Native Graphical InteractionAside from simply sharing data, the JNI can also be used to access the user interface resources of the underlying platform. There are a number of mechanisms for integrating Java and Cocoa user interface components using the JNI. This section discusses those mechanisms, and the important guidelines for using them successfully. The content and examples below will assume some understanding of Quartz and Cocoa; for familiarity with these technologies see Getting Started With Cocoa and Quartz 2D Programming Guide. AWT Native InterfaceThe AWT Native Interface (JAWT) is the oldest and most universal method for drawing into a Java component with native code. A JAWT-enabled AWT component can use a native library to access Quartz (Core Graphics), Core Image, or any other graphical framework available on Mac OS X. For an introduction to the JAWT and how to use it, see The AWT Native Interface. Sample Code Project 'JAWTExample' demonstrates steps and structures specific to Mac OS X. In addition to native rendering, the JAWT also allows access to platform-specific resources such as (in the case of Mac OS X) a Java component's CocoaComponent
A
This is all that is required to create a trivial Messaging CocoaComponent
Listing 7: Example use of CocoaComponent.sendMessage public void actionPerformed(ActionEvent e) { if (e.getSource() == loadButton) { view.loadComposition ("Particle System.qtz"); } else { view.startRendering(); } } The above Listing 8: Abstracting sendMessage for specific use final static int LOAD_MESSAGE = 0; final static int START_MESSAGE = 1; public void loadComposition(String fullPath) { sendMessage(LOAD_MESSAGE, fullPath); } public void startRendering() { sendMessage(START_MESSAGE, null); } Finally, the Listing 9: Implementation of awtMessage:message:env: responds to Java sendMessage calls - (void) awtMessage:(jint)messageID message:(jobject)message env:(JNIEnv *)env { jchar *chars; switch (messageID) { case apple_dts_samplecode_qccocoacomponent_JavaQCView_LOAD_MESSAGE: // CocoaComponent.sendMessage takes an Object; we need a String here if ((*env)->IsInstanceOf(env, message, (*env)->FindClass(env, "java/lang/String"))) { chars = (*env)->GetStringChars(env, (jstring)message, NULL); NSString *cocoaPath = [NSString stringWithCharacters:(unichar *)chars length:(*env)->GetStringLength(env, message)]; [self loadCompositionFromFile:cocoaPath]; (*env)->ReleaseStringChars(env, message, chars); } else { jclass argExcClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException"); (*env)->ThrowNew(env, argExcClass, "JavaQCView: sendMessage with an ID of LOAD_MESSAGE must be accompanied by a String"); } break; case apple_dts_samplecode_qccocoacomponent_JavaQCView_START_MESSAGE: [self startRendering]; break; default: break; } } Receiving Messages From CocoaComponentCalling back into Java is a little more complicated, although it still involves completely standard JNI practices. Simply displaying a Java dialog to alert the user, for example, does not require a specific point of entry. However, it may be necessary for your Cocoa view to talk directly to its Java peer (the Listing 10: Saving a global reference to a CocoaComponent JNIEXPORT jlong JNICALL Java_apple_dts_samplecode_cwcocoacomponent_JavaColorWell_createNSViewLong (JNIEnv *env, jobject caller) { return (jlong)[JavaColorWell colorWellWithCaller:caller env:env]; } @implementation JavaColorWell + (id) colorWellWithCaller:(jobject) caller env:(JNIEnv *)env { return [[[JavaColorWell alloc] initWithCaller:caller env:env] autorelease]; } // Obtain a JNI global reference to the enclosing Java object // This will be used later to fire AWT events after a color change - (id) initWithCaller:(jobject)caller env:(JNIEnv *)env { self = [super init]; // "javaPeer" is declared in JavaColorWell.h; javaPeer = (*env)->NewGlobalRef(env, caller); return self; } The Cocoa Those familiar with the JNI will realize that the above technique presents a problem: since global reference affect garbage collection, the Listing 11: Relaying removeNotify to the underlying Cocoa view final static int REMOVE_NOTIFY = 0; // Tell the peer we've been removed from the hierarchy public void removeNotify() { sendMessage(REMOVE_NOTIFY, null); super.removeNotify(); } Listing 12: Deleting a JNI global reference in response to removeNotify - (void) awtMessage:(jint)messageID message:(jobject)message env:(JNIEnv *)env { switch (messageID) { // Delete the globalRef to the Java peer when the component is removed from its container case apple_dts_samplecode_cwcocoacomponent_JavaColorWell_REMOVE_NOTIFY: if (javaPeer != NULL) { (*env)->DeleteGlobalRef(env, javaPeer); } break; // more cases... } } Thread-Safe JNI ProgrammingThe examples presented in the previous section are fairly simple in principle: clear entry points are defined, and the Java and Cocoa environments do their respective jobs with no evident change in normal application behavior. Care must be taken, however, when integrating code that makes use of both the Cocoa and AWT event models. While it is true that the AWT in Java 1.4 and later is implemented in Cocoa, the AWT Event Queue runs on a separate thread from the underlying Cocoa application's main event loop (which executes on the main thread, sometimes called "Thread-0"). Using JNI techniques such as JAWT and Note: Although this section uses the term "AWT" almost exclusively, it also applies to Swing applications. The term "AppKit" refers to the Cocoa Application Kit framework and supporting APIs. The javadoc for
When working with The first two items above, apply to any integration of AppKit and AWT ( Calling AppKit From AWT/SwingWhen you make calls to AppKit that may result in a view refresh or some other event-based action, you must make those calls on the main AppKit thread. Isolate your call into a method that can be passed into one of the Listing 13: Using performSelectorOnMainThread for AppKit operations JNIEXPORT void JNICALL Java_apple_dts_samplecode_jsheets_JSheetDelegate_nativeShowSheet (JNIEnv *env, jclass caller, jint type, jobject parent, jobject listener) { // Never assume an AutoreleasePool is in place, unless you are on the main AppKit thread NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; JSheetDelegate *jdel; NSWindow *parentWindow = GetWindowFromComponent(parent, env); switch (type) { case apple_dts_samplecode_jsheets_JSheetDelegate_OPEN_PANEL: jdel = [JSheetDelegate delegateWithListener:listener env:env]; // We retain the delegate for use later; released after use in openPanelDidEnd: [jdel retain]; [jdel performSelectorOnMainThread:@selector(showOpenPanelForWindow:) withObject:parentWindow waitUntilDone:NO]; break; } [pool release]; } - (void) showOpenPanelForWindow:(NSWindow *)parentWindow { NSOpenPanel *op = [NSOpenPanel openPanel]; [op setAllowsMultipleSelection:YES]; [op beginSheetForDirectory:NSHomeDirectory() file:nil types:nil modalForWindow:parentWindow modalDelegate:self didEndSelector:@selector(openPanelDidEnd:returnCode:contextInfo:) contextInfo:nil]; } There are two important things to note here: the call to This technique is necessary even in pure Cocoa applications, for example when a peripheral thread needs to refresh the user interface after receiving information from a socket. At this point you may be asking why none of the previous Calling AWT/Swing From AppKitThe same principle applies in the opposite direction — making AWT calls from AppKit. Say your native code receives a Cocoa notification you are subscribed to. Notifications occur on the main AppKit thread, so making a direct AWT call from your notification handler can easily deadlock your application. The solution is to use the standard Listing 14: Reporting NSSavePanel results to Java - (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo { JNIEnv *env; bool shouldDetach = false; if (GetJNIEnv(&env, &shouldDetach) != JNI_OK) { NSLog(@"savePanelDidEnd: could not attach to JVM"); return; } // Call back to Java if the user clicked Save if (returnCode == NSOKButton) { if (saveFinish_mID != NULL) { jsize buflength = [[sheet filename] length]; jchar buffer[buflength]; [[sheet filename] getCharacters:(unichar *)buffer]; jstring str = (*env)->NewString(env, buffer, buflength); (*env)->CallStaticVoidMethod(env, jDelegateClass, saveFinish_mID, sheetListener, str); (*env)->DeleteLocalRef(env, str); } else NSLog(@"savePanelDidEnd: null Java methodID"); } else if (cancel_mID != NULL) { (*env)->CallStaticVoidMethod(env, jDelegateClass, cancel_mID, sheetListener); } (*env)->DeleteGlobalRef(env, sheetListener); if (shouldDetach) { (*jvm)->DetachCurrentThread(jvm); } [self autorelease]; } Listing 15: Using invokeLater for AWT operations private static void fireSaveSheetFinished(final SaveSheetListener ssl, final String filename) { EventQueue.invokeLater(new Runnable() { public void run() { ssl.saveSheetFinished(new SheetEvent(filename)); } }); } Note: The Calling AppKit or AWT from non-event threadsThe guidelines listed above change very little when dealing with peripheral "worker" threads: AppKit must still be messaged using one of the For more on thread safety in both Java and Cocoa, see Multithreaded Programming Topics: Cocoa Thread Safety and Threads and Swing. Invoking the Java Virtual Machine from Native CodeThe VM Invocation Interface is described in detail in Chapter 7 of The Java Native Interface: Programmer's Guide and Specification. Embedding a JVM into a native Mac OS X application has one major difference from other platforms: If using AWT, the JVM must not be started on the application's main thread. Many tutorials and documents start the JVM on the main thread, so it is important to recognize this unique requirement of Mac OS X. The following sections explain how to correctly create a JVM from both Core Foundation and Cocoa environments, as well as how to request an explicit version of Java. Creating a JVM from Core FoundationCore Foundation developers wishing to create a JVM must create a new thread (using the Listing 16: Creating a new pthread to invoke a JVM /* Start the thread that runs the VM. */ pthread_t vmthread; /* create a new pthread copying the stack size of the primordial pthread */ struct rlimit limit; size_t stack_size = 0; int rc = getrlimit(RLIMIT_STACK, &limit); if (rc == 0) { if (limit.rlim_cur != 0LL) { stack_size = (size_t)limit.rlim_cur; } } pthread_attr_t thread_attr; pthread_attr_init(&thread_attr); pthread_attr_setscope(&thread_attr, PTHREAD_SCOPE_SYSTEM); pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED); if (stack_size > 0) { pthread_attr_setstacksize(&thread_attr, stack_size); } /* Start the thread that we will start the JVM on. */ /* startupJava is a separate function that creates the JVM */ pthread_create(&vmthread, &thread_attr, startupJava, launchOptions); pthread_attr_destroy(&thread_attr); Creating a JVM from CocoaThe principles behind invoking a JVM from Cocoa are the same as from Core Foundation. Cocoa developers can use the Listing 17: Detaching a new NSThread to invoke a JVM - (IBAction)applicationWillFinishLaunching:(id)sender { // Detach a new thread, and in that thread invoke the VM. [NSThread detachNewThreadSelector:@selector(startupJava:) toTarget:self withObject:nil]; } - (void)startupJava:(id)userData { // All new native threads (Cocoa and Java) need an autorelease pool. NSAutoreleasePool *pool = [[NSAutoreleasePool allocWithZone:NULL] init]; // Startup the JVM (startupJava is a C function defined elsewhere) startupJava(); [pool release]; // Once the JVM has exited we will want to exit this application. [[NSApplication sharedApplication] terminate:self]; } Staying off the Main ThreadIt is important to understand that the JVM cannot be started from the native application's main thread if your Java code uses an AWT/Swing-based GUI; in such applications the main thread must be kept free for use by Cocoa's event loop. Both of the above examples abide by this rule: Listing 16 starts a Listing 18: Error initializing AWT after starting Java on the main thread Exception in thread "main" java.lang.InternalError: Can't start the AWT [...] at java.lang.ClassLoader$NativeLibrary.load(Native Method) at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1751) at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1668) at java.lang.Runtime.loadLibrary0(Runtime.java:822) at java.lang.System.loadLibrary(System.java:992) at sun.security.action.LoadLibraryAction.run(LoadLibraryAction.java:50) at java.security.AccessController.doPrivileged(Native Method) at sun.awt.NativeLibLoader.loadLibraries(NativeLibLoader.java:38) at sun.awt.DebugHelper.<clinit>(DebugHelper.java:29) at java.awt.Component.<clinit>(Component.java:545) Native launchers running "headless" Java code — code that does not use AWT — do not have this requirement and can safely start the JVM on the main thread. However, if there is any chance that your invoked Java code will use the AWT event queue, keep the main thread free. Regardless of your situation, it is highly recommended to minimize risk by starting the JVM from a non-main thread. Requesting a specific J2SE versionThe presence of multiple Java versions on Mac OS X presents a dilemma for native launchers: selecting the Java version you actually want. This problem is solved by defining a A few things to remember when using the invocation API on Mac OS X:
Sample Code Project 'simpleJavaLauncher' declares a Listing 19: Detecting and requesting J2SE 5.0 from Core Foundation CFStringRef targetJVM = CFSTR("1.5"); CFBundleRef JavaVMBundle; CFURLRef JavaVMBundleURL; CFURLRef JavaVMBundlerVersionsDirURL; CFURLRef TargetJavaVM; UInt8 pathToTargetJVM [PATH_MAX]; struct stat sbuf; // Look for the JavaVM bundle using its identifier JavaVMBundle = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.JavaVM") ); if(JavaVMBundle != NULL) { // Get a path for the JavaVM bundle JavaVMBundleURL = CFBundleCopyBundleURL(JavaVMBundle); CFRelease(JavaVMBundle); if(JavaVMBundleURL != NULL) { // Append to the path the Versions component JavaVMBundlerVersionsDirURL = CFURLCreateCopyAppendingPathComponent (kCFAllocatorDefault,JavaVMBundleURL,CFSTR("Versions"),true); CFRelease(JavaVMBundleURL); if(JavaVMBundlerVersionsDirURL != NULL) { // Append to the path the target JVM's version TargetJavaVM = CFURLCreateCopyAppendingPathComponent (kCFAllocatorDefault,JavaVMBundlerVersionsDirURL,targetJVM,true); CFRelease(JavaVMBundlerVersionsDirURL); // Verify the desired major version exists in the framework if(TargetJavaVM != NULL) { if(CFURLGetFileSystemRepresentation (TargetJavaVM,true,pathToTargetJVM,PATH_MAX )) { if(stat((char*)pathToTargetJVM,&sbuf) == 0) { if(CFStringGetCString(targetJVM, (char*)pathToTargetJVM, PATH_MAX, kCFStringEncodingUTF8)) { setenv("JAVA_JVM_VERSION", (char*)pathToTargetJVM,1); } } CFRelease(TargetJavaVM); } } } } } Further ReadingDocument Revision History
|