When 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.