Modules in Objective-C
| January 15th, 2010Categories in Objective-C can be a great thing. Being able to add methods to a class is a powerful feature. However, one weakness is that those methods only get added to one class. If I want the methods in a category to apply to more than one class, I have to put that category on a subclass. If I want to generic apply the category, I have to put it on NSObject (and then the methods are almost certainly available on classes I don’t really care about). Sometimes it would be nicer if I could choose individual classes that the methods are added to. This is done often in Ruby through the use of modules.
Adding a Method
I won’t get into any details on how this is done in Ruby, there’s plenty of other good resources for that. But here’s an idea of what I’m shooting for:
@protocol BRDog
-(NSString*)bark;
@end
@interface BRDogModule : NSObject <BRDog> {
}
@end
@implementation BRDogModule
-(NSString*)bark
{
return @"bow wow";
}
@interface BRHusky : NSObject <BRDog> {
}
@end
@implementation BRHusky
// Magically add the methods from DogModule to Husky here
@end
When I call bark on an instance of BRHusky I want the implementation from BRDogModule to be called. Why might I want to do this? Well, this is a fairly trivial example, but basically we avoid having to use inheritance to get the bark implementation. In a complex system, not being forced to use inheritance can be an important gain.
A couple of quick notes about the use of protocols above: since BRDogModule will never use used directly, it actually doesn’t need to implement the BRDog protocol. However, doing so ensures that the compiler will warn us if we forget to implement some of the methods. Similarly, since BRHusky isn’t implementing the protocol methods itself, putting the protocol directly on the class will generate a warning. Instead I actually put the protocol on a separate category which will get rid of the warning.
So how do we go about magically adding methods to a class? It’s actually pretty easy with Objective-C’s runtime:
+(void)initialize
{
Method barkMethod = class_getInstanceMethod([BRDogModule class], @selector(bark));
IMP methodImp = method_getImplementation(barkMethod);
const char* types = method_getTypeEncoding(barkMethod);
class_addMethod(self, @selector(bark), methodImp, types);
}
It’s pretty self-explanatory when you look at it. We simply take the implementation from our module class and add it to the class we’re interested in. Of course we wouldn’t want to have to do this for every class that we want to import our dog methods into. Instead we’ll
add an extend: class method onto our module, this way we only have to call [BRDogModule extend:self] in a class’s initialize method to do the extension.
Overriding Methods
So what happens when I want my own implementation for a particular class rather than the method that the module would give us? It turns out that our first naive implementation is okay with this. The call to class_addMethod will fail and return NO if the method already exists on the class. What about the reverse? This is probably a much less common need, but if we want the module implementation to override the class implementation we have to change things a bit.
One thing I wanted to do is save the original implementation before overriding it. So we’ll save it by just prefixing the original method name with the classes name. We end up with something like this in BRDogModule:
+(void)addMethod:(SEL)methodName toClass:(Class)cls override:(BOOL)override
{
Method method = class_getInstanceMethod(self, methodName);
IMP methodImp = method_getImplementation(method);
const char* types = method_getTypeEncoding(method);
Method existingMethod = class_getInstanceMethod(cls, methodName);
if (existingMethod == NULL) {
class_addMethod(cls, methodName, methodImp, types);
} else if (override) {
// first save the original implementation
SEL newSelector = NSSelectorFromString(
[NSString stringWithFormat:@"%@_%@",
[cls description], NSStringFromSelector(methodName)]);
IMP existingImp = method_getImplementation(existingMethod);
class_addMethod(cls, newSelector, existingImp, types);
// now override
method_setImplementation(existingMethod, methodImp);
}
}
+(void)extend:(Class)cls
{
NSLog(@"adding %@ extension to class: %@", self, cls);
[self addMethod:@selector(bark) toClass:cls override:NO];
[self addMethod:@selector(speak) toClass:cls override:YES];
}
Now we’re overriding any implementation of speak with the module version. The class’s version will still be available using <class name>_speak.
Inheritance Issues
In my quest to add more features, I added a new problem (which is usually the case). In general, initialize is only called once per class. However, if the class doesn’t implement initialize, it’s the initialize of the super class will be called. Because of this, we might end up extending a class multiple times and maybe overriding methods multiple times. You can easily see this by just watching the logging statements. At first I came up with a complicated solution to make sure I don’t extend a class with a given module more than once, but Apple’s documentation gives me a much simpler solution. In classes our classes that are importing the module, we simply change the initialize to look something like this:
+(void)initialize
{
if (self == [BRHusky class]) {
[BRDogModule extend:self];
}
}
This ensures that subclasses of BRHusky don’t import the BRDogModule methods again. Another option is to use the load method instead. There are a few limitations on load that you should read up on before using this. One problem I found is that there was no NSAutoReleasePool at the time load was being called. I had to make a few changes to some my extension methods to make sure nothing was leaking.
I cleaned up things a bit and moved extension methods into BRClassExtender. Modules can then just use this to add whichever methods they choose in their own extend method.
I do this stuff a lot in implementing Mail Plugins in 64 Snow Leopard (because Mail’s internals are now marked hidden from the dynamic linker, you can’t create categories to extend functionality and have to resort to things like using the runtime to extend class functionality after code has been loaded.)
These techniques are not trivial and there are many gotchas and pitfalls one must be very careful about when doing this — not the least being ivars. Direct access of ivars in methods are using the ivar layout of the class the method is compiled for. If ivar layout is different than class being extended, it will crash. Related to this is the loading of nibs — if a class is to load nibs, unless you have coded acccessors, the nib loading mechanisms will use the ivar layout to attach ivars to iboutlets — if these are different, you will have problems. Properties will not necessarily work here because again properties will use the declared ivar layout. What you have to do is create accessors that use the runtime functions to look up and access ivars by name. And alway alway always use these accessors.
Another major pitfall is the sending a message to the super. (eg [super doMethod:] the super keyword will refer to the super of the compiled class, not the super of the extended class. If this are different you will get unexpected problems.
There are other issues — but
finishing off.
but — ivars are the major one and will cause all sorts of head aches if one is not careful.
For super, I define a macro
define SUPER(…) objc_msgSendSuper(&(struct objc_super){self, class_getSuperclass([self class])},_cmd, ##VA_ARGS)
that will automatically look up the super of the class at runtime and call the appropriate method
rather than -(void) doThis: (id)param1 andThat:(id)param2{ //used to be [super doThis: param1 andThat: param2]; // now SUPER(param1,param2); }
Only issue with this is on a dealloc — compiler will complain if you have your dealloc method doesn’t have a [super delloc] — trick is to do both with a return statement after the SUPER() call.
-(void)dealloc{ SUPER(); return; [super dealloc]; }
whoops — that VA_ARGS should be _ VA_ARGS _ without the spaces (silly mark up doesn’t let double underscore be)
Thanks, that’s all great information. I doubt I’d ever attempt to access ivars in anything but the class itself, but I wasn’t aware that properties would have the same issues. Perhaps my understanding of properties is incorrect, but I always assumed it’s just syntactic sugar covering standard methods, and not potentially direct access to an ivar.
The point about sending messages to super is important. Although it makes perfect sense, I doubt I’d have thought about it before blindly sending a message and then being confused by the results. I guess I’ve never needed to call super from an extension method before.
In general, I’m using this kind of technique to extend my own classes so I’m probably safe from the ivar issues. But it’s definitely great to know all the places you might get into trouble.