9

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.

Richard J. Ross III
  • 55,009
  • 24
  • 135
  • 201
  • Just a note to anyone reading this who may be confused: I answered my own question, as I figured this out on my own. – Richard J. Ross III Apr 27 '13 at 20:06
  • 1
    You worked it out in zero minutes flat, good work! – Wain Apr 27 '13 at 20:10
  • @Wain hardly. Took me about 2 days of solid work to get this. – Richard J. Ross III Apr 27 '13 at 20:10
  • No offence intended, but you didn't post this as a question so it should probably be a community wiki? – Wain Apr 27 '13 at 20:17
  • 2
    @wain nope, self answered questions like this are encouraged not to be CW: http://meta.stackexchange.com/questions/153113/posting-self-answered-questions-for-information-should-they-be-community-wiki – Richard J. Ross III Apr 27 '13 at 20:19
  • 2
    @Wain Self-answered questions are allowed and actively encouraged, as long as they follow the usual guidelines and standards for answers and questions. There is no problem here and no need to make anything community wiki. Any reputation gained from upvotes is well deserved. – Bart Apr 28 '13 at 20:52
  • I know I'm a little late to the party, but didn't we invent [trampolines](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/ReactiveCocoaFramework/ReactiveCocoa/RACBlockTrampoline.h) to solve this stuff? Granted, the GitHub guys like to hack around with the preprocessor more than the runtime, but still. – CodaFi Jun 24 '13 at 06:54
  • @CodaFi As far as I can tell, you pass that method a block, and it invokes it dynamically. This *creates* a block from a target & selector pair instead. – Richard J. Ross III Jun 24 '13 at 15:40
  • But you can see how easy it would be to refactor the trampoline into this. Have the trampoline return a block constructed with the metamacros in that file. – CodaFi Jun 24 '13 at 18:45
  • @CodaFi selectors are a bit more complex than a block, especially with types that aren't objects, which is more common in methods than blocks. Also, C++ solves that problem as well, templates can actually do all this stuff at compile time with no UB. – Richard J. Ross III Jun 24 '13 at 18:47

2 Answers2

14

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.

bbum
  • 162,346
  • 23
  • 271
  • 359
  • 8
    @RichardJ.RossIII - I don't know that we need to be so strict here. He's added some good information to your self-answered question, so I don't think this qualifies as a broken window to be removed. His answer wouldn't work in a comment, so I have no problems with it remaining. – Brad Larson Apr 28 '13 at 02:06
6

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!

Richard J. Ross III
  • 55,009
  • 24
  • 135
  • 201
  • 1
    Seems like an awful lot of magic (but is quite cool; I didn't look at the implementation in detail, but it looks solid). Note that treating non-varargs call sites as variable arguments when called is a violation of the C ABI and *will* break on certain architectures with certain argument lists. – bbum Apr 27 '13 at 21:34
  • @bbum that is correct, and I included that in my answer, it on the archs where objc is most likely to be used (x86, ARM) this isn't a problem. In fact, I used the same thing in my famous "iOS app in C" answer. – Richard J. Ross III Apr 27 '13 at 22:00
  • the first arg should not be declared `id`, since ARC will try to retain it and if it's not `id`, it will crash – newacct Apr 28 '13 at 00:07
  • @newacct turns out that endianness was the problem, it should now be fixed, try it now! – Richard J. Ross III Apr 28 '13 at 14:16
  • I have another problem (iOS simulator): `@implementation MyObj -(id) doSomethingWithArgs:(char)arg :(char)arg2 :(char)arg3 { return [NSString stringWithFormat:@"%d %d %d", arg, arg2, arg3]; } @end` ... and then it's producing the wrong result: `MyObj *obj = [MyObj new]; NSLog(@"%@", [obj doSomethingWithArgs:42 :17 :5]); /* prints 42 17 5 */ id (^asBlock)(char, char, char) = createBlock(obj, @selector(doSomethingWithArgs:::)); NSLog(@"%@", asBlock(42, 17, 5)); /* prints 42 17 17 */` – newacct May 01 '13 at 09:47
  • @newacct interesting. It's entirely possible that that is the result of the undefined behavior bbum is talking about (as integers with varargs functions are aligned to 32 bit boundaries and not 8 bit), but let me see what I can do. – Richard J. Ross III May 01 '13 at 11:56
  • @newacct I cannot reproduce (running it on Mac OSX). Are you sure that you have the current version of the implementation? – Richard J. Ross III May 01 '13 at 12:06
  • @RichardJ.RossIII: it happens for me on the iOS simulator, not as a mac app. I'll try to debug it further – newacct May 02 '13 at 10:41
  • @RichardJ.RossIII: upon further debugging, it appears to be caused by passing the `va_list` to the `getArgFromListOfSize` function. C99 standard, section 7.15 para. 3: "The object ap may be passed as an argument to another function; if that function invokes the va_arg macro with parameter ap, the value of ap in the calling function is indeterminate and shall be passed to the va_end macro prior to any further reference to ap." So it seems you cannot repeatedly pass the `va_list` to another function for it to call `va_arg`. The footnote in the standard suggests to pass `va_list *` instead – newacct May 02 '13 at 21:52
  • @newacct Very interesting. Is it possible that its inlined on Mac but not on iPhone? Maybe a force inline attribute would work. – Richard J. Ross III May 02 '13 at 22:35
  • @newacct Well by golly, you're right! I just plugged a pointer into it on the simulator and it works! Turns out on mac, `va_list` is already a pointer type behind the scenes, but not on iOS. Thanks for helping me figure that out! – Richard J. Ross III May 03 '13 at 00:02