Java and Cocoa on Mac OS X

How to better integrate Java application into Mac OS X and use Mac OS X features in a Java application.


Introduction

I have recently come to Mac OS X and as a Java developer I have tried to better integrate my application with Mac OS X. Although Apple does not help Java developers much, there is a solution that lets you access any Cocoa library from within a Java application. It is called Rococoa and is build on top of JNA (Java Native Access). One disadvantage is that Rococoa does not currently work with Java 6 on Leopard, so you have to stick with Java 5 until universal 32/64-bit version of Rococoa will be available.

JNA

Java Native Access (JNA) enables any Java application to access native libraries. It is a successor of previously available Java Native Interface (JNI). JNA has one several advantage over JNI - you don't have to write a single line of native (C/C++/Obj-C etc.) code, so you use only Java. Of course, the native libraries you use must be available, so you must either provide them with your application or use built-in system libraries.
Using JNA may lock your Java application to a single platform, when you use a native non-cross-platform library. This may require writing the code using native libraries multiple times for different libraries on different platforms. You can also omit support for some features on some platforms - for example you can support Time Machine on Mac OS X, but as there is no equivalent of Time Machine in other operation systems, you won't support it on Windows, Linux etc.

Rococoa

Rococoa[1] is built on top of JNA and basically translates calls to Java methods to Objective-C and then translates the result back to Java. Rococoa is relatively thin layer and is very simple to use. A good example how to start with Rococoa is here: https://rococoa.dev.java.net/rococoa-whistlestop.html . The idea is - you create an interface (or abstract class) that mimics an Objective-C class you want to use. This means that method names, parameters and return types in you interface correspond with the Objective-C class. Rococoa provides interfaces for some Objective-C classes, so you can right out of box play a QuickTime movie etc.

Making Mac OS X say "Hello World!"

Hello World is a good example to show using Cocoa from a Java application. The initial reason why I tried Rococoa was speech synthesis I needed for a project. As Mac OS X Leopard has quite a good synthesized voice "Alex" I decided that Rococoa could be the simplest way to use basic speech features. Remember that the example currently works only with Java 5 (until 64-bit version of Rococoa is available).

Making Objective-C compatible interface

First you can study how an Objective-C (Cocoa) class looks like. You can use Cocoa Reference Library [2] for that. I have used NSSpeechSynthesizer reference to create a simple speech synthesis application. You will probably find a lot of classes in Apple documentation you might like to use in your Java application.
After studying an Objective-C class you create a Java interface that mimics the original class. You don't have to implement the interface. Rococoa will do that for you. You also do not have to copy all methods, but only the methods you need in your application. But copying all is not so hard and allows you to use all features of the class. This is how my interface looked like:

public interface NSSpeechSynthesizer extends NSObject {

final _Class CLASS = Rococoa.createClass("NSSpeechSynthesizer", _Class.class);

interface _Class extends NSClass {

NSArray availableVoices();

NSDictionary attributesForVoice(String voiceIdentifier);

String defaultVoice();

boolean isAnyApplicationSpeaking();
}

NSSpeechSynthesizer initWithVoice(String voice);
boolean usesFeedbackWindow();
void setUsesFeedbackWindow(boolean use);
String voice();
void setVoice(String voice);
float rate();
void setRate(float rate);
float volume();
void setVolume(float volume);
void addSpeechDictionary(NSDictionary dictionary);
ID objectForProperty_error(String speechProperty, NSError error);
boolean setObjectForProperty_error(ID object, String speechProperty, NSError error);
ID delegate();
void setDelegate(ID delegate);
boolean isSpeaking();
boolean startSpeakingString(String text);
boolean startSpeakingString_toURL(String text, NSURL url);
void continueSpeaking();
void stopSpeaking();
String phonemesFromText(String string);
}

Notice that the above interface contains an inner field CLASS of type _Class. The class _Class provides access to static methods of the Objective-C class (as Java does not allow static methods in an interface, you have to do that way). Static methods in Objective-C start with plus "+" sign, so all methods having plus in their declaration should be put to inner class _Class.
Also notice that the methods are exact copies of the methods in Apple's NSSpeechSynthetizer documentation. For example:
- (BOOL)startSpeakingString:(NSString *)text
from Apple's documentation is translated into Java as:
boolean startSpeakingString(String text);
The method names should be same with the original class, so Rococoa is able to decide what Java method is mapped to which Objective-C method. Rococoa does some basic type conversions for you. So you can use Java String instead of having to use NSString (but you can use NSString if you really want to).

Using the interface

public final class Test {

public static void main(String[] args) throws InterruptedException {
NSAutoreleasePool pool = NSAutoreleasePool.new_();
try {
NSSpeechSynthesizer synthesizer = Rococoa.create("NSSpeechSynthesizer", NSSpeechSynthesizer.class);
synthesizer.setVolume(1.0f);
String s = "Hello World!";
synthesizer.startSpeakingString(s);
System.out.println("synthesizer = " + synthesizer.phonemesFromText(s));

                        // just a very ugly way how to wait until the "Hello World" is said
Object object = new Object();
synchronized (object) {
object.wait(10000);
}
}
finally {
pool.release();
}
}
}

As you can see using our interface is pretty straightforward:
  1. we create a NSSpeechSynthetizer instance by calling Rococoa.create method and we pass the Object-C class name and our Java interface
  2. we set volume to max
  3. we make synthesizer say "Hello World!"
  4. we print to console phonemes of the "Hello World!" (this means a transcript how the text is pronounced by synthesizer)
The NSAutoreleasePool.new_() and pool.relase() just help to manage garbage collection of Objective-C objects.

Conclusion

Using native libraries in Mac OS X is really simple. You don't have to write anything in language other than Java. You just have to create interface that mimics an Objective-C class you want to use. You don't have to implement the interface - Rococoa will do it for you. After retrieving an instance implementing your interface by calling for example Rococoa.create method, you use the interface as any other Java interface.
Good luck with Java, Rococoa and JNA. If there is anything that is hard to understand or is not clear, please let me know. Below you have links to start with.

Comments

Vaclav Slovacek
Vaclav Slovacek
PhD. Student
Prague, Czech Republic
Article rating:
Your rating:

Activity for this knol

This week:

28pageviews

Totals:

1698pageviews