The Java Native Interface (JNI) lets developers mix Java code with C or C++ source code. It provides access to platform-specific features that Java does not provide, so that Java applications on Mac OS X can take advantage of the wide range of powerful Apple-only technologies to provide the best possible user experience. Using the JNI, your Java application can access a user's Address Book, make Spotlight queries, take advantage of Core Image and Core Video, and leverage many other rich features unique to Mac OS X. This step-by-step tutorial explains how to use the JNI to integrate Java and C code using a single Xcode project with multiple targets. In addition, the resulting JNI library will be a universal binary, allowing the Java application to run properly on both Intel-based and PowerPC-based Macs. Note: Java applications that wish to access native code on both Intel-based and PowerPC-based Machintosh computers must provide universal JNI libraries since the Java Virtual Machine does not run under Rosetta. See Technical Q&A QA1295 for more information. What You Need to Know to Get StartedTo get the most out of this article, you'll need a basic grasp of Xcode, some knowledge of both C and Java, and at least a limited understanding of the JNI. Review Apple's Xcode documentation for more on Xcode. Sun has excellent documentation for both Java and the JNI available online on the Sun Java website, and Technical Note TN2147 discusses the details of JNI programming on Mac OS X. We'll be using Xcode version 2.2 for this article with the All-In-One window configuration. Be aware that the screen shots in this article may look a bit different that what you see on your screen if you're using a different version of Xcode or you have the window configuration set up differently. Most of the source code for our example project comes from the ADC Sample Code project MyFirstJNIProject. If you've explored that sample already or you attended the JNI session at last year's (2005) World Wide Developers Conference, then the code should look familiar. However, we're going to cover some additional information in this article beyond what was discussed at that WWDC. In particular, we'll explain in more detail how to build a universal version of the JNI library so that your Java application will run on both Intel-based and PowerPC-based Machintosh computers. In addition, we'll show how to control whether or not the JNI library gets loaded based on whether or not your Java application is running on Mac OS X or not. About Our JNI ProjectIn this article, we're going to start with a standard Swing Application project. As we proceed through the tutorial, we'll make small modifications to the source and project settings. We'll add two additional targets to build a header file for the native method and the native library itself. For the native code portion, we're going to follow the MyFirstJNIProject sample and retrieve the current user's full name using the Address Book framework. Step-by-Step GuideWe'll start by creating a Java Swing Application project in Xcode. Creating the Initial ApplicationLaunch Xcode and select "New Project..." from the File menu. Scroll down and select "Java
Swing Application" from the list (Figure 1). Give the project a name (we've
chosen ![]() Figure 1: Choosing the Java Swing Application Project Template. ![]() Figure 2: Creating a project named
If you build and run this new project, you'll notice one of two things. If
you've already upgraded to Java 1.5 (also called J2SE 5.0) and you see an error
in the Run Log that says " ![]() Figure 3: Setting the Target JVM Version. ![]() Figure 4: The Java Swing Application Window. Adding the Native Method Declaration and Call
Now that we've got the sample up and running, we're ready to add our native
method. Open up the public native String getMyFullName();
Also change the ![]() Figure 5: The Exception You'll See if You Run the Project without the Loading the Dynamic Library
The problem is that unlike Java class files, Java native libraries don't get
loaded automatically. We'll need to add one more line of Java code to load the
JNI library that contains the native implementation of the static { System.loadLibrary( "MyFirstJNILib" ); } The final code should look like Figure 6. ![]() Figure 6: The Modified Code for
The static code will load the library as soon as the Creating the Header File
Add a new target to your project by selecting New Target... from the Project
menu and choosing Java Package from the list (Figure 7). Call the new target
![]() Figure 7: Creating the ![]() Figure 8: Naming the Target.
The purpose of this target is to create a header file, ![]() Figure 9: Dragging the Java Files to the
The ![]() Figure 10: Adding the Run Script Build Phase from the Project Menu. You can also bring up the contextual menu for the JavaHeaders target (control-click or right-click) and select "New Run Script Build Phase" from the "New Build Phase" submenu in the Add submenu as shown in Figure 11. ![]() Figure 11: Adding the Run Script Build Phase from the Contextual Menu. Whichever way you choose to add the new build phase, Xcode should bring up the information window for the new phase. Type the following shell script (all on one line - a simple copy and paste will break the script into two lines) into the Script text box (see Figure 12): javah -classpath "${BUILT_PRODUCTS_DIR}/JavaHeaders.jar" -force -o "${BUILT_PRODUCTS_DIR}/MyFirstJNIProject.h" "MyFirstJNIProject" ![]() Figure 12: The
If you switch over to and build the ![]() Figure 13: Selecting the
The new header file should contain a lot of machine-generated code and
definitions but the important part is about halfway down. Search for
/* * Class: MyFirstJNIProject * Method: getMyFullName * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_MyFirstJNIProject_getMyFullName(JNIEnv *, jobject);
Notice that the machine-generated version has prepended "Java" and the name of
the project onto the name of the native method. Also notice that there are two
parameters to the C function: a pointer to the JNI environment ( Adding the JNI Library Target
We're finally ready to create the JNI library. Add a new BSD Dynamic Library
target (Project -> New Target... -> BSD Dynamic Library) to the project and name
it
Add a new C file to the ![]() Figure 14: Adding a New BSD C File to the ![]() Figure 15: Do not Create
Open the #include "MyFirstJNIProject.h" #include <AddressBook/AddressBook.h> Open the /* * Class: MyFirstJNIProject * Method: getMyFullName * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_MyFirstJNIProject_getMyFullName(JNIEnv *env, jobject obj) { }
For the implementation of this native function, we'll turn to the
// Find myself in the Address Book ABAddressBookRef book = ABGetSharedAddressBook(); ABPersonRef me = ABGetMe( book ); // Retrieve my first and last name CFMutableArrayRef names = CFArrayCreateMutable( kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks ); CFStringRef firstName = ABRecordCopyValue( me, kABFirstNameProperty ); CFStringRef lastName = ABRecordCopyValue( me, kABLastNameProperty ); if ( firstName && CFStringGetLength( firstName ) > 0 ) { CFArrayAppendValue( names, (const void*)firstName ); } if ( lastName && CFStringGetLength( lastName ) > 0 ) { CFArrayAppendValue( names, (const void*)lastName ); } // Merge the first and last names into one CFString CFStringRef firstAndLastName = CFStringCreateByCombiningStrings( kCFAllocatorDefault, names, CFSTR( " " ) ); // Release the first and last name strings CFRelease( firstName ); CFRelease( lastName ); // Retrieve the Unicode characters from the CFString CFIndex strLen = CFStringGetLength( firstAndLastName ); UniChar uniStr[ strLen ]; CFRange strRange; strRange.location = 0; strRange.length = strLen; CFStringGetCharacters( firstAndLastName, strRange, uniStr ); // Make a jstring from the full name's bytes jstring javaName = (*env)->NewString( env, (jchar*)uniStr, (jsize)strLen ); // Clean up CFRelease( names ); CFRelease( firstAndLastName ); // Return the Java string return javaName;
Now, add the native frameworks to the project that the ![]() Figure 16: Adding Frameworks to the In the standard file dialog box that follows, you can select the frameworks one at a time or all three at once. When you add the frameworks, you'll see a dialog box like the one in Figure 17. ![]() Figure 17: Making Sure that the Frameworks Get Added to the Correct Target.
Make sure that only the ![]() Figure 18: The Three Frameworks Added to the
In order to compile the native library code, we'll need to make one more set of
changes, this time to the ![]() Figure 19: The Info Window General Tab for
First, add a dependency on the $(SDKROOT)/System/Library/Frameworks/JavaVM.framework/Headers ![]() Figure 20: The Info Window Build Tab for Once you've edited the entry, the dialog should look like Figure 21. ![]() Figure 21: The New Header Search Paths Entry.
If you've changed the names of any of the files or libraries as you've been
following along, there are several very important connections to watch out for.
First, the ![]() Figure 22: Checking the Product Name Setting. ![]() Figure 23: Checking the Executable Prefix Setting.
There are two more settings that you need to change in this dialog. The
![]() Figure 24: Changing the Executable Extension to
At this point, a placeholder for the JNI library should appear under the
Products group in the Groups & Files view. Assuming that you're using the same
file and project naming convention, the entry should be named
![]() Figure 25: The Placeholder for the Unbuilt JNI Library.
The other setting that you need to change for the ![]() Figure 26: Building a Universal Binary.
There is one final setting we need to change: the
![]() Figure 27: Changing the Target SDK to Mac OS X 10.4 (Universal)..
Now we can finally build the Connecting Everything Together
We can build each target individually, and the
Our three targets will now build in the correct sequence but we'll need to add
one last step in order to build a complete application. Building the
Note: if you build and run the application at this point, it will run just fine
from within Xcode. However, if you copy the application somewhere else, it will
fail with the same
Adding a Copy Files Build Phase to the ![]() Figure 28: Adding a New Copy Files Build Phase to the ![]() Figure 29: Changing the Destination to ![]() Figure 30: Dragging the ![]() Figure 31: The Final Target List.
Clean all of the targets, select the ![]() Figure 32: The Final Result. Variations on This ThemeThere are three approaches you may wish to consider as you build and use your JNI libraries. First, in this example, we built both our native code and the system libraries into a single BSD Dynamic Library target. Depending on your requirements, you may wish to keep the system libraries separate from your native code, although the difference is mostly organizational. Second, if you are already using an IDE other than Xcode for your Java development, you can still use the techniques described in this article to build your JNI library by isolating the Java code in your application that makes native calls. Partition those classes out into one or more separate classes and incorporate only those classes into your Xcode project. If you don't need to use Xcode to build a Java application (perfect for testing your native library), you can also leave out the Java Swing Application target and focus on the JavaHeaders and JNI library targets. Third, for cross-platform applications, you can even make sure that your Java application only loads the Mac OS X version of your JNI library when you are running on Mac OS X. Add the following line of code in a static block in your main application class so that it runs before any other classes are loaded. boolean MAC_OS_X = System.getProperty("os.name").toLowerCase().startsWith("mac os x");
In any class that makes native calls or loads native libraries, check the value
of the public String methodInAnotherClass () { if (MAC_OS_X) { return com.companyname.nativewrappers.NativeAddressBookAccess.getFullName(); } else { return "The AddressBook is only available on Mac OS X"; } }
In this way, the only reference to the class containing the native ConclusionIn this article you've learned how to apply Xcode and the JNI to integrate Java and C code using a single Xcode project with multiple targets. In addition, we've built the resulting JNI library as a universal binary, allowing the sample Java application to run properly on both Intel-based and PowerPC-based Macs. And finally, we've created a Java application that takes advantage of one of the many Mac OS X-specific technologies, the Address Book framework, to provide a better user experience. This article serves as a starting point—you can use this knowledge now to add other Mac OS X technologies to enrich and expand your Java application. Resources
Updated: 2006-03-27 |