Here at Friday Q&A, I pride myself on occasionally taking my readers to places where normal people dare not tread. Today is one of those days. This is not a reader suggested topic, but today I want to talk about a fun hack I came up with that allows proxying block invocations in much the way that one can proxy Objective-C messages.
What It Means
In Objective-C, it's possible to intercept messages. Any message sent to an object that isn't implemented gets anNSInvocation object constructed, and that is then sent toforwardInvocation:. In there, you can do whatever you like with the message, like messing with its parameters before passing it on to another object, or sending it over a network.
The most common use for this facility is to write a proxy class which doesn't implement much of anything. Nearly any message sent to it will be caught by the forwarding mechanism, and the proxy can then do clever things with any message, while still mostly acting like the object being proxied. This is useful for building things liketransparent futures andtransparent zeroing weak references.
Block proxying is much the same, but with blocks instead of objects. You wrap an arbitrary block with another block which is able to intercept the call and interfere as it sees fit.
The technique I'm about to present works well, but it's definitelynot supported and should not be used in real code. It relies on private APIs and private quirks of public APIs. It's an interesting experiment, not a stable library.
Code
As usual, the code is available on GitHub. Today's journey into the forbidding depths can be found here:
https://github.com/mikeash/MABlockForwarding
Theory
Objective-C message dispatch works by taking the selector and the class and looking up the method in the class that corresponds to that selector. More specifically, it looks up the function, orIMP, that actually implements the method, then calls that function.
Message forwarding hooks right into this system. When looking up a function, if no method is found in the class, a special forwardingIMP is returned. That function takes care of all the painful and platform-specific details of how to turn a function call into anNSInvocation object.
If we can obtain this special forwardingIMP then we can build a fake block around it and accomplish our goal of forwarding blocks. Turns out that the special forwardingIMP is really easy to obtain. All you need to do is ask the system for theIMP for an unimplemented selector. There are several ways to do this, but the easiest is to simply call[self methodForSelector:...] and pass a selector you know doesn't exist in the class.
A block is just an Objective-C object with a function pointer in the right place. To call the block, the compiler calls the function pointer and passes the object as the first parameter. We can construct an Objective-C object with a pointer to the forwardingIMP in the right place, and the forwarding machinery will kick into action, build anNSInvocation, and then call ourforwardInvocation: method.
The forwarding machinery needs the method signature of the method being called in order to know how to package the arguments. Fortunately, with reasonably recent compilers, blocks embed signature information in the same format.
Forwarding deals with messages, which have two implicit arguments: the object and the selector. Blocks only have one implicit argument: the block object. The second argument to a block can be anything, or not even exist at all (for a block with no parameters). Fortunately, the forwarding function doesn't seem to care about the type of the second parameter, as long as it's present. For blocks that don't have a second parameter, a fake one can be inserted into the signature without screwing things up.
Implementation
The goal is to build this function:
typedefvoid(^BlockInterposer)(NSInvocation*inv,void(^call)(void));idMAForwardingBlock(BlockInterposerinterposer,idblock);
MAForwardingBlock takes two parameters. The first is the interposer block, which is the block which is called to handle the invocation. The second is the original block to wrap. The interposer gets a block as a parameter which, when called, will call through to the original block using theNSInvocation as the parameters. The function returns a new block which forwards calls to the interposer block passed in.
The first thing to do is to create a new class which will pretend to be a block. Instances of this class will act like blocks and will handle all of the proxying duties. The layout of this class needs to be compatible with the layout of a block. A block contains five fields which can then be followed by other data. There's anisa field (necessary for it to work as an Objective-C object), flags, some reserved space, the block's function pointer, and a pointer to a block descriptor which contains other useful information about the block.
Theisa field is already taken care of, and then the rest can be laid out as instance variables. After the block fields are laid out, other data can follow. In this case, the class stores the interposer block and the original block as instance variables after the block fields:
@interfaceMAFakeBlock :NSObject{int_flags;int_reserved;IMP_invoke;structBlockDescriptor*_descriptor;id_forwardingBlock;BlockInterposer_interposer;}
This class has a single method in its interface, an initializer:
-(id)initWithBlock:(id)blockinterposer:(BlockInterposer)interposer;
Everything else happens through block calling conventions and forwarding, so nothing else needs to be done. The implementation for this method copies and stores the two blocks passed in, and then sets theinvoke field to the forwardingIMP by fetching a method that isn't implemented:
-(id)initWithBlock:(id)blockinterposer:(BlockInterposer)interposer{if((self=[superinit])){_forwardingBlock=[blockcopy];_interposer=[interposercopy];_invoke=[selfmethodForSelector:@selector(thisDoesNotExistOrAtLeastItReallyShouldnt)];}returnself;}
With everything now set up, whenever an instance ofMAFakeBlock is called like a block, it will end up going through the regular Objective-C forwarding machinery. There are two steps in the general forwarding path: first, the runtime fetches the method signature usingmethodSignatureForSelector:, then it constructs anNSInvocation and callsforwardInvocation:.
To figure out the method signature to give to the runtime, we first need to get the method signature of the block being wrapped. This is done by delving into thatBlockDescriptor structure and pulling out the signature. The details are a bit boring, and I'm going to skip over them and simply assume that there's aBlockSig function which takes a block and returns its signature as a C string. For the curious, the code ison GitHub.
NSMethodSignature provides a method to get a signature object from a C string,+signatureWithObjCTypes:. The only wrinkle is that the forwarding machinery will crash if the provided signature doesn't have at least two objects. To fix that, I fake it by adding extra fakevoid * parameters to the signature so that it has at least the required number of parameters. These extra parameters are harmless, although they will be filled with random junk from registers or the stack. ThemethodSignatureForSelector: implementation then looks like this:
-(NSMethodSignature*)methodSignatureForSelector:(SEL)sel{constchar*types=BlockSig(_forwardingBlock);NSMethodSignature*sig=[NSMethodSignaturesignatureWithObjCTypes:types];while([signumberOfArguments]<2){types=[[NSStringstringWithFormat:@"%s%s",types,@encode(void*)]UTF8String];sig=[NSMethodSignaturesignatureWithObjCTypes:types];}returnsig;}
The implementation of-forwardInvocation: is then pretty simple. Change the invocation's target to the original block, then call the interposer:
-(void)forwardInvocation:(NSInvocation*)inv{[invsetTarget:_forwardingBlock];_interposer(inv,^{
The call block that gets passed to the interposer is a bit tricky. In its public interface,NSInvocation only provides methods to invoke it with a particular selector, which goes throughobjc_msgSend. This is no good for calling a block, of course.
Fortunately, there's a private method calledinvokeUsingIMP:. This bypassesobjc_msgSend and simply calls the providedIMP. In practice, it'll call any arbitrary function pointer, as long as it's compatible with the signature that it has. We can then pass it the function pointer for the inner block, and off we go:
[invinvokeUsingIMP:BlockImpl(_forwardingBlock)];});}
Again, I use a little helper function here to deal with internal block structure.BlockImpl fetches the function pointer out of a block. This one is really simple: it just interprets the object as a block structure and fetches theinvoke field. If want to see it,the code is available.
All that remains for this class is a dummy implementation ofcopyWithZone:, since blocks are copied a lot. Nothing has to be done for this implementation besides retaining the fake block, since there isn't any mutable state in this class:
-(id)copyWithZone:(NSZone*)zone{return[selfretain];}
Now that this class is complete, all that remains is the implementation ofMAForwardingBlock. All this function has to do is create and return a new instance of the fake block class, properly initialized:
idMAForwardingBlock(BlockInterposerinterposer,idblock){return[[[MAFakeBlockalloc]initWithBlock:blockinterposer:interposer]autorelease];}
That's it! Now we can proxy blocks. Here's a silly example:
void(^block)(int)=ForwardingBlock(^(NSInvocation*inv,void(^call)(void)){[invsetArgument:&(int){4242}atIndex:1];call();},^(inttestarg){NSLog(@"%d %d",argc,testarg);});block(42);
Even though the block is called with42, the call actually prints4242, since the interposing block changes the argument before calling the original block.
Since this code leverages Cocoa's forwarding machinery, it will work with nearly any block taking nearly any combination of parameters and return values, not just simpleint blocks. It suffers from the same limitations of Cocoa's forwarding, of course. In particular, it's not able to handle blocks that take variable arguments or unions. It also doesn't deal with the peculiarities of struct returns. Because of how struct returns work on most architectures, there's actually a separate forwardingIMP for those. To work with struct returns, this code would have to detect whether the block signature uses the struct return calling convention and fetch that separateIMP instead.
Conclusion
Understanding how mechanisms like message forwarding work at a low level makes it possible to twist them to do entirely new things. Sometimes you get something really useful. Sometimes you just get an interesting toy that can't really be used in real code. While this one is just a toy, it's still an interesting exploration of the guts of the system, and this sort of thing can often lead to real, solid, useful code later on.
That's it for today. Come back in two weeks when I discuss how to use this block proxying code to implementmemoization. Until then, keep sending your ideas for topics. With the occasional exception, Friday Q&A is driven by reader suggestions, so if you have a topic that you would like to see covered here,send it in!
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.