Subclassing a non-existent Objective-C class
Let’s say you’re a Mac OS X developer, and you have the following scenario:
- You have an application that’s required to start up without a particular Mac OS X class, framework or function definition, because it may not be installed yet on the user’s system, or they’re running an older version of Mac OS X that doesn’t have the relevant library installed. (Note that this requirement may only be there so that you can check for the presence of a particular framework, and throwing up a simple error message to the user when your application starts that tells the user to install the framework, rather than simply terminate with some obscure output to stderr.)
- On the other hand, your application also uses some features of the potentially missing class/framework/function on Mac OS X. This is normally pretty easy to do by using weak linking and checking for undefined functional calls (or using Objective-C’s
-respondsToSelector
method), except when… - You need to subclass one of the Cocoa classes that may not be present on the user’s system.
The problem here is that if you subclass an existing class that may not be present on the user’s system, the Objective-C runtime terminates your application as it starts up, because it tries to look up the superclass of your subclass when your application launches, and then proceeds to panic when it can’t find the superclass. The poor user has no idea what has happened: all they see is your application’s icon bouncing in the dock for the short time before it quits.
Here’s some examples where I’ve needed to do this in real-life applications:
- You use Apple’s QTKit framework, and you’re subclassing one of the QTKit classes such as QTMovie or QTMovieView. QTKit is installed only if the user has QuickTime 7 or later installed, and you’d like to tell the user to install QuickTime 7 when the application starts up, rather than dying a usability-unfriendly death.
- You need to use all-mighty
+poseAsClass:
in a+load
method to override the behaviour of existing class, but the class you’re posing as only exists on some versions of Mac OS X.
There are several potential solutions to this (skip past this paragraph if you just want a solution that works): the first one I thought of was to use a class handler callback, which gets called when the Objective-C runtime tries to lookup a class and can’t find it. The class handler callback should then be able to create a dummy superclass so that your subclass will successfully be loaded: as long as you don’t use the dummy superclass, your application should still work OK. Unfortunately this approach encountered SIGSEGVs and SIGBUSs, and I ended up giving up on it after half an hour of trying to figure out what was going on. Another method is to actually make your subclass a subclass of NSObject, and then try to detect whether the superclass exists at runtime and then swizzle your class object’s superclass pointer to the real superclass if it does. This causes major headaches though, since you can’t access your class’s instance variables easily (since the compiler thinks you’re inheriting from NSObject rather than the real superclass)… and it doesn’t work anyway, also evoking SIGSEGVs and SIGBUSs. One other possibility is to simply create a fake superclass with the same name as the real Objective-C class, and pray that the runtime choses the real superclass rather than your fake class if your application’s run on a system that does have the class.
The solution that I eventually settled on is far less fragile than all of the above suggestions, and is actually quite elegant: what you do is compile your subclass into a loadable bundle rather than the main application, detect for the presence of the superclass at runtime via NSClassFromString or objc_getClass, load up your bundle if it’s present, and finally call your class’s initialisers.
Practically, this means that you have to:
- add a new target to your project that’s a Cocoa loadable bundle,
- compile the relevant subclass’s source code files into the bundle rather than the main application,
- ensure that the bundle is copied to your application’s main bundle, and
- detect for the presence of the superclass at runtime, and if it’s present, load your bundle, and then initialise the classes in the bundle via the
+initialize
method.
Here’s some example code to load the bundle assuming it’s simply in the Resources/ directory of your application’s main bundle:
NSString* pathToBundle = [[NSBundle mainBundle] pathForResource:@"MyExtraClasses"
ofType:@"bundle"];
NSBundle* bundle = [NSBundle bundleWithPath:pathToBundle];
Class myClass = [bundle classNamed:@"MyClass"];
NSAssert(myClass != nil, @"Couldn't load MyClass");
[myClass initialize];
You will also have to end up specifying using the -weak*
parameters to the linker (such as -weak_framework
or -weak-l
) for your main application if you’re still using normal methods from that class. That’s about it though, and conceptually this technique is quite robust.
Credit goes to Josh Ferguson for suggesting this method, by the way, not me. I’m merely evangelising it…