Cocoa-Java Porting Step 2: Life Support

February 20th, 2007

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:

  1. Rename the Java class to something new. For instance, “CustomView” becomes “JavaCustomView.”
  2. Create a new Objective-C class, named after the original. In this case, “CustomView.m” is our new source file.
  3. 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.

10 Responses to “Cocoa-Java Porting Step 2: Life Support”

  1. Darkshadow Says:

    Interesting outlook, Daniel. I tend to just go into my code and rip it apart at will, whether or not that’ll keep it from working. Which it tends not to. In fact, I’m going through a major rewrite of one of my apps (pretty much ground up), and it’s just now getting back to where it mostly runs.

    Is this more of an aesthetic thing for you? I keep my code up to date in my svn repository in case I need to roll back, so it’s not really a giant issue to me when I break things.

  2. Daniel Jalkut Says:

    Hi Darkshadow – it’s more of an insurance that the “roll back” will only be a day or two of work, in the worst case scenario. If I was to tear everything apart and hope for the best, I’d probably end up 2 or 3 weeks away from a stable product again. And yeah, chances are it would “all work out” eventually, but if it didn’t, I’d have to roll back 3 weeks!

    Using the “incremental and almost never broken” technique, I just get a bit more security that I’m not breaking things terribly, as I go. I would be more likely to be adventurous with a product that I knew extremely well. In this case I have the added pleasure of a continuous reference for functionality. I don’t have to compare with the original product, to see if it behaves the same. I can constantly compare against my last checkin, to see if I busted anything.

  3. Stephan Cleaves Says:

    I think this is a wise approach. Without a “live” app its hard to know that the new method you just wrote does what you intended. I build new applications in this incremental manner as well. Try out my ideas with simple prototypes, and use some of the code from those, but generally more what I learned about the functionality to begin building the app. Then from there add on features while trying to keep a “live” app. Sometimes you’ve got to change something that makes it hard to do that, but I always feel much better knowing that the app still works.

  4. Gareth Says:

    I agree 100% with the “keep it working” approach. There’s nothing more frustrating than having to roll back a weeks worth of work because you cannot pinpoint where you made your fatal error and broke everything.

    My approach to development is to do it incrementally, in the smallest functional working blocks at a time. It’s not always possible, but when it is, it makes writing the code much easier.

  5. Logan Says:

    I have a Cocoa-Java app that I am slowly moving to Obj-C as well. This has been really interesting to see another perspective and some new techniques. It’s quite lonely in Cocoa-Java land.

    I’ve found the “keep it working” approach to be better for me as well, especially since this is a side hobby project and I don’t get very much time to work on it. I did some major refactoring a while back and the app wasn’t runnable for a couple of months. I found that to be quite frustrating, especially when my planned refactorings brought on even more changes and I felt like I was getting farther behind with each step instead of making progress.

    I’m trying to avoid adding any new Java code, and doing everything new in Obj-C as much as I can. One approach I’ve tried for the few times when I need to call from Java into Obj-C is to (ab)use notifications. It doesn’t always feel right, but is easy to implement and keeps me away from bridget, which I am also loathe to use and have avoided thus far.

    I am going to have to try this Chimera approach and see if I can make it work. I look forward to your porting tricks, as I have much porting still ahead of me.

  6. Jon Trainer Says:

    Good stuff Daniel. I think your chimera approach is a good way to attack your challenge. It sounds like a good approach to pull out of the old toolbox for porting or when ever a major structural change to an app needs to be made.

    I wonder how it could apply to a language that doesn’t have an Objective-C bridge, like RealBasic or .NET via mono.

  7. flight16 Says:

    Is NSClassFromString deprecated? Apple said they are no longer supporing the Cocoa Java bridge… but what about the Objective-C Java bridge? People are now suggesting to use JNI if you need to use Java from Objective-C. Is anyone know for sure if you will no longer be able to use Java from Objective-C?

  8. Logan Says:

    @flight16:
    NSClassFromString can also be used to look up Obj-C classes (for example, it is used in some Core Data examples) so it won’t be deprecated entirely. The fact that it can look up a Java class is likely deprecated.

    The Cocoa Java bridge is the Obj-C Java bridge. There is not a separate bridge. More specifically, Cocoa-Java uses the Obj-C Java bridge to do it’s work.

    As long as there is a Java SDK on Mac OS X you will be able to use JNI to get to Java from Obj-C since JNI is part of Java. I don’t expect that to ever go away unless Apple drops Java entirely, which is extremely unlikely.

    As far as the bridge is concerned, I believe Apple has stated that the bridge will still exist on Leopard, but there are no guarantees after that. So it may or may not be included or work in 10.6, and users of the bridge best be prepared for it to not be available.

  9. Richy Says:

    I’d agree with the majority of replies that this is a good approach. As a consultant I’m often having to pick up code in all kinds of states and if it’s working it’s probably best to try and keep it that way.

    A working app is an ideal substitute for a formal specification of just exactly how feature ‘X’ is supposed to work, particularly if you weren’t involved in the original design decisions, development cycle, bug fixing and so on. It’s easy to look at something and, based on your impressions of what it should do, write replacement code – however you can often end up throwing the baby out with the bathwater in doing so.

    Martin Fowler describes code refactoring as “a series of small behavior preserving transformations.” (www.refactoring.com) and he’s spent a lot more time analyzing, writing about and probably doing refactoring than most people. This approach has consistently worked well for me over the years.

  10. Scott Stevenson Says:

    Fantastic post.

Comments are Closed.

Follow the Conversation

Stay up-to-date by subscribing to the Comments RSS Feed for this entry.