elado
elado

Reputation: 8760

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?

Upvotes: 2

Views: 2030

Answers (2)

user102008
user102008

Reputation: 31323

The problem is that when a block is created, it will copy (make a separate copy) of any non-__block variables it captures. Since test1 is uninitialized at the time your block is created, you will be using an uninitialized pointer for test1 when you run the block.

The proper solution is to declare test1 with __block. That way, state is shared between the block and the enclosing scope, and after test1 is assigned in the enclosing scope, the block can access the changed value:

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

p.s. The 3rd example (doing alloc before and then assigning the result of init) is not reliable because in general an object's init method is not guaranteed to return the object it is called on (init is allowed to deallocate itself and return nil if it failed, for example).


Update: The above code is only for MRC, as __block variables are not retained by the block.

But in ARC, the above code will cause a retain cycle, as __block object pointer variables are by default retained by the block. In ARC, the correct code is:

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

Upvotes: 1

user557219
user557219

Reputation:

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.

Upvotes: 5

Related Questions