Leap of Faith
Volume Number: 19 (2003)
Issue Number: 3
Column Tag: Leap of Faith
Leap of Faith
Why and how we switched from Carbon to Cocoa
by Steven Frank
From the beginning, we were excited. A new Mac OS based on Unix! We loved the look and feel of Mac OS, and we loved the stability and standards of Unix, and the thought of the two combined was so wonderful that it was as if a choir of angels had descended with baskets of fine Belgian chocolates and sweet-smelling flowers on down pillows. That is to say, we liked the idea very much. But we also realized the magnitude of the change, and that it wasn't going to happen overnight.
After the release of Mac OS X we found ourselves, like many other Mac developers, confronted with a dilemma. We had two successful applications under our collective belt and, being serious Mac OS X fans, we knew beyond a doubt we wanted to upgrade them to Mac OS X native status. Our dilemma was, should we quickly port our applications using Carbon or essentially start them over in Cocoa?
Time being of the essence in those early days, we started ripping out the legacy code, and went the Carbon route. The Wild West-like frontier of Carbon was a little rough at first, but as Carbon itself was revised and finalized, it became mostly painless to get Classic code running on Mac OS X. Apple's Carbon team did a truly commendable job in making so many old applications run on a brand-new operating system architecture with so few changes. However, when the dust settled, it became apparent that our newer application, Audion, had fared much better in the transition than our older application Transmit.
Transmit, a graphical File Transfer Protocol (FTP) client, was originally written in 1996-1997, targeting Mac OS 7.5. Open Transport was still something new on the horizon. The Appearance Manager had not yet made its debut. Transmit was the first application I had ever written in C++ and my first PowerPlant application, and as such had a number of, shall we say, fascinating design decisions that, to be honest, are too embarrassing to mention.
As the Mac OS evolved, so did Transmit. Code was added to decide between MacTCP and Open Transport networking at runtime. More bridging code decided whether Mac OS 8 Appearance Manager widgets could be used, or whether backwards-compatible work-alikes were needed.
Mac OS X was unkind to our four year old code, and rightly so. Little bugs that had slipped past us, due to the lax nature of earlier OS versions, brought Transmit down in a flaming wreck on Mac OS X. I was grateful (no, really) to have these bugs brought to my attention, but it was clear that the code was due for an overhaul.
I had been learning Cocoa in my spare cycles for some time, and I very much liked what I saw. Here was a framework that was well designed, extensible, and very easy to use. The more I learned about it, the more I liked. It seemed like a good way for us to embrace the future, and bury that old code once and for all.
In March 2002 I decided to take the plunge, and started a skunkworks rewrite of Transmit in Cocoa that would later become the official Transmit 2 project with a three-person strong development team. What follows are some of the lessons we learned along the way.
Learning To Speak
One of the first decisions we faced was which language to use. Cocoa code can be written in either Objective-C or Java. My being more familiar with C and C++, as well as having an existing codebase written in those languages pretty much sealed the deal for Objective-C.
I've been surprised by the overwhelming reluctance of some of my fellow developers when it comes to Objective-C. It's a phenomenon I, as of five minutes ago, call "bracket shock". Developer sees what appears to be straight C code, but is confused by inexplicable brackets. Developer runs screaming back to their currently preferred language.
Objective-C is a very easy language to learn and surprisingly powerful once you understand its nuances. If you currently know ANSI C, and are at least familiar with object-oriented programming concepts, you can learn enough in one or two days to grope your way through your first project. I'm not exaggerating. You just have to get over your bracket shock.
In fact, to help you out, here are the only things a practicing C programmer really needs to know about Objective-C to get started:
1. It's just C, but with object-oriented programming facilities.
2. Those brackets surround method invocations. They're essentially just fancy function calls.
3. Method arguments have names indicating their purpose.
4. A hyphen indicates a method that is invoked on an instance of a class. A plus sign indicates a method that is invoked on the class itself.
Come to grips with those, then bounce back and forth between sample code, tutorials, and developer documentation, and you will be a master Objective-C programmer in less time than it would take you to complain about Objective-C's syntax. Apple provides a great introduction called "The
Objective-C Programming Language", located at: http://developer.apple.com/techpubs/macosx/Cocoa/ObjectiveC/index.html
Make no mistake though, the best way to write Cocoa code is not to hurriedly shoehorn in your old C++ code. Beyond the syntax itself, Objective-C code has its own style. There are "traditional" ways of naming methods and their arguments. I spent some time fighting this, and eventually realized I was just making things more difficult for myself. If you follow the traditional styles, other Objective-C programmers will be able to read your code and offer assistance. Learn about the Foundation types, especially NSString, NSArray, and NSDictionary, as these are workhorse classes that are used over and over and over again. Use these Foundation types instead of re-inventing your own container classes for the n-th time. It will make things easier. After a while, it gets under your skin and you will start to subconsciously realize when you are not doing things "the Cocoa way". As a general rule for Cocoa newcomers, if you ever find yourself saying, "There's got to be an easier way to do this," you are almost certainly right, and you should go find out what it is before continuing down your current path.
Keep in mind that learning additional programming languages beyond your comfort zone exposes you to new ideas and concepts, and makes you more employable. Go on! Try it! I had bracket shock at first too, but Objective-C is now among my favorite programming languages.
Dealing With Humans
Pesky humans! As developers, we write all this amazing and wondrous code, and then we have to figure out a way to expose the greatness of it all to plain-old ordinary human beings.
Fortunately for us, Apple has spent hundreds of thousands of dollars figuring out really good ways for us to do so, and they're giving this research away to us, for free, in the form of the Human Interface Guidelines, available online at: http://developer.apple.com/techpubs/macosx/Essentials/AquaHIGuidelines/index.html
Have you actually ever read the Human Interface Guidelines (HIG)? Be honest. I won't tell. What you need to know, as a Classic Mac OS programmer considering making the move to Mac OS X, is that these guidelines have changed for Aqua. In some areas, they've changed a lot.
When is it appropriate to use sheets? Drawers? Toolbars? The "metal" appearance? "Whenever it looks cool" is not the right answer for any of these.
If you have directly ported your Mac OS 9 interface to Mac OS X, it's going to look weird. You may not think it looks weird, but your Mac OS X users, even if they don't know exactly why, will feel a vague sense of unease when using your application. Something, they'll think, looks off. They'll find themselves compelled to use a different application that doesn't give them that feeling. You can get away with a sloppy interface on Windows, but on the Mac OS, it's cause for a public flogging.
In Mac OS X, the default fonts and font sizes have changed. The recommended spacing between controls has changed. Sheets are generally preferred over dialogs, but not always. The location of the Preferences and Quit menu items has changed. There are specific guidelines for designing your application's icon. Whitespace is preferred over separator lines. Visual hierarchy is preferred over group boxes. Do your windows avoid overlapping the Dock when zoomed?
If nothing else, you owe it to yourself to read the HIG section "Checklist for Creating Aqua Applications" which will give you a nice summary of the rest of the material. While it is obviously important that your application perform its tasks quickly and reliably, never underestimate the weight placed by users on your application's appearance, familiarity, and usability. That's true no matter which version of Mac OS you're developing for.
It used to be that if you wanted to write a Cocoa application, you had to use Apple's Project Builder. Since then, Metrowerks has updated CodeWarrior to also support development with Cocoa.
Since CodeWarrior's Cocoa support was not completely available at the very beginning, I ended up learning Project Builder, and it's now my development environment of choice. Unfortunately, never having used it, I'm not qualified to talk to you about CodeWarrior's Cocoa support. But, I can tell you what I like about Project Builder.
First and foremost, I love Project Builder's integration with the gdb debugger. If you are not a Unix aficionado, you may not be familiar with gdb. At the surface, setting breakpoints within Project Builder and so forth is as easy as any other IDE, but if you're willing to spend a little time learning about gdb and delving into the debugging console, you can do some phenomenally advanced debugging. It is possible, just as an example, to set a programmatic breakpoint in a loop that stops execution, dumps the state of a few variables and then resumes, all automatically.
I've also come to appreciate Project Builder's single window layout option. At first, it felt a bit foreign, but before long I was zipping around as fast as ever. Integration with the CVS source management system was another big plus.
One Cocoa-ism that tends to throw people coming over from Carbon is the significance of Interface Builder in the development process. At first glance, it appears to be just a user interface layout tool like PowerPlant's Constructor. It is used for that purpose, but its role goes much further. In Interface Builder, you actually describe relationships between GUI controls and living, breathing objects in your code. If you find yourself looking for a call that just doesn't seem to exist ("How do I get a pointer to that button object in my window?") the answer probably lies in Interface Builder. (In this case, connect the button to a member variable in the class that is looking for it; Cocoa will set its value at runtime.) Project Builder and Interface Builder are tightly integrated, which is another advantage to using Apple's IDE.
On the flip side, Project Builder (or rather gcc, the underlying compiler) does seem slower at compilation than CodeWarrior did. My personal opinion is that the speed factor is outweighed by Project Builder's integration and overall stability. In this case, if it's good enough for Apple's developers, it's good enough for me.
Development environments are like religions though, and I know better than to recommend either. Try all of the available environments and see what you like. You may even be a truly hardcore user and prefer the Terminal's command line for all your development needs. That's OK too. Use whatever works best for you. But, as I said in the section about Objective-C, don't be a stereotypical stubborn developer. Keep your mind open, and don't be afraid to try new things. You might just learn something.
Getting Some New Threads
The first real issue I encountered while rewriting Transmit in Cocoa was a fundamental change in the operating system's threading model. All well-behaved network applications use some sort of threading, since it's no fun to be locked out of the user interface while network activity is taking place, with nothing but the rainbow beachball to keep you company.
In the old world, Transmit used the Thread Manager, which implemented the moderately cheesy "cooperative threading" model. In cooperative threading, each thread of execution is responsible for transferring control to the next eligible thread whenever it feels the time is right. Cocoa's threading model (NSThread) is built atop POSIX threads, which are preemptive. In the world of preemptive threading, your threads must be prepared to be swapped in and out at the operating system's whim. This is much harder to get right, but it results in significantly more efficient threading. The kernel is even smart enough to often be able to execute threads on different processors on multiprocessor Macs.
The issue of threading is complicated by somewhat vague documentation on which parts of Cocoa are "thread-safe" and which are not. The very general consensus seems to be that it is best to avoid calling into the Application Kit from any thread other than the main thread. So how, for example, should worker threads update elements in the user interface?
Until 10.2, the answer was to use Cocoa's Distributed Objects (DO) mechanism to invoke a method on the main thread that would perform the update on the worker thread's behalf. Explaining how to do this correctly is another article in itself, but enough information exists on the net that careful web searching will reveal what you need to know.
Alternatively, there is a new call available in 10.2, which makes the whole business much easier: performSelectorOnMainThread in NSApplication. You simply choose which method ("selector" is a bit of Objective-C jargon) you want, and it will be scheduled to run on the main thread. Very handy. But if you use performSelectorOnMainThread, your program will only be compatible with 10.2 and up, so consider your audience carefully.
There's not enough room in this article to get into all the intricacies of threading, so I'll refer you to Apple's TechNote 2028, which compares the OS 9 and OS X threading models in greater depth: http://developer.apple.com/technotes/tn/tn2028.html
Making The Network Work
Around the Mac OS 8 timeframe, Apple introduced a new networking API called Open Transport, which still exists today in Mac OS X's Carbon layer. But there's another API worth considering when doing Cocoa development, which is the traditional BSD sockets interface. (BSD refers to the flavor of Mac OS X's underlying Unix subsystem.)
Open Transport views network connections as "streams". You set up a network "endpoint", and a handler function, and then sit back until you receive notification at your handler of network events, such as connections, incoming data, and so on.
The socket model, an older but arguably more popular metaphor, treats network connections similarly to file handles. You "open" them by connecting to the other side, then you read and write data, and eventually close them.
This is a gross oversimplification of the whole thing, of course. Many long and tiresome battles have been waged about the relative merits of sockets versus streams, and to get into that mess would be beyond the scope of this article. What's relevant here is that the availability of industry-standard BSD socket calls on Mac OS X opens up a wonderful world of third-party code, of which you can take advantage. In our case, I discovered LibNcFTP, a highly competent C library implementing most all facets of the FTP protocol.
With the threading model already in place, it was easy enough to drop in calls to LibNcFTP and let it do the heavy lifting as far as network activity was concerned. Having been originally written for Unix, the library compiled effortlessly on Mac OS X, and plugged into our project without much sweat at all. Nothing's perfect of course, and we sent our share of feedback to the very patient library developer, who dutifully incorporated our suggestions and patches into his own code. The availability of this library saved us probably weeks worth of re-inventing the wheel. I had a barebones version of Cocoa Transmit able to connect to FTP servers and get a file listing within the first week of development, and we were more than happy to pay the modest licensing fee asked for LibNcFTP. It would have been a much more rigorous ordeal to use this same library in a Classic Mac OS application.
Switching to Mac OS X and Cocoa allowed us to ditch our capable but sometimes fragile homegrown FTP implementation in favor of one that has been in continuous development and refinement for ten years. It was something that actually provided a visible benefit to our customers too, not just a gratuitous technical achievement.
Mach-o, Mach-o Man
If you are used to Classic Mac OS programming, you are probably at least remotely familiar with the PEF binary format. PEF is the native format of executable binaries on Mac OS 9. Mac OS X can load PEF binaries for backwards compatibility, but its native format is Mach-O. In fact, Mach-O is the only binary format that Project Builder's tool chain will generate.
Classic PEF binaries had the concept of code modules that could be "weak-linked" or "strong-linked". To very quickly summarize, if you wanted to call an operating system API that was present in one version of the OS, but not another, you could tell the linker to either weak-link or strong-link that reference. If strong-linked, your application would not launch unless that API was present. Instead, the Finder would display a (usually quite terse) error message about the missing API. If the same application was weak-linked to the API, the application would launch, but the function pointer for the missing API would be set to NULL. This allowed the programmer to check at runtime what capabilities were available. The downside was if you forgot to check first and jumped to a NULL API, your application would crash.
Under Mac OS X and Mach-O, by default, if you have calls to functions in the BSD subsystem that are not implemented on the OS version that your program is running on, the OS will abort launching your application and log the offending call into the Console. We quickly discovered this within ten minutes of sending out our first Transmit 2 beta release, as we started getting reports from about half the testers that it wouldn't launch on their computers. We finally determined that all of these testers were running Mac OS X 10.1 and we were all running 10.2.
The problem was we had called a BSD function that was implemented on 10.2, but not 10.1. The 10.1 users saw only a generic Finder error message ("The application has unexpectedly quit"), while the true cause could be found in the Console's log. The system had logged the exact name of the missing function, which lead us straight to the root of the problem. Once we added a runtime check for the function, and appropriate bridging code, everything was back to normal.
However, if you invoke methods in the Cocoa frameworks that are unimplemented on the running system, a message will be logged to the Console, but your program will continue execution. This can lead to insidious bugs that seem inexplicable, unless your user happens to be running with the Console open. (Hint: He or she isn't.)
The best way to check if a Cocoa method exists before calling it is to send a respondsToSelector: message to the target object, passing the method in question. It will return a Boolean value indicating whether the method is implemented.
It's also beneficial to read the release notes for the Cocoa frameworks with each new release of the developer tools. They will explain which functions are new to each version.
An extra tip to those using the Jaguar developer tools: Apple now provides a preprocessor macro to control the compatibility level of the Cocoa headers. In other words, if you want precompiled headers that contain only APIs present in 10.1, you can run the following Terminal command:
sudo fixPrecomps -force -precompFlags -DMAC_OS_X_VERSION_MAX_ALLOWED=1010
Then, in the area labeled "Other C Compiler Flags" in the "GCC Compiler Settings" section of your Project Builder project's target settings, add:
After completing these two steps, you'll get compiler errors if you try to use a Cocoa feature that is not available in 10.1.
There are two more probably obvious bits of advice that can be gleaned from our experience:
1. Always try running your application on a system that meets only your application's minimum requirements before sending it off to anyone. Before you say your program runs on 10.1, try it to make sure!
2. If your users are reporting weird behavior in your Cocoa application that does not match your understanding of the world, the first place you should look is the Console log. The odds are high that there will be some sort of message that will give you a clue, if it doesn't lead you directly to the source of the problem.
You should be aware that the few short paragraphs above represent only a very light treatment of the Mach-O linking and loading process. For more in-depth information, I recommend reading Apple's "Mach-O Runtime Architecture" located at: http://developer.apple.com/techpubs/macosx/DeveloperTools/MachORuntime/index.html
Another aspect of Cocoa and Mac OS X programming in general that Classic Mac OS programmers will want to familiarize themselves with is application packaging. The Classic version of Transmit was distributed as a single file with code in the data fork and GUI resources in the resource fork.
The preferred format on Mac OS X is the application package. This is a folder hierarchy organized in a specific, known way, containing your binary executable, application metadata, and any resource files your application uses. The entire folder structure is presented to the user as a single double-clickable application icon.
Project Builder will automatically package your Cocoa application, so this is generally not something you have to worry too much about. However, it is worth your time to understand the layout of an application package and the reasoning behind it. To this end, you should read the "Application Packaging" section of Apple's Mac OS X "System Overview", which can be found here:
The Cocoa class NSBundle assumes most of the roles of the classic Resource Manager, automatically locating application resources by name and type. A significant benefit of the application package appears when it comes time to localize your application into another language. In a nutshell, a folder is added to your application package for each language that your application supports. The files within each of these folders are identical for each language, but their content is localized for that language. When you ask Cocoa to retrieve, for example, your Preferences window from your resources, it is smart enough to return the French version when running on a French system, the German version on a German system, and so on. You only have to maintain one build, and it will localize itself at runtime. If you've ever localized a Classic Mac OS application, you can appreciate how much easier this is than the previously required gyrations.
It's not uncommon for your more devoted users to do the dirty work of localization for you, before you've even started looking for a translator. Within days of Transmit 2's release, we received a set of fully localized French resources which we were able to simply drop into the project, rebuild, and add French localization to our list of features.
Before releasing your code into the wild, you should go to the Terminal and type man strip. This curiously worded command will give you probably more information than you want to know about the "strip" command, which can be used to remove debugging symbols from your code. Even in a "deployment" build, Project Builder appears to leave behind a lot of symbolic information that is not needed in a shipping project. This can bloat the size of your executable dramatically, often to two or three times more than its natural size. The man page will describe all of the available options in gory detail, but my favorite incantation is strip -S <filename> (note the capital S), which removes symbols related to debugging, but not things like function names. You'll need to run this command on the actual executable, located in the Contents/MacOS directory of your application package. Why would you want to leave behind function names? For crash logs, which I'll get to in just a moment. Using strip -S reduces the size of the Transmit 2 executable by about three megabytes, which is nothing to sneeze at. One thing to look out for, though - if you leave function names in your shipping executable, you do make it slightly easier for pirates and other nefarious types to find their way through your code and deactivate any copy protection or registration schemes you may have employed.
Inevitably, the day will come when one of your users will report that your application has crashed. Wouldn't it be nice if you could get some truly detailed technical information about the nature of the crash? Mac OS X makes it possible! The first thing your user can do to help you is to head to the preferences of the Console utility. There, under the Crashes tab, are two checkboxes: Enable crash reporting, and Automatically display crash logs. Have the user turn both checkboxes on, then try to reproduce the crash. If the application does crash again, a log window will automatically pop up, containing vital clues about what happened, including a call stack trace of all running threads. Best of all, it's very easy for the user to cut and paste this information into an email to you. Much easier than it ever was with MacsBug!
Because you took my advice and used strip -S, the function names will be present in the stack traces, and hopefully it will be obvious what happened. One caveat about the crash logs - they get appended to the bottom of the file each time, but when the log window appears, its scrollbar is always scrolled to the top of the file. If you're not paying attention, you may think a bug you have already fixed is reappearing, when in fact you have to scroll down to get at the truth of the matter. Be sure to advise your users of this too. The quickest way to get to the most recent crash log is to scroll all the way down to the bottom, then page up until you see a row of 10 asterisks (**********). This indicates the start of an individual crash log.
Precious Natural Resources
The most important thing you need to know if you decide to make the journey from Carbon to Cocoa is that you aren't alone. There are plenty of other developers in the same boat, and many who have already traveled the road ahead. Don't make the mistake of isolating yourself from these great resources!
Cocoa sample code is increasingly abundant. If you find yourself stuck on a particular API, try plugging its name into Google and see what you find. Odds are good that relevant information is just waiting for you to discover it.
Web sites dedicated just to Cocoa programming are popping up left up and right. Just to name a few: www.cocoadevcentral.com, www.stepwise.com, and my own www.cocoadev.com each provide a slightly different approach to the topic, and are all worth a visit.
There are at least two mailing lists on the topic, which are worth joining. You can join Apple's cocoa-dev list at http://lists.apple.com/mailman/listinfo/cocoa-dev. The macosx-dev mailing list, hosted by Cocoa gurus The Omni Group, tends
to delve a bit deeper into the technical: http://www.omnigroup.com/developer/mailinglists/macosx-dev/. It's not necessary to read every message on the list (although you'd certainly learn a great deal if you did). I have a Mail filter set up that saves each mailing list message as it arrives into a mail sub-folder. This provides me with an extensive archive of discussions that's easily searchable for those times when I'm just stumped. Almost always I find that my most pressing questions have already been asked. If you do find you need to pose a new question, please be careful to observe good netiquette when posting to the lists, or you may find yourself feeling somewhat unwelcome.
If you are brand new to Cocoa and looking for something in hardcopy, I enthusiastically recommend Cocoa Programming for Mac OS X by Aaron Hillegass. It was the first Cocoa book that I felt was truly accessible when I was just starting out.
Last but not least, don't underestimate the value of joining Apple's Developer Connection (ADC) at http://connect.apple.com/. Basic accounts are free, but for a nominal subscription fee you'll receive prerelease seeds of upcoming versions of Mac OS X, as well as access to developer technical support from actual Apple employees, and a wealth of other perks.
All of these resources came to our rescue over and over again as we came to grips with the brave new world of Cocoa. It would have taken us much longer to release the product if we had not been fully plugged-in to them. Don't go it alone!
You're probably wondering, after all that work, was it worth it? We started over with a blank page and worked for about six months to recreate something that, by and large, already existed. It seems hard to justify. It seems like a duplication of effort.
I can assure you that it was absolutely worth it.
Our users were almost universally delighted to see the Cocoa version of Transmit. They were loud and clear that the time we had spent in the rewrite had paid off in terms of stability and functionality. User interface elements, such as toolbars and tables, that were tough to implement in Carbon required very little effort in Cocoa. Our native threading and networking model took full advantage of Mac OS X's architecture, especially on multiprocessor machines, and paid off dividends in speed. Not only that, but we got a chance to apply four years of user feedback in a major re-design of the program's underpinnings. Transmit's code is now significantly easier to maintain as well, and adding new features no longer has that "hope I don't bump the house of cards" feeling that I know you've experienced at some point in your projects. I don't regret any part of the endeavor.
You (or your manager) may have qualms about the size of the Mac OS X market. All indications from this end are that it's growing and fast. We released Transmit 2 for Mac OS X only in October of 2002 and it is selling at least as well, if not better than the previous OS 9 versions. Although we still offer an older version, 1.7, for our OS 9 users, we did not receive the torrent of "Where is Transmit 2 for OS 9?" email that we were expecting. In fact the people who are writing in about the OS 9 version seem to be almost universally indicating that it's only a stepping-stone until they get the chance to install Mac OS X.
Don't forget that Apple has decreed that new Macs will no longer be able to boot into OS 9. Whether you ultimately choose Carbon or Cocoa, the message is clear: migrate your code to OS X or become irrelevant. Mac OS X is the future of the Macintosh platform.
Moving to Cocoa isn't suitable or possible for everyone, and I can understand that. Every project has its own unique situation, scope, and requirements. But if you think there's even a slight chance you might be able to make the leap, you have my full encouragement to go for it, and hopefully this article will help you avoid some of the snake pits along the way. Good luck!
Steven Frank is the co-founder of Mac software renegade Panic, Inc. He was the original programmer of Transmit, an FTP client, and Audion, a digital audio player, as well as other smaller utilities. He can be contacted via email at firstname.lastname@example.org.