« The Java Bridge
If you try to use Cocoa-Java, many things will simply not work as you expect. If you want to start a JVM from Cocoa or use AppKit controls from Java, you can use the JNI, as described in Technote 2147.
The Java Bridge is the worst documented part of Mac OS X (well, except those 'Description forthcoming' things, but at least they know that these parts are lacking!). The worst part of it is the documentation on how to use Java classes from Objective-C. This also happens to be something I've used extensively, so that is the topic of this article. This is one of the few "Undiscovered Countries" of Objective-C (you should feel like a pioneer now!).
At first glance, Java is much more like C++ than Objective-C, but don't be fooled by the wrapping: Java can be just as dynamic as Objective-C. That's why this Bridge is only possible with these two languages (and Python).
// Why Should I Use Java?
Maybe you read that you shouldn't use Java in Cocoa. That's true (in my opinion), but the Java Bridge enables you to write your application partly in Java and partly in Objective-C. This enables you to do the following (in agreement with the MVC model):- Write the Controller part in Objective-C, for easy integration and high speed.
- Write the Model part in Java, which can be kept cross-platform to enable easy porting.
// Implementation
As a rule of thumb, you can treat Java classes just like Objective-C classes. There are just two exceptions:
- The initialization is different.
- You have to write the header for yourself (if you don't want hundreds of compiler warnings).
// How to Initialize
First, you have to add the key "NSJavaNeeded" to your Application Settings (Command-Opt-E in PB) with the string-value of "YES". This will tell the runtime to load Java when the application launches.
From Apple's documentation:
MyJavaClass = NSClassFromString(@"java.util.AJavaClass"); MyJavaObj = [[MyJavaClass alloc] init];
I like to write it that way:
id vector=[[NSClassFromString(@"java.util.Vector") alloc] init];
Now you can use it just as if it were Objective-C:
[vector add:@"one item!"]; NSLog(@"item 1=%@",[vector get:0]); [vector release];
This should output "item 1=one item!"
Calling a method with two arguments works this way (note that there's an error in the Apple docs here):
[vector add:0 :@"Inserted at the beginning"];
// Converting Objects
You may have noticed that I sent @"one item!", which is an NSString, to a Java class. How does a Java class handle it?
Simply: it doesn't. All NSStrings are automatically converted to java.lang.Strings, and vice versa (see the second line in the example above).
However, since all Cocoa API classes are properly wrapped to behave like Java classes, you can send them over the bridge. That does not work with your own objects without manual wrapping! But there's no problem with accessing Java objects you got from a Bridge call.
Additionally, Java's booleans are properly converted to BOOLs, and integers work, too (I haven't tried floats yet). There should be no problem. Converting a java.util.Date to NSDate is a bit more complicated. This is the method I use:
+ (NSDate*)NSDateFromJavaDate:(id)javaDate { // [javaDate toString] format: "dow mon dd hh:mm:ss zzz yyyy" if(javaDate==nil) return nil; return [NSCalendarDate dateWithString:[javaDate toString] calendarFormat:@"%a %b %d %H:%M:%S %Z %Y"]; }
That should help you to get started. Look at the bottom of this tutorial to get more information about Java arrays.
// Writing Those Header Files
Writing header files is optional, but some folks don't like compiler warnings, so here is an example:
@interface Enumeration : NSObject {} - (BOOL)hasMoreElements; - (id)nextElement; @end @interface Vector : NSObject {} - (Enumeration*)elements; - (id)get:(int)index; - (int)size; - (void)set:(int)index :(id)object; @end
That's a part of java.util.Enumeration and java.util.Vector (you only need to implement the things you actually call). Now you have to tell the compiler that it should use these definitions:
void example() { Vector *test=[[NSClassFromString(@"java.util.Vector") alloc] init]; Enumeration *iter=[test elements]; while([iter hasMoreElements]) { NSLog(@"%@",[iter nextElement]); } [test release]; }
// Reference Counting vs. Garbage Collection
No, this isn't a discussion on what's better, but how they are used when mixed. This is a part Apple did really well. In Objective-C, all classes (even Java classes) use reference counting. In Java, all classes (even Objective-C classes) use Garbage Collection. That means you still have to release all Java objects you have allocated. End of discussion. Or is it "Discussion forthcoming?"
// What About Non-Empty Constructors?
NOTE: This feature is broken in 10.0.x. You need 10.1 or later to use it.
Believe it or not, Apple actually forgot to document this. Let's pretend you want to call this Java constructor:
class Testclass { public Testclass(String test) { System.out.println(test); } }
It's not possible to access it using the above [[class alloc] init] way. Even the Apple employees I asked didn't know the solution I accidentally found when I looked through the headers on my system:
id Testclass=[NSClassFromString(@"Testclass") newWithSignature:@"(Ljava/lang/String;)",@"Hello there!"];
(Note that '[class new]' is the same as '[[class alloc] init]' in terms of reference counting). That first argument (the signature) describes the constructor's parameters, it's documented in the Java specs. For easy accessibility, here's the important part:
BaseType Character Type Interpretation B byte signed byte C char Unicode character D double double-precision floating-point value F float single-precision floating-point value I int integer J long long integer L<classname>; reference an instance of class <classname> S short signed short Z boolean true or false [ reference one array dimension
For historical reasons the syntax of fully qualified class and interface names that appear in class file structures differs from the familiar syntax of fully qualified names documented in section 2.7.5. In this internal form, the ASCII periods ('.') that normally separate the identifiers that make up the fully qualified name are replaced by ASCII forward slashes ('/'). For example, the normal fully qualified name of class Thread is java.lang.Thread. In the form used in descriptors in the class file format, a reference to the name of class Thread is implemented using a CONSTANT_Utf8_info structure representing the string "java/lang/Thread".
OK, enough copied. (Are you still there? Hello??)
For example, the constructor public Testclass(int a, String b, boolean c); is identified by @"(ILjava/lang/String;Z)".
// Defining the Classpath
If you want to access your Java classes, you usually have to define where the Java runtime should look for them. I won't go into many details here: that's something for the Bare Basics section of this site. You have to add the following keys to your expert application settings:
Key | Type | Comment |
---|---|---|
NSJavaRoot | String | The '.' directory for the classpath (a relative path from the application wrapper, so it should always begin with "Contents/...") |
NSJavaPath | Array of Strings | All classpath entries in an array |
// Using The Bridge In Foundation Tools
In tools, additional steps have to be taken in order to load the Java VM.
- Add the JavaVM-framework (located in /System/Library/Frameworks) to your project.
- Insert "#import <JavaVM/JavaVM.h>" to the top of your main source file.
- Before using the Bridge the first time, insert "[NSJavaVirtualMachine defaultVirtualMachine];" to load it (this command may take some time).
- If you want to specify the class path, use the following command instead (this example adds the PostgreSQL jar-file):
[[NSJavaVirtualMachine alloc] initWithClassPath: [[NSJavaVirtualMachine defaultClassPath] stringByAppendingString:@":/usr/local/pgsql/share/java/postgresql.jar"]];
In regular Cocoa-apps, these steps are done by the Cocoa framework using the keys in the target's build settings.
That's all folks. Go and code some nice Objective-C/Java apps!
// Additional Information
John DeNisi sent me a mail with some additional information he got by "trying things once or twice", on some small test programs, not through any rigorous testing protocol. As always, your mileage may vary...
I found that Java booleans, shorts, ints, longs, floats, doubles and Strings map nicely across the Java bridge as long as you declare the appropriate Java classes/methods in Objective-C .h header files.
Java Number objects also auto-morph nicely to Objective-C NSNumber objects (just like String <-> NSString), as long as you don't try to directly pass Java Number sub-classes.
However, bytes don't work properly. When negative byte values are returned from Java to ObjC, they appear to get cast to unsigned values. I also haven't found any way to directly map Java Unicode (16-bit) char types.
Attempting to directly return Java primitive arrays to Objective-C causes the Java bridge to blow up! However, if you first cast them to the generic Java "Object" type on the Java side and return that "Object", you can then successfully get it as an "id" in Objective-C and use the static methods of the Java.lang.reflect.Array class to access elements one at a time. It's probably inefficient, but does at least give you a way to access the elements of Java primitive arrays from Objective-C.
Finally, your article didn't mention another very nice aspect of the Java bridge: Uncaught Java Exceptions thrown by Java code called from Objective-C are nicely auto-morphed to Objective-C NSException objects and re-thrown up the Objective-C calling chain. I found this critical to successfully getting my test app to work [andy: This is mentioned in the Apple docs].
I tried to use some java classes from Objective-C code, refering this page. But I received (null) from
NSClassFromString(@"java.util.Vector");
I turned the 'requires Java' switch on, and specified NSJavaRoot & NSVavaPath...
Any suggestion?
thanks.
I too am having the problem hiro explained... you seem to have found a way to do this bridging yourself, but for some reason I am not able to reproduce your results, perhaps there is some other project builder setting that needs to be set somewhere that I havn't found, but as it stands, your: well written, easy to follow, and informative description of using the java bridge... is useless.
Posted by: Jacob on June 30, 2003 06:26 PMI got this working without any problems. Just used the simple part...
[NSJavaVirtualMachine defaultVirtualMachine];
id vector=[[NSClassFromString(@"java.util.Vector") alloc] init];
[vector add:@"one item!"];
NSLog(@"item 1=%@",[vector get:0]);
[vector release];
and changed my build settings (note: not application settings) to NSJavaNeeded=YES. I couldn't find anythign that said application settings, in the latest PB. Got to bring in the framework and import the header, also.
Note that this article was written for an earlier version of Project Builder (the one that came with 10.0 I think). Things might have changed a bit since then.
Posted by: andy on July 3, 2003 11:28 AMCan someone tell me the difference between running the app out of PB directly, and just double clicking on the .app file built from PB. It seems that my Java calls work great if I am running out of PB, but if I build and the run the .app by double clicking it, it doesn't execute. Thanks! Maybe VM path or something needs to be specified...I missed it. Any ideas?
Posted by: Pete Gordon on July 9, 2003 02:44 PMThere's only a single difference, the current directory is different (the app's bundle in PB, / in Finder). Maybe the application depends on some relative path?
Posted by: andy on July 9, 2003 03:10 PMDocuments look like here
http://developer.apple.com/documentation/ReleaseNotes/JavaCocoaAPIs.html
though I am still having a difficult time to collect information about the java bridge.
I'm just starting to really understand this, but can answer two of the above questions.
1) First, don't just copy that code into your main. Make it the action of a button or something and it will work beautifully.
2) You don't actually need that name/value pair to enable Java any more. Instead press those same buttons--Cmd-Opt-E--go to Info.plist entries => Cocoa-Java specific => Needs Java.
Posted by: Andy Gerweck on September 5, 2003 10:25 PM
The Jave Bridge as outlined in this excellent article worked great for me until recently. Now the bridge is not working, but only not working for classes where constructors are required (ie newWithSignature: is used).
Here is some code for a test String using a constructor that takes a String.
Class jClass = [[NSJavaVirtualMachine defaultVirtualMachine] findClass: @"java.lang.String"];
id javaTestString = [[jClass newWithSignature:
@"(Ljava/lang/String;)", @"hi there" ] autorelease];
The error message I get is as follows:
"NSMutableString +newWithSignature: sent to non-java class NSMutableString"
Funnily enough,
[ NSClassFromString(@"java.lang.String") alloc] init]
works fine.
Has anyone else experienced this behaviour?
I am now running system 10.2.6, I think the last time it was working satisfactorily I may have been running 10.5. I prefer to find a solution without reinstalling to system 10.2.5 though.
thanks and regards
Peter
peter: did you try to use [NSClassFromString(@"java.lang.String") newWithSignature:@"..."]?
Posted by: andy on September 14, 2003 09:07 PMAndy
Yes - the original code had this and it gives the same result. Using the longer version of getting the Class was an attempt to find a solution.
Peter
Just like any obj-c argument that has multiple objects separated with commas you must end it with a nil as exhibited here:
id Testclass=[NSClassFromString(@"Testclass")
newWithSignature:@"(Ljava/lang/String;)",@"Hello there!", nil];
Oh also i wouldn't do any
[NSClassFromString(@"java.lang.String") newWithSignature:@"..."]
as there is transparent bridging between java.lang.String and NSMutableString. You might as well just create an NSString the obj-c way and use it as a java.lang.string
This is how I got it to work using Xcode.
(1) Created a new java-cocoa project.
(2) Added the JavaVM framework (Located in /System/Library/Frameworks/JavaVM.framework)
(3) Cut and paste the following code in main.m -
#import <Cocoa/Cocoa.h>
#import <JavaVM/JavaVM.h>
int main(int argc, const char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
//[NSJavaVirtualMachine defaultVirtualMachine];
id vector=[[NSClassFromString(@"java.util.Vector") alloc] init];
[vector add:@"one item!"];
NSLog(@"item 1=%@",[vector get:0]);
[vector release];
[pool release];
return 0;
//return NSApplicationMain(argc, argv);
}