Forwarding Objective-C Messages

December 5, 2009

Unlike Java or plain C, Objective-C work with messages. You don’t invoke a method on an object, instead, you send a message to it (like the Ruby language). This approach let a more dynamic behavior. For instance, suppose that you have this class:

// SomeClass
#import <Cocoa/Cocoa.h>

@interface SomeClass : NSObject {
}

-(void)doSomething
@end

#import "SomeClass.h"
#import "Delegate.h"

@implementation SomeClass

-(void)doSomething {
   NSLog(@"doSomething was called on %@", [self className]);
}

@end

As you can see it has only one instance method definition, called doSomething. Thus, it can respond to the doSomething message. But, what happens when we send a message that the receiver can’t respond to ? Lets make a try:

The receiver is the object that will receive the message. For example, in the statement: [dog bark] dog is the receiver and bark is the message.

Here is our main code:

// MethodMissing
#import <Foundation/Foundation.h>
#import "SomeClass.h"

int main (int argc, const char * argv[]) {
  NSAutoreleasePool *pool = [NSAutoreleasePool new];
  SomeClass *someClass = [SomeClass new];
  [someClass doSomething];
  [someClass doSomethingElse];
  [pool drain];
  return 0;
}

This code compiles with warnings, because the message “doSomethingElse” is not defined in the SomeClass interface. Errors in the Objective-C message dispatch system occurs at runtime. Programmers need to be more careful when dealing with more dynamic languages.

Running the code we got the following result:

MethodMissing[1695:a0f] doSomething was called on SomeClass
MethodMissing[1695:a0f] -[SomeClass doSomethingElse]: unrecognized selector sent to instance 0x10010c6c0
MethodMissing[1695:a0f] *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '-[SomeClass doSomethingElse]: unrecognized selector sent to instance 0x10010c6c0'

Not surprisingly the program crashed. First it invoke our declared doSomething message and logged the message to the console, all ok until now. But in line eight we got a NSInvalidArgumentException with the error -[SomeClass doSomethingElse]: unrecognized selector sent to instance 0x10010c6c0.

This error message is very readable, the problem is that, there isn’t any message that respond to doSomethingElse in class SomeClass.

And we got to the key topic of this post. We can handle unrecognized selectors messages and do a special treatment when it arrive to the receiver. How we do that ?

We need to overwrite two methods from the NSObject class in the receiver class: -(void)forwardInvocation:(NSInvocation *)invocation -(NSMethodSignature*)methodSignatureForSelector:(SEL)selector

An NSInvocation is an Objective-C message rendered static, that is, it is an action turned into an object. NSInvocation objects are used to store and forward messages between objects and between applications.

Now our classes looks like this:

// ForwardClass
#import <Cocoa/Cocoa.h>

@interface ForwardClass : NSObject {
}

-(void)doSomethingElse;

@end

#import "ForwardClass.h"

@implementation ForwardClass	

-(void)doSomethingElse {
	NSLog(@"doSomething was called on %@", [self className]);
}

@end

// SomeClass

#import <Cocoa/Cocoa.h>

@interface SomeClass : NSObject {
	id forwardClass;
}

-(void)doSomething;

@end

#import "SomeClass.h"
#import "ForwardClass.h"

@implementation SomeClass

-(id)init {
	if (self = [super init]) {
		forwardClass = [ForwardClass new];
	}
	return self;
}

-(void)doSomething {
	NSLog(@"doSomething was called on %@", [self className]);
}

-(void)forwardInvocation:(NSInvocation *)invocation {
	if (! forwardClass) {
		[self doesNotRecognizeSelector: [invocation selector]];
	}
	[invocation invokeWithTarget: forwardClass];
}

-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
	NSMethodSignature *signature = [super methodSignatureForSelector:selector];
	if (! signature) {
		signature = [forwardClass methodSignatureForSelector:selector];
	}
	return signature;
}

@end

When an object receive a message it doesn’t recognize, it wraps the invocation in a NSInvocation object and call the -(void)forwardInvocation passing it as parameter. But, first, it call the -(NSMethodSignature*)methodSignatureForSelector to get the method signature for the given selector.

Now, running the same program again we got:

MethodMissing[523:a0f] doSomething was called on SomeClass
MethodMissing[523:a0f] doSomethingElse was called on ForwardClass

And the program finished without any problems.

There are a lot of uses for this technic, some examples are:

  • Wrap one object in a logger object that intercepts and records the invocation of interesting messages.

  • Implement synthetic messages that are handled by other methods in your class. Imagine creating a generic database record object that catches any property message it receives (i.e., -saleDate, -setSaleDate:) and automatically translates it into a record query. Instead of coding date = [record getDateFieldWithKey:@"SaleDate"], you could simply write date = [record saleDate], without ever writing a -saleDate method. NSManagedObject and CALayer are examples of classes that implement synthetic properties.

  • Create an object that forwards the message to a hierarchy of other objects, like a responder chain. The proxy object would search a collection of other objects looking for one that implements the message

In a future post I will demonstrate one real world application of this.

comments powered by Disqus