Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I create a block that 'wraps' a target/selector pair?

I love blocks, and they are very cool.

However, I find that blocks can clutter up my code and make it harder to read without folding all of them up inside Xcode (which I don't like doing).

I like splitting my code into logical methods (selectors) to keep it easier to read, but it appears (on the surface) that this isn't readily possible with frameworks like dispatch, AFNetworking, and several others.

I also don't care for the delegate approach, as that means I cannot name my methods as I would like to, instead relying on what other people think I need.

So, without writing a bunch of glue code like this:

-(void) reloadData { 
    ...
    [[EventsManager instance] data:YES async:^(NSArray *events) { [self processEvents:events] }];
    ...
}

I could instead do something like this:

-(void) reloadData {
    ...
    [[EventsManager instance] data:YES async:createBlock(self, @selector(processEvents:))];
    ...
}

Which is easier to read (to me).

With the power that we have with objective-c, and it's runtime, this should be possible, no? I haven't seen anything like this out there, though.

like image 225
Richard J. Ross III Avatar asked Apr 27 '13 20:04

Richard J. Ross III


2 Answers

I liked your answer from an academic standpoint; +1 and, clearly, you learned something.

From a practical perspective, it seems like an awful lot of added fragility for very little reduction in typing while it also leads to some information loss at the call site.

The advantage of this is that it is exactly explicit:

-(void) reloadData { 
    ...
    [[EventsManager instance] data:YES async:^(NSArray *events) { [self processEvents:events] }];
    ...
}

Reading that, one sees that the asynchronous callback block is required to process the arguments and that the processEvents: method on self will be used to do the actual work.

The expression createBlock(self, @selector(processEvents:)) is a lossy representation of the same; it loses the explicit argumentation of the callback and the mapping between that argumentation and the method being invoked (I often see callback blocks like the above with multiple arguments where there is some lightweight logic and/or argument processing before invoking the method).

Note also that processing a non-varargs call site as a varargs when called is a violation of the C standard and will not work on certain ABIs with certain lists of arguments.

like image 69
bbum Avatar answered Oct 14 '22 08:10

bbum


Yes, this is indeed possible, but this solution is ABI-specific (not guaranteed to work on all platforms), and makes extensive use of the information available at run-time about methods.

What we first must do is get information about the method we are wrapping with the block. This is done via NSMethodSignature, which contains information such as:

  • Number of arguments
  • Size (in bytes) of each argument
  • Size of return type

This allows us to wrap (almost) any method with no specific code for that method, thus creating a re-usable function.

Secondly, we need a way to safely dispatch method calls at run-time. We do this via NSInvocation, which grants us the ability to create a dynamic, and safe, method call at run-time.

Thirdly, we need to have a block that can take any number of arguments passed in, and the dispatch that. This is done via C's va_list APIs, and should work for 99% of methods.

Finally, we need to get the return value, and be able to return that from our block. This is the part of the entire operation that is possible to not work, because of weirdness with returning structs and such with the Objective-C runtime.

However, as long as you keep to primitive types and Objective-C objects, this code should work great for you.

A couple of things to note about this implementation:

  • It is reliant upon undefined behavior with casting of block & function types, however, because of the calling conventions of iOS and Mac, this should not pose any issues (unless your method has a different return type than what the block expects).

  • It also relies upon undefined behavior with the result of calling va_arg with a type that may not be what is passed - however, since the types are of the same size, this should never be an issue.

Without any further ado, here is an example of the code, followed by the implementation:


@interface MyObj : NSObject

-(void) doSomething;

@end

@implementation MyObj

-(void) doSomething
{
    NSLog(@"This is me, doing something! %p", self);
}

-(id) doSomethingWithArgs:(long) arg :(short) arg2{
    return [NSString stringWithFormat:@"%ld %d", arg, arg2];
}

@end

int main() {
    // try out our selector wrapping
    MyObj *obj = [MyObj new];

    id (^asBlock)(long, short) = createBlock(obj, @selector(doSomethingWithArgs::));
    NSLog(@"%@", asBlock(123456789, 456));
}

/* WARNING, ABI SPECIFIC, BLAH BLAH BLAH NOT PORTABLE! */
static inline void getArgFromListOfSize(va_list *args, void *first, size_t size, size_t align, void *dst, BOOL isFirst) {
    // create a map of sizes to types
    switch (size) {
            // varargs are weird, and are aligned to 32 bit boundaries. We still only copy the size needed, though.
            // these cases should cover all 32 bit pointers (iOS), boolean values, and floats too.
        case sizeof(uint8_t): {
            uint8_t tmp = isFirst ? (uint32_t) first : va_arg(*args, uint32_t);
            memcpy(dst, &tmp, size);
            break;
        }

        case sizeof(uint16_t): {
            uint16_t tmp = isFirst ? (uint32_t) first : va_arg(*args, uint32_t);
            memcpy(dst, &tmp, size);
            break;
        }

        case sizeof(uint32_t): {
            uint32_t tmp = isFirst ? (uint32_t) first : va_arg(*args, uint32_t);
            memcpy(dst, &tmp, size);
            break;
        }

            // this should cover 64 bit pointers (Mac), and longs, and doubles
        case sizeof(uint64_t): {
            uint64_t tmp = isFirst ? (uint64_t) first : va_arg(*args, uint64_t);
            memcpy(dst, &tmp, size);
            break;
        }
            /* This has to be commented out to work on iOS (as CGSizes are 64 bits)
            // common 'other' types (covers CGSize, CGPoint)
        case sizeof(CGPoint): {
            CGPoint tmp = isFirst ? *(CGPoint *) &first : va_arg(*args, CGPoint);
            memcpy(dst, &tmp, size);
            break;
        }
             */

            // CGRects are fairly common on iOS, so we'll include those as well
        case sizeof(CGRect): {
            CGRect tmp = isFirst ? *(CGRect *) &first : va_arg(*args, CGRect);
            memcpy(dst, &tmp, size);
            break;
        }

        default: {
            fprintf(stderr, "WARNING! Could not bind parameter of size %zu, unkown type! Going to have problems down the road!", size);
            break;
        }
    }
}

id createBlock(id self, SEL _cmd) {
    NSMethodSignature *methodSig = [self methodSignatureForSelector:_cmd];

    if (methodSig == nil)
        return nil;

    return ^(void *arg, ...) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];

        [invocation setTarget:self];
        [invocation setSelector:_cmd];

        NSUInteger argc = [methodSig numberOfArguments];
        va_list args;
        va_start(args, arg);

        for (int argi = 2; argi < argc; argi++) {
            const char *type = [methodSig getArgumentTypeAtIndex:argi];

            NSUInteger size;
            NSUInteger align;

            // get the size
            NSGetSizeAndAlignment(type, &size, &align);

            // find the right type
            void *argument = alloca(size);

            getArgFromListOfSize(&args, arg, size, align, argument, argi == 2);

            [invocation setArgument:argument atIndex:argi];
        }

        va_end(args);

        [invocation invoke];

        // get the return value
        if (methodSig.methodReturnLength != 0) {
            void *retVal = alloca(methodSig.methodReturnLength);
            [invocation getReturnValue:retVal];

            return *((void **) retVal);
        }

        return nil;
    };
}

Let me know if you have any issues with this implementation!

like image 30
Richard J. Ross III Avatar answered Oct 14 '22 09:10

Richard J. Ross III