Cliff Hacks Things.

Saturday, January 21, 2006

Making those 'Universal Binaries' a bit more universal

Some of you may be familiar with Cesta, my network analyzer. I'm starting to get settled in California, so I'm beginning to hack on it again.

Like any Mac developer in January of 2006, I'm interested in having my app be a Universal Binary, so it will run on both PowerPC and x86. Apple provides pretty good support for this. However, there's one minor gap:

Tiger (10.4) uses GCC 4.0. Panther (10.3.x) uses GCC 3.3. There are minor incompatibilities and feature gaps between these — for example, on Tiger, I can use an accelerated dispatch routine for Objective-C message sends. The C++ ABI is also subtly different, and tends to break some routine language features.

Building a Universal Binary for 10.3 PPC and 10.4 x86 is straightforward, but there's no Apple-supported way to build a three-way binary: 10.3 PPC, 10.4 PPC, 10.4 x86. Sure, I could use weak linking to use the 10.4 API from a 10.3 binary — but that doesn't get me the compiler and ABI changes I want from GCC 4.0, which will only run on 10.4. (With some of the C++ changes, the binary won't even get past dyld.)

Now, I've been developing for Unix a lot longer than Mac OS X has been around, and Unix has this marvelous family of system calls, exec(3). They allow you to replace your current process image with another — you keep the same process, with the same PID and RUID, but the running program changes. This is used all over Unix, particularly in the time-honored fork/exec sequence. Of course, these are available in OS X.

"So I realize," I realized, "that I could use this to effectively change ABIs on the fly."

Here's how it works.
The official binary (Contents/MacOS/$APPNAME) is a Mach-O fat^H^H^HUniversal binary, containing code for ppc (compiled against 10.3.9 using gcc 3.3) and x86 (compiled against 10.4 using gcc 4.0.1).

However, using an auxiliary target, I also include a non-fat copy, containing only ppc code, compiled using gcc 4.0.1 against the 10.4 SDK.

Normal developers would, at this point, have their users download one version or the other — and if I wanted my app to feel like a crappy Windows package, I would do just that.

Instead, I've dropped the following code fragment into the main function. It ain't pretty, because it's a first crack that I wrote some fifteen minutes ago, but it works.

1. Checks for GCC 3.3. This means it's the PowerPC 3.3 binary.
2. Checks for Tiger.
3. Tries to construct a path to the Tiger-optimized PPC binary.
4. execs it, thereby replacing the current process.
5. If anything goes wrong, the PPC 3.3 binary is used.

int main(int argc, char *argv[])
#if __GNUC__ == 3
// We're the gcc3.3 PPC binary. Check for Tiger.
// If any error occurs, simply fall back to running this version.
SInt32 MacVersion;
if(Gestalt(gestaltSystemVersion, &MacVersion) == noErr) {
if(MacVersion >= 0x1040) {

// On 10.4 or above. Let's do nasty things to the runtime.
char *lastSlash = strrchr(argv[0], '/');
if(lastSlash != NULL) {
// Build a path to the Tiger binary.
char binaryPath[lastSlash - argv[0] + 1 + 13 + 1];
strncpy(binaryPath, argv[0], lastSlash - argv[0] + 1);
strncpy(binaryPath + (lastSlash - argv[0]) + 1, "TigerExecTest", 14);
fprintf(stderr, "%s\n", binaryPath);
char *execArgs[argc + 1];
memcpy(execArgs, argv, argc * sizeof(char *));
execArgs[argc] = 0;
execv(binaryPath, execArgs);

// Oh crap. I guess we'll stick with the 3.3 version.
perror("Exec failed");

return NSApplicationMain(argc, (const char **) argv);

Error handling could be better, but there you have it.