Welcome back to another exciting Friday Q&A. This week I'm going to continue the series on the Objective-C runtime. Yuji Tachikawa suggested talking about how@dynamic properties work in CoreData and I'm going to take that and expand it to talk about message forwarding in general.
No Such Method
Last week I talked about how Objective-C messaging works, and mentioned that interesting things happen when no method is found for a given selector. Those interesting things are what make forwarding happen.
(If you aren't totally clear on what aselector is or what the difference is between amethod and amessage, you might want to go read through that article real quick, or at least re-read the Definitions section if you already read it.)
Just what is message forwarding? Simply speaking, it allows unknown messages to be trapped and reacted to. In other words, any time an unknown message is sent, it gets delivered to your code in a nice package, at which point you can do whatever you like with it.
This kind of thing is incredibly powerful and allows for doing all kinds of nifty, clever things.
What Happens
What happens when you do[foo bar] andfoo doesn't implement abar method? When itdoes implement such a method, it's pretty straightforward: it looks up the appropriate method, then jumps to it. When no such method can be found, a complicated sequence of events ensues:
resolveInstanceMethod: (resolveClassMethod: for class methods) to the class in question. If that method returns YES, the message send is restarted under the assumption that the appropriate method has now been added.forwardingTargetForSelector: to the target, if it implements it. If it implements this method and it returns something other thannil orself, the whole message sending process is restarted with that return value as the new target.methodSignatureForSelector: to see what kind of argument and return types are present. If a method signature is returned, the runtime creates anNSInvocation describing the message being sent and then sendsforwardInvocation: to the object. If no method signature is found, the runtime sendsdoesNotRecognizeSelector:.Lazy Resolution
As we learned last week, the runtime sends messages by looking up a method, orIMP, and then jumping to it. Sometimes it can be useful to dynamically plug IMPs into a class instead of setting them all up beforehand. Doing this allows for really fast "forwarding", because after the method is resolved, it gets invoked as part of the normal message sending process. The disadvantage is, of course, that this isn't very flexible, since you need to have an IMP ready to plug in, and that in turn means that you need to have already anticipated the argument and return types that will be arriving.
This kind of thing is great for stuff like @dynamic properties. The method signature is something you should know in advance: you'll either take one parameter with a void return, or have no parameters and return one value. The types of the values will vary, but you can cover the common cases. Since the IMP gets passed the selector that's been sent to the object, it can use that selector to get the name of the property and look it up dynamically. Plug it in to the class using+resolveInstanceMethod: and off you go.
Fast Forwarding
The next thing the runtime does is see if you want to just send the whole message unchanged to a different object. Since this is a common case of forwarding, this allows it to be done with minimal overhead.
For some reason, fast forwarding is really poorly documented. The only place Apple even mentions it, aside from a commented-out declaration inNSObject.h, is in theLeopard release notes. (Search for "New forwarding fast path".)
This technique is great for faking multiple inheritence. You can write a little override like this:
-(id)forwardingTargetForSelector:(SEL)sel{return_otherObject;}
_otherObject, which will make your object appear from the outside as though it combined your object with this other object in one.Normal Forwarding
The first two are basically just optimizations that allow forwarding to go faster. If you don't take advantage of them, the full forwarding mechanism goes into action. This creates anNSInvocation object which fully encapsulates the message being sent. It holds the target, the selector, and all of the arguments. It also allows full control over the return value.
Before the runtime can build theNSInvocation it needs anNSMethodSignature, so it requests one using-methodSignatureForSelector:. This is required due to Objective-C's C heritage. In order to bundle the arguments up in theNSInvocation, the runtime needs to know what kind of arguments there are, and how many of them there are. This information isn't normally provided in a C runtime environment, so it has to do an end run around the C "bag of bytes" view of the world and get that type information in another way.
Once the invocation is constructed, the runtime then invokes yourforwardInvocation: method. From there you can do whatever you want with the invocation it hands you. The possibilities are endless.
Here's one quick example. Imagine you're tired of writing loops, so you want to be able to manipulate arrays more directly. Add this little category toNSArray:
@implementationNSArray(ForwardingIteration)-(NSMethodSignature*)methodSignatureForSelector:(SEL)sel{NSMethodSignature*sig=[supermethodSignatureForSelector:sel];if(!sig){for(idobjinself)if((sig=[objmethodSignatureForSelector:sel]))break;}returnsig;}-(void)forwardInvocation:(NSInvocation*)inv{for(idobjinself)[invinvokeWithTarget:obj];}@end
[(NSWindow*)windowsArraysetHidesOnDeactivate:YES];
NSProxy.NSProxy is basically a class that's expilicitly designed for proxying. It implements a minimal subset of methods, leaving everything else up for grabs. This means that a subclass that implements forwarding can capture basically any message.
To useNSProxy for this kind of thing, you'd write an NSProxy subclass that can be initialized to point at an array, and then add a little stub method toNSArray that returns a new instance of the proxy, like so:
@implementationNSArray(ForwardingIteration)-(id)do{return[MyArrayProxyproxyWithArray:self];}@end
[[windowsArraydo]setHidesOnDeactivate:YES];
Declarations
Another consequence of Objective-C's C heritage is that the compiler needs to know the full method signature of every message that you're going to send in your code, even purely forwarded ones. To make a contrived example, imagine writing a class that uses forwarding to produce integers from code, so that you can write this:
intx=[converterconvert_42];
The trouble is that the compiler doesn't know about anyconvert_42 method, so it has no idea what kind of value it returns. It will give you a nasty warning, and will assume that it returnsid. The fix to this is simple, just declare one somewhere:
@interfaceNSObject(Conversion)-(int)convert_42;-(int)convert_29;@end
Conclusion
Message forwarding is a powerful technique that greatly multiplies the expressiveness of Objective-C. Cocoa uses it for things likeNSUndoManager and distributed objects, and it can let you do a lot of nifty things in your own code.
That wraps up this week's Friday Q&A. Tune in next week for more riveting tales of programming, and leave your comments on this edition below.
Friday Q&A is powered by the contribution of your ideas. If you have an idea you'd like to see discussed here, post it in the comments ore-mail it directly. I will use your name unless you ask me not to.
Add your thoughts, post a comment:
Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.