Craig Gidney
Craig Gidney

Reputation: 18316

When *exactly* is it necessary to copy a block in objective-C under ARC?

I've been getting conflicting information about when I need to copy a block when using ARC in objective-C. Advice varies from "always" to "never", so I'm really not sure what to make of it.

I happen to have a case I don't know how to explain:

-(RemoverBlock)whenSettledDo:(SettledHandlerBlock)settledHandler {
    // without this local assignment of the argument, one of the tests fails. Why?
    SettledHandler handlerFixed = settledHandler;

    [removableSettledHandlers addObject:handlerFixed];

    return ^{
        [removableSettledHandlers removeObject:handlerFixed];
    };
}

Which is called with a block inline like this:

-(void) whatever {
    [self whenSettledDo:^(...){
        ...
    }];
}

(The actual code this snipper was adapted from is here.)

What does copying the argument to the local variable change here? Is the version without the local making two distinct copies, one for addObject and one for removeObject, so the removed copy doesn't match the added copy?

Why or when isn't ARC handling blocks correctly? What does it guarantee and what are my responsibilities? Where is all of this documented in a non-vague fashion?

Upvotes: 1

Views: 330

Answers (3)

gnasher729
gnasher729

Reputation: 52632

A block must be copied when the application leaves the scope where the block was defined. A bad example:

BOOL yesno;
dispatch_block_t aBlock;
if (yesno)
{
    aBlock = ^(void) { printf ("yesno is true\n");
}
else
{
    aBlock = ^(void) { printf ("yesno is false\n");
}
aBlock = [aBlock copy];

It's too late already! The block has left its scope (the { brackets } ) and things can go wrong. This could have been fixed trivially by not having the { brackets }, but it is one of the rare cases where you call copy yourself.

When you store a block away somewhere, 99.99% of the time you are leaving the scope where the block was declared; usually this is solved by making block properties "copy" properties. If you call dispatch_async etc. the block needs to be copied, but the called function will do that. The block based iterators for NSArray and NSDictionary typically don't have to make copies of the block because you are still running inside the scope where the block was declared.

[aBlock copy] when the block was already copied doesn't do anything, it just returns the block itself.

Upvotes: 0

newacct
newacct

Reputation: 122518

In C, correctness cannot be inferred from running any number of tests, because you could be seeing undefined behavior. To properly know what is correct, you need to consult the language specification. In this case, the ARC specification.

It is instructive to first review when it is necessary to copy a block under MRC. Basically, a block that captures variables can start out on the stack. What this means is when you see a block literal, the compiler can replace it with a hidden local variable in that scope that contains the object structure itself, by value. Since local variables are only valid in the scope they are declared in, that is why blocks from block literals are only valid in the scope the literal is in, unless it is copied.

Furthermore, there is the additional rule that, if a function takes a parameter of block pointer type, it makes no assumptions about whether it's a stack block or not. It is only guaranteed that the block is valid at the time the block is called. However, this pretty much means that the block is valid for the entire duration of the function call, because 1) if it is a stack block, and it is valid when the function was called, that means somewhere up the stack where the block was created, the call is still within the scope of the stack literal; therefore it will still be in scope by the end of the function call; 2) if it is a heap block or global block, it is subject to the same memory management rules as other objects.

From this, we can deduce where it is necessary to copy. Let's consider some cases:

  • If the block from a block literal is returned from the function: It needs to be copied, since the block escapes from the scope of the literal
  • If the block from a block literal is stored in an instance variable: It needs to be copied, since the block escapes from the scope of the literal
  • If the block is captured by another block: It does not need to be copied, since the capturing block, if copied, will retain all captured variables of object type AND copy all captured variables of block type. Thus, the only situation where our block would escape this scope would be if the block that captures it escapes the scope; but in order to do that, that block must be copied, which in turn copies our block.
  • If the block from a block literal is passed to another function, and that function's parameter is of block pointer type: It does not need to be copied, since the function does not assume that it was copied. This means that any function that takes a block and needs to "store it for later" must take responsibility for copying the block. And indeed this is the case (e.g. dispatch_async).
  • If the block from a block literal is passed to another function, and that function's parameter is not of block pointer type (e.g. -addObject:): It needs to be copied if you know that this function stores it for later. The reason it needs to be copied is that the function cannot take responsibility for copying the block, since it does not know it is taking a block.

So if your code in the question was in MRC, -whatever would not need to copy anything. -whenSettledDo: will need to copy the block, since it is passed to addObject:, a method that takes a generic object, type id, and doesn't know it's taking a block.


Now, let's look at which of these copies ARC takes care for you. Section 7.5 says

With the exception of retains done as part of initializing a __strong parameter variable or reading a __weak variable, whenever these semantics call for retaining a value of block-pointer type, it has the effect of a Block_copy. The optimizer may remove such copies when it sees that the result is used only as an argument to a call.

What the first part means is that, in most places where you assign to a strong reference of block pointer type (which normally causes a retain for object pointer types), it will be copied. However, there are some exceptions: 1) In the beginning of the first sentence, it says that a parameter of block pointer type is not guaranteed to be copied; 2) In the second sentence, it says that if a block is only used as an argument to a call, it is not guaranteed to be copied.

What does this mean for the code in your question? handlerFixed is a strong reference of block pointer type, and the result is used in two places, more than just an argument to a call, thus assigning to it assigns a copy. If however, you had passed a block literal directly to addObject:, then there is not guaranteed to be a copy (since it's used only as an argument to a call), and you would need to copy it explicitly (as we discussed that the block passed to addObject: needs to be copied).

When you used settledHandler directly, since settledHandler is a parameter, it is not automatically copied, so when you pass it to addObject:, you need to copy it explicitly, because as we discussed that the block passed to addObject: needs to be copied.

So in conclusion, in ARC you need to explicitly copy when passing a block to a function that doesn't specifically take block arguments (like addObject:), if it's a block literal, or it's a parameter variable that you're passing.

Upvotes: 3

Craig Gidney
Craig Gidney

Reputation: 18316

I've confirmed that my particular issue was in fact making two distinct copies of the block. Tricky tricky. This implies the proper advice is "never copy, unless you want to be able to compare the block to itself".

Here's the code I used to test it:

-(void) testMultipleCopyShenanigans {
    NSMutableArray* blocks = [NSMutableArray array];
    NSObject* v = nil;
    TOCCancelHandler remover = [self addAndReturnRemoverFor:^{ [v description]; } 
                                                         to:blocks];
    test(blocks.count == 1);
    remover();
    test(blocks.count == 0); // <--- this test fails
}
-(void(^)(void))addAndReturnRemoverFor:(void(^)(void))block to:(NSMutableArray*)array {
    NSLog(@"Argument: %@", block);
    [array addObject:block];
    NSLog(@"Added___: %@", array.lastObject);
    return ^{
        NSLog(@"Removing: %@", block);
        [array removeObject:block];
    };
}

The logging output when running this test is:

Argument: <__NSStackBlock__: 0xbffff220>
Added___: <__NSMallocBlock__: 0x2e283d0>
Removing: <__NSMallocBlock__: 0x2e27ed0>

The argument is an NSStackBlock, stored on the stack. In order to be placed in the array or the closure it must be copied to the heap. But this happens once for the addition to the array and once for the closure.

So the NSMallocBlock in the array has an address ending in 83d0 whereas the one in the closure that is removed from the array has an address ending in 7ed0. They are distinct. Removing one doesn't count as removing the other.

Bleh, guess I need to watch out for that in the future.

Upvotes: 0

Related Questions