In the first part of this series, Triage, I talked about some of the earliest steps I took in the process of purchasing a Cocoa-Java application and porting it to Objective C. In this article, I’ll discuss some techniques I stumbled upon for working with a legacy Java code base while gradually migrating functionality to Objective C. In keeping with the medical metaphors, I’m calling this segment “Life Support” because, while the project in this state is by no means dead, it’s far from healthy.
What does it mean for a project to be alive? I’m adopting a definition of life that is roughly “as freaking close to original functionality as possible.” That means the application needs to be launchable. It needs to perform its advertised purpose. Ideally, a project in “life support” should be less than a week away from “shippable” at any time.
The bad news? To maintain this standard, you might put up a ton of scaffolding code today to keep the application alive, only to tear it down in a few days when the complementary structures in another part of the application have been comparably revised. Some would argue that it’s better to bid farewell to the life of the application, and commit to a dark period of zero functionality while everything is redesigned and/or ported to the desired specification. This would certainly make for less “throwaway” code, but an unfamiliar, non-functioning application scares the living daylights out of me.
Clean Code In A Strange Land
Back to the specific project at hand. You’ll recall that during triage, I discovered some cool Objective-C hacks that allowed me to fix the heinous crashing bugs on Intel, while leaving essentially the entire Java code base unchanged. This trick relied on a basic truism of the Cocoa-Java runtime: calling methods on Java objects from Objective-C is relatively easy, while calling back to Objective-C from Java is not.
To call a Java class method from Objective-C, you look up the class by name, and send it a message that will, by convention, map to an associated Java class method. When the Java class you’re messaging is in fact a subclass of a Cocoa class, the messaging can feel extremely natural. For instance, assuming you’ve got a Java-based Preferences Window controller, this is how you might message it to show its window, from an Objective-C based class:
Class theClass = NSClassFromString(@"PrefsController"); NSWindowController* myPrefs = [theClass sharedInstance]; [myPrefs showWindow:self];
Going the other direction, from Java to Objective-C, requires compiled-in bridge code. Much of the Cocoa frameworks from around the 10.2 era is represented in the bridge code provided by Apple. Everything else, including any custom Objective-C classes you’ve written, would require new bridge code to be generated and linked in to your application. This is apparently still possible, using Apple’s unsupported and poorly documented “bridget” tool. Personally I didn’t want to touch it. The idea here is to eventually get rid of the Java code completely, so I don’t want to waste a lot of time making it easier for it to stick around.
So here’s my approach: pragmatically move stuff from Java to Objective-C, keeping in mind that everything I move “up” must not be depended on by any other Java code. For example, if I move the Preferences controller from Java to Objective-C, then no longer can any Java code say “PrefsController.sharedInstance().showWindow(this)”. So deciding how to proceed will require at least some sense of the dependencies between the various Java classes in my project.
I was hoping there might be a nifty way of automatically gathering this information from Apple’s class diagram tool in Xcode, but it turns out not to be as entirely useful as I’d hoped. I used a combination of its member variable information, combined with some brute-force global text searches, to roughly plot out a list of all classes, and the Java-based classes that they depended upon. For instance my Java-based application delegate depended upon the Java-based document class, which depended on several Java-based model classes. So I can’t really port the document or model classes to Objective-C, but I can port the delegate, because no other Java code links directly to it.
The idea is to look for “childless nodes” in the class hierarchy, and decide whether it is pragmatic to port them no or later. As classes are migrated up from Java to Objective-C, it reduces the amount of Java code in the project, while keeping the application functionally identical to its former self.
The Objective-Java Chimera
Obviously things are never quite so simple. In a real project, dependencies between classes never fall into a perfect tree structure where one can simply pluck classes one-by-one from the top of the tree. In some cases, two classes might mutually depend on one another, in which case it would make sense to port both of them to Objective-C in one fell swoop. Or maybe the vast majority of a class is not dependent on Java, but one small component is. It might make sense to port the class with the caveat that things will not be completely functional, for a short period. The application isn’t dead! It might be wheezing, but it still basically works.
I came up with a technique which I found pretty useful, especially for some of the largest classes in the project. What if a class is half dependent on Java, and half not? It would be great to be able to port just the half of the class that can make it to Objective-C, while postponing the rest for later. Using a “Chimera” object that is conceptually part Java and part Objective-C, I was able to achieve this monstrous goal.
I applied the technique first to a custom NSView subclass, because I observed a slowness in drawing that I guessed might be sped up by minimizing the amount of cross-bridge messaging and structure conversion that going on every time the view was drawn. In any case, I knew I was not confident about applying Shark measurements to a Java class, and would have a much easier time fine-tuing the performance if all drawing was done in Objective-C. The custom view in this case was a major piece of the application, though. It had its hooks into the model code, and other classes had their hooks into it. And it was relatively huge. It made a great candidate for the Chimera technique.
The technique depends on Objective-C’s ability to forward messages a class doesn’t implement to another class. So for most practical purposes, an Objective-C class can “wrap” another class such that clients of the class think all the work is being done by the wrapper. In this case, we want to sneak a new CustomView class in to the runtime, where the nib file will unarchive and automagically contain an instance of our view instead of the old Java one. Then we’ll fulfill the contract of the old view class, both by implementing our own methods and by dispatching to it when necessary. Simplifying the situation somewhat, converting a Java class to a Chimera class involves the following steps:
- Rename the Java class to something new. For instance, “CustomView” becomes “JavaCustomView.”
- Create a new Objective-C class, named after the original. In this case, “CustomView.m” is our new source file.
- Implement message-forwarding methods. These are where the magic happens.
That’s the skeletal Chimera. Now you can add whatever methods you like in ObjC and they will automatically take precedence over the Java equivalents. You can even rename the Java equivalents if appropriate, and partly implement methods in ObjC. For instance, you could implement “awakeFromNib” such that it first called the Java class’s “javaAwakeFromNib” and then proceeded to do whatever Objective-C based setup was appropriate.
So which are the “message-forwarding methods” that you need to implement? In my experience, it’s important to implement respondsToSelector:, methodSignatureForSelector:, and forwardInvocation:. In each case, you’ll want to dispatch to the underlying Java object, but only when appropriate. To have an instnce of the Java object around, you’ll need to add code that, at init time and awakeFromNib time, respectively, instantiates and configures the underlying class. You may also need to do some patching up of IBAction connections or whatnot. The fine-tuning will all be very application-specific.
The idea for the “responds to” and “method signature” methods is you want to return the union of your Objective-C class functionality and the Java class functionality. So first ask your superclass to handle the method, and then only if the result is NO or nil, pass-through to the Java class for further inquiry. The forwardInvocation method will only be reached if nothing in your class could handle the method, so you can just blindly pass these through to the Java class, if it will take them.
To make this work, you have to manage the dependencies between the two halves of the Chimera. For instance, it might make sense to maintain redundant copies of the IBAction outlet variables, so that both sides of the world have access to “outside the class” dependencies. A pitfall is you might end up storing data in one half of the Chimera and not realizing that the other half is still trying to depend on it. I never said this wasn’t going to be messy, but it gets the job done.
Applying this technique essentially opens the doors wide to staging the port. You can carefully port parts to Objective-C while keeping the application alive. You’ve got a full-fledged Objective-C class to pass around in “clean land,” while Java objects continue to operate as necessary on the Java subset of the class.
A Healthy Outlook
In the next and final installment of this series, I’ll talk about the grunt-work of actually going the final mile, and what we gain by getting there. Porting all those Java classes to Objective-C is fairly easy work, but it’s tedious. I came up with some tricks that might affect your approach in attacking a similar problem.