6

I love blocks and it makes me sad when I can't use them. In particular, this happens mostly every time I use delegates (e.g.: with UIKit classes, mostly pre-block functionality).

So I wonder... Is it possible -using the crazy power of ObjC-, to do something like this?

   // id _delegate; // Most likely declared as class variable or it will be released
   _delegate = [DelegateFactory delegateOfProtocol:@protocol(SomeProtocol)];
   _delegate performBlock:^{
       // Do something
   } onSelector:@selector(someProtocolMethod)]; // would execute the given block when the given selector is called on the dynamic delegate object.
   theObject.delegate = (id<SomeProtocol>)_delegate;
   // Profit!

performBlock:onSelector:

If YES, how? And is there a reason why we shouldn't be doing this as much as possible?

Edit

Looks like it IS possible. Current answers focus on the first part of the question, which is how. But it'd be nice to have some discussion on the "should we do it" part.

hpique
  • 119,096
  • 131
  • 338
  • 476
  • In other words, I'd like to implement Java's anonymous classes with blocks. – hpique Mar 15 '13 at 17:20
  • This is possible, but in the general case it requires going from an `NSInvocation` to the block by way of libffi. I have a class floating around on my hard drive that does exactly what's in your code snippet, but I'm out of town for a week and won't be able to post much code until I get back. – jscs Mar 15 '13 at 18:01
  • @JoshCaswell Neat. No hurry. :) – hpique Mar 15 '13 at 18:02
  • I've been waiting a while for an opportunity to post about it, too. Figures it would be the first day of vacation! – jscs Mar 15 '13 at 18:05
  • @JoshCaswell Would you be as kind to have a look at my code I just posted? I'm wondering whether I'm on the right track. –  Mar 15 '13 at 18:08
  • See this question: http://stackoverflow.com/questions/14155878/how-to-convert-a-delegate-based-callback-system-into-block-based which seems to have links to some other implementations. – jscs Mar 15 '13 at 18:31
  • @JoshCaswell Looks like REKit does exactly what I want. Will check it out with time. – hpique Mar 16 '13 at 11:09

3 Answers3

10

Okay, I finally got around to putting WoolDelegate up on GitHub. Now it should only take me another month to write a proper README (although I guess this is a good start).

The delegate class itself is pretty straightforward. It simply maintains a dictionary mapping SELs to Block. When an instance recieves a message to which it doesn't respond, it ends up in forwardInvocation: and looks in the dictionary for the selector:

- (void)forwardInvocation:(NSInvocation *)anInvocation {

    SEL sel = [anInvocation selector];
    GenericBlock handler = [self handlerForSelector:sel];

If it's found, the Block's invocation function pointer is pulled out and passed along to the juicy bits:

    IMP handlerIMP = BlockIMP(handler);

    [anInvocation Wool_invokeUsingIMP:handlerIMP];
}

(The BlockIMP() function, along with other Block-probing code, is thanks to Mike Ash. Actually, a lot of this project is built on stuff I learned from his Friday Q&A's. If you haven't read those essays, you're missing out.)

I should note that this goes through the full method resolution machinery every time a particular message is sent; there's a speed hit there. The alternative is the path that Erik H. and EMKPantry each took, which is creating a new clas for each delegate object that you need, and using class_addMethod(). Since every instance of WoolDelegate has its own dictionary of handlers, we don't need to do that, but on the other hand there's no way to "cache" the lookup or the invocation. A method can only be added to a class, not to an instance.

I did it this way for two reasons: this was an excercise to see if I could work out the part that's coming next -- the hand-off from NSInvocation to Block invocation -- and the creation of a new class for every needed instance simply seemed inelegant to me. Whether it's less elegant than my solution, I will leave to each reader's judgement.

Moving on, the meat of this procedure is actually in the NSInvocation category that's found in the project. This utilizes libffi to call a function that's unknown until runtime -- the Block's invocation -- with arguments that are also unknown until runtime (which are accessible via the NSInvocation). Normally, this is not possible, for the same reason that a va_list cannot be passed on: the compiler has to know how many arguments there are and how big they are. libffi contains assembler for each platform that knows/is based on those platforms' calling conventions.

There's three steps here: libffi needs a list of the types of the arguments to the function that's being called; it needs the argument values themselves put into a particular format; then the function (the Block's invocation pointer) needs to be invoked via libffi and the return value put back into the NSInvocation.

The real work for the first part is handled largely by a function which is again written by Mike Ash, called from Wool_buildFFIArgTypeList. libffi has internal structs that it uses to describe the types of function arguments. When preparing a call to a function, the library needs a list of pointers to these structures. The NSMethodSignature for the NSInvocation allows access of each argument's encoding string; translating from there to the correct ffi_type is handled by a set of if/else lookups:

arg_types[i] = libffi_type_for_objc_encoding([sig getArgumentTypeAtIndex:actual_arg_idx]);

...

if(str[0] == @encode(type)[0]) \
{ \
    if(sizeof(type) == 1) \
        return &ffi_type_sint8; \
    else if(sizeof(type) == 2) \
        return &ffi_type_sint16; \

Next, libffi wants pointers to the argument values themselves. This is done in Wool_buildArgValList: get the size of each argument, again from the NSMethodSignature, and allocate a chunk of memory that size, then return the list:

NSUInteger arg_size;
NSGetSizeAndAlignment([sig getArgumentTypeAtIndex:actual_arg_idx], 
                      &arg_size, 
                      NULL);
/* Get a piece of memory that size and put its address in the list. */
arg_list[i] = [self Wool_allocate:arg_size];
/* Put the value into the allocated spot. */
[self getArgument:arg_list[i] atIndex:actual_arg_idx];

(An aside: there's several notes in the code about skipping over the SEL, which is the (hidden) second passed argument to any method invocation. The Block's invocation pointer doesn't have a slot to hold the SEL; it just has itself as the first argument, and the rest are the "normal" arguments. Since the Block, as written in client code, could never access that argument anyways (it doesn't exist at the time), I decided to ignore it.)

libffi now needs to do some "prep"; as long as that succeeds (and space for the return value can be allocated), the invocation function pointer can now be "called", and the return value can be set:

ffi_call(&inv_cif, (genericfunc)theIMP, ret_val, arg_vals);
if( ret_val ){
    [self setReturnValue:ret_val];
    free(ret_val);
}

There's some demonstrations of the functionality in main.m in the project.

Finally, as for your question of "should this be done?", I think the answer is "yes, as long as it makes you more productive". WoolDelegate is completely generic, and an instance can act like any fully written-out class. My intention for it, though, was to make simple, one-off delegates -- that only need one or two methods, and don't need to live past their delegators -- less work than writing a whole new class, and more legible/maintainable than sticking some delegate methods into a view controller because it's the easiest place to put them. Taking advantage of the runtime and the language's dynamism like this hopefully can increase your code's readability, in the same way, e.g., Block-based NSNotification handlers do.

jscs
  • 63,694
  • 13
  • 151
  • 195
5

I just put together a little project that lets you do just this...

@interface EJHDelegateObject : NSObject

+ (id)delegateObjectForProtocol:(Protocol*) protocol;

@property (nonatomic, strong) Protocol *protocol;
- (void)addImplementation:(id)blockImplementation forSelector:(SEL)selector;

@end


@implementation EJHDelegateObject
static NSInteger counter;

+ (id)delegateObjectForProtocol:(Protocol *)protocol 
{
    NSString *className = [NSString stringWithFormat:@"%s%@%i",protocol_getName(protocol),@"_EJH_implementation_", counter++];
    Class protocolClass = objc_allocateClassPair([EJHDelegateObject class], [className cStringUsingEncoding:NSUTF8StringEncoding], 0);
    class_addProtocol(protocolClass, protocol);
    objc_registerClassPair(protocolClass);
    EJHDelegateObject *object = [[protocolClass alloc] init];
    object.protocol = protocol;
    return object;
}


- (void)addImplementation:(id)blockImplementation forSelector:(SEL)selector
{
    unsigned int outCount;
    struct objc_method_description *methodDescriptions = protocol_copyMethodDescriptionList(self.protocol, NO, YES, &outCount);
    struct objc_method_description description;
    BOOL descriptionFound = NO;
    for (int i = 0; i < outCount; i++){
        description = methodDescriptions[i];
        if (description.name == selector){
            descriptionFound = YES;
            break;
        }
    }
    if (descriptionFound){
        class_addMethod([self class], selector, imp_implementationWithBlock(blockImplementation), description.types);
    }
}

@end

And using an EJHDelegateObject:

self.alertViewDelegate = [EJHDelegateObject delegateObjectForProtocol:@protocol(UIAlertViewDelegate)];
[self.alertViewDelegate addImplementation:^(id _self, UIAlertView* alertView, NSInteger buttonIndex){
    NSLog(@"%@ dismissed with index %i", alertView, buttonIndex);
} forSelector:@selector(alertView:didDismissWithButtonIndex:)];

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Example" message:@"My delegate is an EJHDelegateObject" delegate:self.alertViewDelegate cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil];
[alertView show];
ehope
  • 506
  • 3
  • 9
  • This won't work. The first parameter of the block must be the object that the method was called on (in this case, the `whatever_EJH_implementation_` object) – newacct Mar 16 '13 at 00:33
  • @newacct imp_implementationWithBlock handles that for us – ehope Mar 16 '13 at 00:38
  • do you understand what `imp_implementationWithBlock` does? it takes a block which has an object first argument, and returns an IMP, that when called, calls the block with the receiver as the first argument and the "regular arguments" as the remaining arguments. Try it if you don't believe me. – newacct Mar 16 '13 at 00:39
  • Ah yes you are right. Fixed, block should have the receiver as the first parameter. Thanks! – ehope Mar 16 '13 at 04:39
2

Edit: This is what I've come up after having understood your requirement. This is just a quick hack, an idea to get you started, it's not properly implemented, nor is it tested. It is supposed to work for delegate methods that take the sender as their only argument. It works It is supposed to work with normal and struct-returning delegate methods.

typedef void *(^UBDCallback)(id);
typedef void(^UBDCallbackStret)(void *, id);

void *UBDDelegateMethod(UniversalBlockDelegate *self, SEL _cmd, id sender)
{   
    UBDCallback cb = [self blockForSelector:_cmd];
    return cb(sender);
}

void UBDelegateMethodStret(void *retadrr, UniversalBlockDelegate *self, SEL _cmd, id sender)
{
    UBDCallbackStret cb = [self blockForSelector:_cmd];
    cb(retaddr, sender);
}

@interface UniversalBlockDelegate: NSObject

- (BOOL)addDelegateSelector:(SEL)sel isStret:(BOOL)stret methodSignature:(const char *)mSig block:(id)block;

@end

@implementation UniversalBlockDelegate {
    SEL selectors[128];
    id blocks[128];
    int count;
}

- (id)blockForSelector:(SEL)sel
{
    int idx = -1;
    for (int i = 0; i < count; i++) {
        if (selectors[i] == sel) {
            return blocks[i];
        }
    }

    return nil; 
}

- (void)dealloc
{
    for (int i = 0; i < count; i++) {
        [blocks[i] release];
    }
    [super dealloc];
}

- (BOOL)addDelegateSelector:(SEL)sel isStret:(BOOL)stret methodSignature:(const char *)mSig block:(id)block
{
    if (count >= 128) return NO;

    selectors[count] = sel;
    blocks[count++] = [block copy];

    class_addMethod(self.class, sel, (IMP)(stret ? UBDDelegateMethodStret : UBDDelegateMethod), mSig);

    return YES;
}

@end

Usage:

UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
UniversalBlockDelegate *d = [[UniversalBlockDelegate alloc] init];
webView.delegate = d;
[d addDelegateSelector:@selector(webViewDidFinishLoading:) isStret:NO methodSignature:"v@:@" block:^(id webView) {
    NSLog(@"Web View '%@' finished loading!", webView);
}];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://google.com"]]];
  • You're assuming I'm writing the class that has the delegate. If it were the case this wouldn't be a problem. I'll clarify it. – hpique Mar 15 '13 at 17:24
  • @hpique I see. So basically you want a block to be called instead of a delegate sent a message? –  Mar 15 '13 at 17:27
  • By `performBlock:onSelector:` I meant to perform the given block when the given selector is called on the `_delegate` object. – hpique Mar 15 '13 at 17:28
  • That's it. In my mind this has encapsulation advantages, and makes the code contextual, and thus easier to read. – hpique Mar 15 '13 at 17:33
  • @hpique Well. I actually tried to whip up a class that enables blocks to be called instead of selectors on a class, but I'm not getting any further because I would need to pass down variadic arguments to `objc_msgSend()` and/or `objc_msgSend_stret()`. –  Mar 15 '13 at 17:50
  • I'm going to be looking at OCMock's code this weekend. They seem to be doing something very similar. https://github.com/erikdoe/ocmock – hpique Mar 15 '13 at 18:00
  • @hpique I've written something less general with a few restrictions. I hope this will help you. –  Mar 15 '13 at 18:09
  • @H2CO3: `objc_msgSend()` is probably too early. Intercept the process later, at `forwardInvocation:`. You still need assembler to get the arguments into the block, but libffi will allow you to do it. – jscs Mar 15 '13 at 18:11
  • @JoshCaswell Check the update I posted - it's not universal, but it *may* work with some restrictions regarding the signature of the delegate method. –  Mar 15 '13 at 18:13
  • 1
    I wouldn't be surprised to see you implement this before I get back home so I can post my version. – jscs Mar 15 '13 at 18:15
  • Oh, yes, `class_addMethod()` _is_ the other way to make this work! I had forgotten about that, but that reminds me that there is an implementation of this concept out there. I didn't like the solution because it created a new _class_ for every delegate _instance_ you needed, but it certainly is clever and does work. Let me see if I can find it... – jscs Mar 15 '13 at 18:22
  • @JoshCaswell Cool, lemme see it! –  Mar 15 '13 at 18:57
  • 1
    This is pretty likely completely overly simplistic, but I put together a little sample project with an implementation that uses objc_methodsend. @JoshCaswell like you said it creates a new *class* for every *instance*... Anyway probably missing something major why this implementation stinks -- https://github.com/ehopealot/EJHDelegateObject – ehope Mar 15 '13 at 19:01
  • 1
    Found the one I was thinking of! EMKAssociateDelegate in [EMKPantry](https://github.com/BenedictEMK/EMKPantry/tree/origin/Classes/EMKPantry/Foundation) @hpique – jscs Mar 15 '13 at 21:39