Cocoa-Java Porting Step 1: Triage
February 8th, 2007When I chose to purchase an existing Java-based Cocoa application, I knew I was taking on some risk. Mac OS X still has a technology within it called the Cocoa-Java bridge, which makes it relatively easy for Java classes to participate in the Cocoa runtime, automatically translating objects like arrays and strings to their language-specific counterparts, and allowing for messaging between objects of the two languages. In short, it makes it possible for Java developers to write Cocoa apps just like an Objective-C developer would, only using Java for all of the custom classes in their code.
Unfortunately, Apple has all but abandoned the technology. The implications for developers are that Cocoa-Java applications cannot easily adopt new APIs, are mysteriously buggy, and are not easily maintained with the latest versions of Xcode. In short, they are a mine-field of opportunities for disappointment and despair.
Nonetheless, I decided to buy a Cocoa-Java application, after looking through the existing sources and making best and worst-case scenario predictions. The previous owner had already disclosed some nuanced issues with running on Intel Macs, so I knew there would be some work before I could ship it in good faith. The amount of work would range from “a few hacks to get it working on Intel as Cocoa-Java” to “a complete rewrite in Objective C.” I planned to port to Objective C eventually, but I hoped to be able to put the product on the market soon with just a few tweaks to make it robust on Intel. Then I could port to Objective C at my leisure while counting all the money.
Now that I’m 95% done with the process of getting the application into shape, I’m ready to start writing about it. I hope that by explaining the process of evaluating and overcoming obstacles in the process of doing this port, it will establish a reference for others who are stuck doing the same kind of work. I expect there will be many Cocoa-Java parts in the months to come, as support for the legacy technology continues to diminish. Hopefully some of the wisdom I have earned will come in handy for others.
Evaluate The Source Base
The first thing you’ll want to do in approaching any port is to look closely at the existing source files. You don’t need to completely understand what everything does, you just need to see it and take away an impression. At this stage the style and organization of the content should pose as much interest to you as the raw technical functionality. For example, is the object hierarchy conducive to a typical Cocoa design? Is the code well-commented? Does the code exploit language specific features being that will be difficult to replicate in the target language?
In my case the code is well-written, well-commented, and since it’s Cocoa-Java, it already conforms more or less to a conventional Cocoa way of doing things. Lucky me! Nice purchase, Daniel. The only part of the project that doesn’t directly translate to Objective-C is the notion in Java of a class that “extends Thread.” There is no Objective-C class representation of a “thread,” so this would obviously take some modest reorganization. But the use of threads was not extensive, so I did not get scard off.
It doesn’t hurt that Java and C are such close siblings, any amount of porting that does need to be done will retain much of its syntactic appearance. In particular, when porting Cocoa-Java to Cocoa-ObjC, probably 80% of the work will fall into two broad categories of concern:
- Convert syntax().like().this() to [[syntax like] this].
- Pay attention to memory management.
The syntax point is a simplification, but really a lot of it could be ported by trained monkeys, or better yet, regular expressions. Anyway, I’ll talk more about this in a later segment, but suffice to say that the work of straight porting from Java classes to Objective-C classes can be tedious, but is eminently doable.
Build And Run
Realistically, this step was probably the first thing you did in evaluating the sources. You’ve got the Xcode project staring right back at you, how can you resist at least trying? The great news is that my product built and run flawlessly, from the minute I unpacked the source archives and installed them on my Mac. The bad news goes back to those aforementioned “subtle Intel problems.” Specifically? Crash on print. Crash on window zoom. Ouch.
The crashes in either case point to the Cocoa-Java bridge. An infuriatingly cryptic backtrace essentially lets me know that Cocoa-Java was trying to print some log message (perhaps explaining why it was about to crash?) when it bit the dust:
#0 0x90a53387 in objc_msgSend () #1 0xbfffe250 in ?? () #2 0x908103a2 in _CFStringAppendFormatAndArgumentsAux () #3 0x9080ec8c in _CFStringCreateWithFormatAndArgumentsAux () #4 0x925e2a5d in -[NSPlaceholderString initWithFormat:locale:arguments:] () #5 0x92604670 in -[NSString initWithFormat:arguments:] () #6 0x9672d308 in _BRIDGELog () #7 0x9673305d in _BRIDGEMethodImpStructReturn () #8 0x93519c6e in -[NSWindow _standardFrame] () #9 0x93732d7c in -[NSWindow zoom:] () #10 0x9335cd88 in -[NSApplication sendAction:to:from:] () #11 0x9335cce1 in -[NSControl sendAction:to:] () #12 0x9335ee91 in -[NSCell _sendActionFrom:] () #13 0x9335e94c in -[NSButtonCell performClick:] () #14 0x97723a6b in Java_com_apple_cocoa_application_NSWindow_performZoom ()
Searching the web for help on this failure was pretty fruitless. I downloaded some other applications that I knew to be Cocoa-Java based, and they didn’t crash on zoom. So what’s the deal? What is my involvement in this problem. Through hard work and luck, I narrowed both crashing problems down to a pretty simple thesis:
“Cocoa-Java will crash on Intel if Cocoa asks Java to return an NSRect.”
It might be overstating the case, but those are the two crashing cases in my application. I suspect there is a byte swapping problem or something. If you return an NSRect, you will crash. Sad story, but fixable. The two method calls in question for me are NSWindowController’s “windowWillUseStandardFrame:defaultFrame:” and NSView’s “rectForPage:”.
Keep It Running
I went to the auto parts store a few months back, around the beginning of autumn. My car had been running hot, and on the verge of overheating all through the summer. I brainstormed with the clerk about what might be wrong, and we finally agreed that it is probably a problem with my thermostat, but could be a problem with the radiator itself. Fine, it’s settled. I’ll replace the thermostat first. But even this involves draining the radiator, perhaps removing it, and a lot of other messy stuff. The shop clerk looked outside at the chilling weather and asked me a few pointed questions: “How old is this car?” “Do you drive it a lot?”, etc. After hearing my responses of “pretty old,” and “not too much,” he gave his pragmatic verdict: “It’s not gonna overheat in wintah, and who knows if it will even run in the spring?”
The point being, it’s not always the right time to do the right thing. I’m confident that that my Cocoa-Java NSRect crashes will disappear if I port the application to Objective C. But can I get away with patching the rough spots today? Moreover, won’t my customers (existing customers of this application) benefit from having the Intel problem solved sooner, rather than later? I decided to attack the problem of crashing NSRect across the bridge.
My first thought was to try to gauge exactly why it crashes. If it’s truly a byte-swapping issue, I could detect the fact that I’m running on Intel, and perhaps massage the data to suit the bridge. The major downside to an approach like this is you’re now at the mercy of Apple leaving the bug in place. In the unlikely event that Apple shows the bridge some love, and fixes the crash, where does that leave me? Now I’m byte-swapping myself into the same crash some ways down the road.
The solution I came up with is simple albeit somewhat of a hack. It’s the duct tape around the muffler, if you will. If you think about the crashing problem with returning an NSRect, an obvious solution would be for Java to not return an NSRect. To work around this problem, I modified the affected Java classes to return NSString instead of NSRect:
public String javaSafeStandardFrame(theWindow, defaultFrame) public String javaSafeRectForPage(pageNumber)
Now the program doesn’t crash. Of course, it doesn’t zoom or print correctly, either. The problem of course is that Cocoa doesn’t know to call these “javaSafe” versions of the methods, and even if it did, it wouldn’t know to expect a string in return.
This is where the hacking comes in to play. The fact that I’m working with a Cocoa-Java project doesn’t mean I can’t also have Objective-C classes in my project. In fact, at runtime the majority of classes in memory are Objective-C, coming from Apple’s frameworks. What’s to keep me from selectively implementing some classes of my own to help soothe this problem? Using a custom class and the powerful technique of class posing, I’m able to effectively patch out standard versions of the above methods, and replace them with code that calls through to Java, gets a string representation of the rect, and converts it to NSRect for the benefit of Cocoa. Since the NSRect is now returned from Objective C, there’s no more crash. In my main.m source file:
[RectSafeWindowController poseAsClass:[NSWindowController class]];
And in my custom NSWindowController implementation:
- (NSRect)windowWillUseStandardFrame:(NSWindow *)theWindow defaultFrame:(NSRect)newFrame { // Your java class instead of "JavaController" Class myJavaClass = NSClassFromString(@"JavaController") if ([self isKindOfClass:myJavaClass]) { NSString* goodRectString = [self javaSafeStandardFrame:theWindow :newFrame]; return NSRectFromString(goodRectString); } else if ([super respondsToSelector:@selector(windowWillUseStandardFrame:defaultFrame:)]) { return [super windowWillUseStandardFrame:theWindow defaultFrame:newFrame]; } else { // Just return the default return newFrame; } }
A similar approach was used in the NSView override of rectForPage.
Take Stock
Now that my application builds and runs without crashing, it’s time to ship it, right? Possibly, but in this case I chose not to. Although the application has a mature feature set and benefits from years of evolutionary design, it has become a little bit dated in some ways that I grew increasingly uncomfortable with.
Now that the application is running crash free, I can breathe a sigh of relief. I could ship this thing, but I choose not to. I want to be prudent about what I change for the “1.0 relaunch,” but there are some nuances of the appearance and performance that need tweaking. And to comfortably tweak those things, I’m going to have to selectively port some of the Java classes to Objective C. But after all that hard work getting the application to “shippable,” I want to be careful not to backslide too much. Tune in next time for a detailed look at migrating Java classes to Objective C without disrupting the overall functionality of the application.
Continue reading – Step 2: Life Support.
February 8th, 2007 at 10:26 am
That is a very interesting your are offering us. I can’t wait to read the next entries. As a long-time Java developer in the process of learning Cocoa, I’m pretty sure your writings will give me valuable information on how to change my Java good practices into Objective-C good practices.
BTW, extending the Thread class is definitely *not* a good practice in Java ;-)
February 8th, 2007 at 11:00 am
I’ve been working on an Objective-C bridge and I know exactly what the problem is here. It really wouldn’t take that much effort on Apple’s part to fix it. Take a look at the Universal Binary Programming Guidlines (http://developer.apple.com/documentation/MacOSX/Conceptual/universal_binary/universal_binary.pdf), page 54, the section “Objective-C Runtime: Sending Messages”. There are two gottchas (which are not made very clear in that document or section):
1- On PowerPC objc_msgSend_stret looks like this:
void objc_msgSend_stret(void * stretAddr, id theReceiver, SEL theSelector, …);
On Intel, it needs to look like this (if you were expecting an NSRect, for example):
NSRect objc_msgSend_stret(id theReceiver, SEL theSelector, …);
You accomplish this by using a function pointer cast. You only do this for stucts larger than 8 bytes. Not doing this usually results in a crash.
2- For structs 8 bytes and smaller, you must call objc_msgSend rather than objc_msgSend_stret. This is because structs 8 bytes and smaller are returned in registers on Intel. Not doing this usually results in memory corruption, resulting in random crashes later on.
NSRect is a 16 byte struct, so basically when the Java Bridge calls your NSRect returning function, it is still using the PowerPC convention of calling objc_msgSend_stret like normal. How to fix this? From your backtrace it seems like the call it taking place in _BRIDGEMethodImpStructReturn, so it might be possible to interpose a rewrite of this function. That would be difficult, though. What would be better is if Apple would just open source their Java Bridge.
February 8th, 2007 at 11:04 am
Also, because of #2 above, be on the lookout for any methods that return NSSize, which is 8 bytes. My bet is that if such a method exists, memory is being corrupted and the app will crash at some random moment. This is just a guess though, as Apple may have fixed #2 and not #1.
February 8th, 2007 at 11:12 am
It looks to me like BRIDGELog() is setting up a format string and passing a bogus value for a %@ format specifier.
February 8th, 2007 at 12:18 pm
Count me as another interested reader. I spent an afternoon trying to use a JDBC driver in an Objective-C application. I gave up and resolved to try JNI at some point. It’s a shame, because the Java bridge is tantalizingly simple.
February 8th, 2007 at 2:40 pm
Nice initial write up. I don’t forsee your overall port from Java to Objective C being too difficult. One of my own apps, SQLGrinder, was originally written in Java-Cocoa, because, well, at the time I knew Java really well and Cocoa/Obective C not so much. The port from Java to ObjC actually wasn’t bad at all. Many of the classes map pretty easily, and aside from a few syntax conversion oddites, the two languages seem to be really close and use a very similar object and runtime model.
(As an aside, SQLGrinder is now an Objective C/Java bridge hybrid that uses a very thin communication layer to pass objects across the bridge, but is otherwise almost entirely a native Objective C app as of 2.0).
February 8th, 2007 at 3:29 pm
More than that–they’ve officially deprecated it in Tiger.
February 8th, 2007 at 5:30 pm
Very interesting write-up Daniel. I look forward to future installments. I don’t want to start a comment war or anything but it is fine to extend Thread in Java. Or at the very least it was fine and common practice back in the day. Take a peek in O’Reilly’s Java Threads book, they do it all over the place. I think you’ll find that NSThread detachNewThreadSelector:toTarget:withObject: will be a suitable replacement for a Java Thread. Although depending on what the Thread was used for you may not need to go to that trouble at all because Cocoa may do it for you behind the scenes.
February 8th, 2007 at 6:42 pm
You can start work on the remaining 95% ;)
February 8th, 2007 at 7:47 pm
Nice write up. I’m looking forward to your future post on migrating java classes to objective-c. I’m learning cocoa right now by re-implementing older code (gotta have a project) I’ve written in java, c and .net.
February 9th, 2007 at 6:42 am
Nice article indeed. What stops me from moving to Obj-C is the huge number of quality open source libraries available in Java which isn’t exactly the case for the niche-language Obj-C.
February 9th, 2007 at 7:34 am
Port JIGS! :-)
February 9th, 2007 at 10:27 am
Just to clarify re: the deprecation of the Cocoa/Java bridge, this effects using Cocoa as your primary API, not necessarily calling across the bridge into the Java from Cocoa. Apple still uses the actual bridge in WebObjects for things like JDBC, so while deprecated, “Deprecation does not mean that support is immediately ending for these tools.” http://lists.apple.com/archives/webobjects-dev/2006/Aug/msg01144.html
February 9th, 2007 at 8:01 pm
This sounds like a groundswell: I’m in the process of moving my application from Cocoa-Java to Objective C as well, mostly prompted by the last straw of it not “just working” in Tiger. It sounds like the conclusion we’re all coming to is that it’s not all that bad to make the switch.
February 9th, 2007 at 9:55 pm
dylan: Love your domain name! :) I think it’s fair to say that it’s not as bad as it could be to make the switch. It’s still a lot of tedious work, but not so hard for anybody who has a firm understanding of Cocoa.
It says something that I could do the port fairly easily without really knowing (or learning) much about Java.