field-theory.org
the blog of wolfram schroers

Dynamic message resolution in Objective-C

This article describes a powerful feature of the Objective-C language: the ability to handle messages dynamically at runtime! This is an advanced article and applies to both MacOS X as well as iOS.

Table of contents:

Introduction
Example project for MacOS X
Download and conclusion

Introduction

Objective-C is a minimalistic extension of the C programming language. Minimalistic in the sense that the syntactic extensions can be summarized on a few pages. However, Objective-C also features a very powerful and versatile runtime environment that offers capabilities far beyond other systems.

Many object-oriented programming languages rely on object identities to be known at compile time, others can handle if object identities are resolved at runtime using introspection APIs. But the Objective-C runtime goes even further: even methods can be dynamically added and handled on the fly!

In this article I present examples of how classes can handle messages that were never declared in their interface. I demonstrate how it works even if there were no methods corresponding to these messages at the time the classes were compiled! Admittedly, these features are not used frequently. Still, they are ubiquitous in the Core Data framework where all properties are declared as @dynamic.

Example project for MacOS X

Asking the boss to get work done

Consider the following code fragment:

    WSBoss *theBoss = [[WSBoss alloc] init];
    [theBoss performSelector:@selector(completeWork)];

This is really a non-exciting piece of code and the only thing looking odd about it is the use of performSelector instead of the natural way of [theBoss completeWork]. (And, of course, the semantics of the object model seems a little odd, too – or who would design a class called “boss” and expect it to complete work?) It starts to get more exciting, however, when we have a look at the interface of the class WSBoss:

@interface WSBoss : NSObject
// The employee (not owned by the boss).
@property (weak) WSEmployee *worker;
@end

This class has no means of actually completing the work – there is no method declaration for doing so! (This makes the semantic of the object model a little more reasonable, but screws up the syntax.) At this point the reason for using performSelector above also becomes clear: This code would not compile as the lexical analyzer knows that there is no method completeWork and the way to get around this limitation is by using the performSelector method!

This still does not help as WSBoss cannot handle the message and thus the code would simply crash. But we can actually get around this problem in an elegant manner:

@implementation WSBoss
@synthesize worker = _worker;

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [[WSEmployee class] instanceMethodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"%@ says: I don't do '%@' myself.",
          NSStringFromClass([self class]),
          [anInvocation description]);
    if ([self.worker respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:self.worker];
    } else {
        [super forwardInvocation:anInvocation];
    }
}
@end

The trick is to forward the message to an instance of WSEmployee instead of handling it herself. This approach will forward all messages sent to an instance of WSBoss to the worker instance if worker says she can handle them (which it does by answering respondsToSelector). If worker cannot handle them (which includes the case that worker is nil) it will be forwarded to WSBoss's superclass which would result in an exception.

Note that we need to override both methodSignatureForSelector: as well as forwardInvocation: method for this approach to work!

How work really gets done

At this point we just need to let the instance of WSEmployee do the work and everything will be fine. But we can do the same trick again:

@interface WSEmployee : NSObject
@end

Now WSEmployee still cannot handle the original message and thus cannot get anything done! Of course, he could simply try to forward the message elsewhere (and it still would not get done), but as our model is simply a small business with a single worker only, further delegation is not possible. Instead, the worker needs to get anything done that comes her way. This is how she does it:

#import <objc/runtime.h>
@implementation WSEmployee

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    class_addMethod([self class], sel, (IMP)genericMethodIMP, "v@:");
    return YES;
}

void genericMethodIMP(id self, SEL _cmd)
{
    NSLog(@"%@ says: I finished '%@'.",
          NSStringFromClass([self class]),
          NSStringFromSelector(_cmd));
}
@end

The worker needs to implement a class method named resolveInstanceMethod which takes a selector – thus, a datatype representing a message signature – and returns whether or not it can handle that message. The runtime function class_addMethod shows how an instance method is added to the class that can handle the message and will be called subsequently. The declaration of this function is in objc/runtime.h so we need to import this header file.

Note that the function is a mere C function that does not return any value — we still need to take worry about whether the function actually does the work it was suposed to be doing. (In our model the employee simply claims to have done them, but if that is really what we wanted is another issue!)

A working example

How does it all play out? Suppose, we instantiate a boss and a worker, set the worker property of the boss and then send several messages her way:

    WSBoss *theBoss = [[WSBoss alloc] init];
    WSEmployee *aWorker = [[WSEmployee alloc] init];
    theBoss.worker = aWorker;
    [theBoss performSelector:@selector(completeWork)];
    [theBoss performSelector:@selector(completeWorkForMe:)
                  withObject:self];
    [theBoss performSelector:@selector(doMoreStuffWithMe:andMe:)
                  withObject:self
                  withObject:self];

If we do not set the worker property of theBoss the code would raise an exception. Otherwise, any message we send theBoss will be forwarded to aWorker who will handle everything that comes her way.

Completely dynamical objects and messages

But we can do even more! Up to this point we have assumed that the object names and messages are at least known at compile-time even though their interfaces were not. We can drop even this requirement and construct our classes and selectors entirely from strings (these could be read in at runtime). This does not give Objective-C the full power of eval, but it is the closest any dialect based on C can get:

    id anotherBoss = [[NSClassFromString(@"WSBoss") alloc] init];
    [anotherBoss performSelector:NSSelectorFromString(@"setWorker:")
                      withObject:self.aWorker];
    [anotherBoss performSelector:NSSelectorFromString(@"anotherJob")];

In this case the class name WSBoss, the configuration (setting the worker property) as well as the actual job to be done (the message anotherJob) is taken from strings. Finally, the anotherJob message is handled by the poor employee who now has to deal with two different bosses! (Still, she does a pretty good job responding to everything she gets!)

It is also possible to extend this case to messages with more than two parameters and with non-object scalar data types. This requires the use of NSInvocation directly – which, of course, can also be done dynamically!

Download and conclusion

This article has shown some of the advanced features of the Objective-C runtime — the ability to resolve message dynamically at runtime, even if the corresponding methods were unknown at compile time (or even development time)! These are certainly advanced features of the language and they are used only in very specific cases (like for Core Data), but we should be aware that they exist in case we might ever need them. For further information see the complete developer documentation on Apple's site.

The source code shown above licensed under the GPLv3 can be downloaded as a gzipped tar-ball.