Search code examples
objective-cclosuresobjective-c-blockshost-object

Objective-C Blocks with closure that references the host object


I've been playing with blocks and encountered a weird behavior. This is the interface/implementation, which just holds a block with the ability to execute it:

@interface TestClass : NSObject {
#if NS_BLOCKS_AVAILABLE
    void (^blk)(void);
#endif
}
- (id)initWithBlock:(void (^)(void))block;
- (void)exec;
@end

@implementation TestClass
#if NS_BLOCKS_AVAILABLE
- (id)initWithBlock:(void (^)(void))block {
    if ((self = [super init])) {
        blk = Block_copy(block);
    }
    return self;
}
- (void)exec {
    if (blk) blk();
}
- (void)dealloc {
    Block_release(blk);
    [super dealloc];
}
#endif
@end

While a regular instantiation and passing a regular block works:

TestClass *test = [[TestClass alloc] initWithBlock:^{
    NSLog(@"TestClass");
}];
[test exec];
[test release];

Using a block with reference to the object which is being created doesn't:

TestClass *test1 = [[TestClass alloc] initWithBlock:^{
    NSLog(@"TestClass %@", test1);
}];
[test1 exec];
[test1 release];

Error is EXC_BAD_ACCESS, stack trace on Block_copy(block); Debugger on: 0x000023b2 <+0050> add $0x18,%esp

I kept playing around, and moved the allocation code above the initialization, it worked:

TestClass *test2 = [TestClass alloc];
test2 = [test2 initWithBlock:^{
    NSLog(@"TestClass %@", test2);
}];
[test2 exec];
[test2 release];

And combining both snippets works too:

TestClass *test1 = [[TestClass alloc] initWithBlock:^{
    NSLog(@"TestClass %@", test1);
}];
[test1 exec];
[test1 release];

TestClass *test2 = [TestClass alloc];
test2 = [test2 initWithBlock:^{
    NSLog(@"TestClass %@", test2);
}];
[test2 exec];
[test2 release];

What's going on here?


Solution

  • In an assignment expression, the rvalue is evaluated before being assigned to the lvalue.

    This means that in:

    TestClass *test1 = [[TestClass alloc] initWithBlock:^{
        NSLog(@"TestClass %@", test1);
    }];
    

    the following sequence of operations is performed. Edit: as pointed out by Jonathan Grynspan, there’s no defined order for steps 1 and 2 so it could be the case that step 2 is executed before step 1.

    1. Send +alloc to TestClass
    2. Create a block that refers to test1, which hasn’t been initialised yet. test1 contains an arbitrary memory address.
    3. Send -initWithBlock: to the object created in step 1.
    4. Assign the rvalue to test1.

    Note that test1 points to a valid object only after step 4.

    In:

    TestClass *test2 = [TestClass alloc];
    test2 = [test2 initWithBlock:^{
        NSLog(@"TestClass %@", test2);
    }];
    [test2 exec];
    [test2 release];
    

    the sequence is:

    1. Send +alloc to TestClass
    2. Assign the rvalue to test2, which now points to a TestClass object.
    3. Create a block that refers to test2, which points to the TestClass object per step 2.
    4. Send -initWithBlock: to test2, which was correctly assigned in step 2.
    5. Assign the rvalue to test2.