Creating JNIs in XCode

-

Friday, August 21st, 2009

Here at Rain we often argue over which language is the best.  Although I am primarily an ActionScript developer, my heart still belongs with Java.  Even though Java is a strict language and mistakenly has a bad rap for performance, I still enjoy it quite a bit because Sun has supported it so well, and it is now open-source which gives you access to all sorts of community libraries and samples.  However, the other day I came across something that simply did not exist in the community and the only way I would be able to accomplish the task was to write some native C code and use JNI to hook it up to Java.

Using XCode, I found that it was really easy to create a JNI, it just took a little effort and lots of googling.  To save some effort for the next go-round, I decided to write about how to create a JNI library in XCode.  Note that I am using version 3.1.2 of XCode.

Creating an XCode JNI project

First, go to File->New Project. Navigate down to the Java section and choose Java JNI Application. Then pick a name, I chose MyJNITest, and hit OK. You should have a fully configured project ready to go. You should see a src directory which contains a Java file and a C file, a resources directory which contains a Manifest used to build your jar, and some empty folders lib, bin, jars, and dist. First, go to Run->Console to open up your console, and then click on the “Build and Go” button. Voila! You should see a few trace statements called from the Java side of the application, as well as the C side of the application.

JNI_success

Now let’s dig into what is actually going on behind the scenes.

Understanding the Generated Code

Java side

First, open up the src directory and look at MyJNITest.java. Inside of the class definition, you will see a static block which tells the system to load the JNI named “MyJNITest”. This needs to happen before any calls are made to the JNI library so it makes sense why this line of code is executed before any other line. Underneath the constructor, you will see a function defined as:

native int native_method(String arg);

The “native” keyword defines a method that exists within your JNI Library C code. Finally, you have your main method which is used to create a new instance of MyJNITest, perform a call to the native_method defined in your C code, and display the result it receives from the native code.

C Code

If you click on the MyJNITestjnilib.c file, you will see two functions and an include statement. The include statement tells the C code to include a header file called “MyJNITest.h”. In most cases, you don’t even need to touch this header file because it is autogenerated automatically by XCode using the javah task. If you want to see what is autogenerated, open up Finder and go to MyJNITest/build/Release/Headers. If you double click on the .h file, you will see a method signature

JNIEXPORT jint JNICALL Java_MyJNITest_native_1method
(JNIEnv *, jobject, jstring);

which matches the method declaration in your .c file. XCode autogenerates this file whenever you build and run the application. It looks for any native defined functions in the .java file and creates a header definition that matches. Looking at this header file can be extremely important as you may want to copy the method declarations that are autogenerated and then paste them into your .c file to reduce the chance of miskeying the method signatures.

Now let’s look at the method signature in the header file (or the .c file, they are the same). The syntax for all JNI functions goes something like this:

JNIEXPORT return_type JNICALL Java_package_class_method(JNIEnv *env, jobject this, ...args)

In the case of your default application, the Java file is in the default package, so you won’t see a package in the JNI method name. Note that if you want to change the package for your JNI, it’s not as easy as you may think. I recommend this tutorial on changing your JNI package.

Adding to the default JNI Application

So, let’s be honest… printing out to the console is pretty lame. So, for our next step, we will create a native method to play a midi file through Core Audio. First, go to the .java file and add a new native method under the native_method declaration. I’ll do one called:

public native void playMidiFile(String path);

the path in this case is a URL to a Midi file on your machine. Note that I have made the method public so we can access it from other Java projects. Now, let’s build the application in order to re-generate the header file (make sure you have no errors, or the build will fail and your header file will not be updated). In the header file, you will see another method declaration that looks like this:

JNIEXPORT void JNICALL Java_MyJNITest_playMidiFile
(JNIEnv *, jobject, jstring);

Copy this declaration and paste it into the .c file underneath the native_method declaration. Now, let’s implement this method by doing the following:

JNIEXPORT void JNICALL Java_MyJNITest_playMidiFile(JNIEnv *env, jobject this, jstring midiURL)
{
// Need to convert jstring to a char* and then to a CFURLRef.
const char *chars = (*env)->GetStringUTFChars(env, midiURL, JNI_FALSE);
CFURLRef fileURL = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8 *)chars, strlen(chars), FALSE);

// Now convert the CFURLRef to a FSRef.
FSRef fileRef;
CFURLGetFSRef(fileURL, &fileRef);

MusicSequence sequence;
MusicPlayer player;

// Initialize the sequence and player.
NewMusicSequence(&sequence);
NewMusicPlayer(&player);

// Load the MIDI file into memory…
MusicSequenceLoadSMFWithFlags(sequence, &fileRef, kMusicSequenceLoadSMF_ChannelsToTracks);

// Set the sequence into the player.
MusicPlayerSetSequence(player, sequence);

// Tell the player to start playing the file.
MusicPlayerStart(player);
}

Now save and build the application and you should see a million errors in our .c file. This is because we haven’t imported the libraries we need to use. First, right-click on the MyJNITest project, choose “Add”, and then select “Add Existing Frameworks”. Then locate AudioToolbox.framework and choose “Add”. When the next dialog comes up, make sure that the JNILib Target is checked and the MyJNITest is not. Select “Add” again. Repeat the same process to add the CoreFoundation.framework as well.

Next, we need to tell our .c file to use these libraries. Underneath the line:

#include "MyJNITest.h"

enter the following lines:

#include <AudioToolbox/AudioToolbox.h>
#include <CoreFoundation/CoreFoundation.h>

finally, Save and Build and you should see no errors. However, our application will still not play our MIDI file unless we call the method from the Java end.

To do this, we will add some Java code to the main method underneath the generated code:

// Replace /Users/nateross/Desktop/trippygaia1.mid with a path to your midi file.
newjni.playMidiFile("/Users/nateross/Desktop/trippygaia1.mid");

try
{
// Tell Java to wait for 10 seconds so we can hear the MIDI.
Thread.sleep(10000);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}

this will tell our native c code to play a midi file for 10,000 milliseconds and then the application will finish, which stops the midi playback (ideally you would implement a stop method, but I won’t do that here). Now, build the application and run it and you should hear MIDI!

Adding the JNI library into another Java project

Now that we have built our JNI, it’s time to import it into another Java project. To do this, copy the .jar and .jnilib files that have been generated in MyJNITest/dist. Place the .jnilib in your Java global extensions directory (/Library/Java/Extensions) so it can be used by all Java applications running on your machine. Then, copy the .jar file into the library for your new project. Now, all you have to do is import the JNILib

import MyJNITest;

and then call it’s playMidiFile native method by doing this:

MyJNITest jniTest = new MyJNITest();
jniTest.playMidiFile("/Users/nateross/Desktop/trippygaia");

and you should be good to go!