10

I'm using Xcode 4.3.3 and developing for iOS 5.0+. In development of an ARC iOS application, I've started using blocks as a callback mechanism for asynchronous operations. The app works fine in the simulator and on the device.

Then I ran it the profiler for the first time, and it started crashing on me nearly right away - in particular, an EXC_BAD_ACCESS when trying to invoke the first callback block.

After a little investigation, it was clear the difference in behavior was because the profiler runs in "Release mode" by default - in particular, with Optimization Level set to "Fastest, Smallest [-Os]" instead of "None [-O0]".

For example, the following code (simplified for this question) would crash when trying to execute the callbackBlock:

- (void) setCallbackBlock:(void (^)(NSString *input))block
{
    callbackBlock = block;
}

- (void) invokeCallbackWithInput:(NSString *)input
{
    if (callbackBlock) {
        callbackBlock(input);
    }
}

Debugging into it, calling setCallbackBlock with optimization level set to "None", the incoming block would be an NSStackBlock, and the callbackBlock would become an NSMallocBlock.

However, with Optimization Level "Fastest, Smallest", it remained an NSStackBlock.

Changing the setter code to use [block copy] fixes the crashing problem (based on iOS 5 blocks crash only with Release Build).

However, another related question indicates that this shouldn't be necessary with ARC - block variables are copied to the heap in ARC - Why does Objective-C block still work without copying it to the heap?

So my question: What's going on here, and why? (Also, how can both of those answers be correct...?)

Edit: To clarify how callbackBlock is being declared - just above my @implementation where those methods are is this:

@interface MyClass ()
{
    void (^callbackBlock)(NSString *input);
}

@end
Community
  • 1
  • 1
Krease
  • 15,805
  • 8
  • 54
  • 86
  • 1
    My continual mantra on this site is to ONLY use debug when you truely need the full power of lldb. The normal mode of use should be Release. If you had done this you'd never have gotten as far as you did without experiencing problems, and when you had a problem you'd know right where to go. – David H Aug 21 '12 at 19:51
  • amen! My thousand lines program is perfect until I tried the release build... – august7cbfa7b7 Jun 19 '13 at 18:04

2 Answers2

14

So my question: What's going on here, and why? (Also, how can both of those answers be correct...?)

I actually think the answer to the other question is wrong, in that it doesn't answer that particular question about blocks in ARC. The question is about passing a stack based block from one function/method to another. The answer is about something different, which is capturing __block variables within a block. That's a different issue.

The answer to your question is in the FAQ of the Transitioning to ARC Release Notes:

Blocks “just work” when you pass blocks up the stack in ARC mode, such as in a return. You don’t have to call Block Copy any more. You still need to use [^{} copy] when passing “down” the stack into arrayWithObjects: and other methods that do a retain.

So the way this works is that when you pass a block (in your case a block literal allocated on the stack), the compiler does not copy that block when it initializes the parameter for that call. The called function or method has the responsibility to copy that block itself if needed.

Where ARC does copy blocks automatically is when you are returning a block from a function or method. In that case, the compiler knows that it must do a copy to the heap for you, and so it does.

So your setter should be doing a block copy, even with ARC.

I hope that helps.

Community
  • 1
  • 1
Firoze Lafeer
  • 17,133
  • 4
  • 54
  • 48
  • What I don't understand is why his `callbackBlock = block;` line doesn't implicitly behave like `callbackBlock = [block retain];` in ARC? (provided his `callbackBlock` declaration isn't `__weak`) – MechEthan Aug 21 '12 at 20:11
  • 5
    It probably is retaining the block, but it isn't copying it. That's the problem, because the stack based block is gone before it is used later. The block here needs to be copied, not retained. – Firoze Lafeer Aug 21 '12 at 20:12
  • Ahhh, I'd read everything and come to mostly the same conclusions as you, but that one bit was still confusing me. Thank you! – MechEthan Aug 21 '12 at 20:15
  • 3
    +1 for entirely correct and well worded answer. The fact that the quoted and accepted answer is not matching the original question indeed is weird and made me shiver for a moment. Pointing out the difference between copy and retain in your last comment also is extremely important and helpful. Great job on this answer. – Till Aug 21 '12 at 20:21
  • Thanks - this clarifies a lot - though it's still not very clear why the change in optimization level affects whether it automatically converts to a `NSMallocBlock` (in the Debug case) – Krease Aug 21 '12 at 20:58
  • I think this answer is *wrong*, but can't fit why easily into a comment so see other answer... – CRD Aug 21 '12 at 21:05
  • Chuck Hockenberry wrote a nice little article about how "Just because we can forget about typing retain and release, doesn’t necessarily mean that we can forget to type copy, too" under ARC: http://furbo.org/2012/05/04/arc-and-copy/ – Christopher Pickslay Aug 22 '12 at 17:34
  • I'm going to mark this one as the accepted answer - even if it's a bug as pointed out by CRD, it still seems "best practice" to do the copy in ARC. – Krease Aug 22 '12 at 20:13
4

This is a long comment on Firoze's answer.

The document Automatic Reference Counting" section 7.5 states:

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.

And this is the behavior I have seen.

So if callbackBlock is a strong instance variable then ARC should copy any stack-allocated block pass in block. I.e. the Debug version was correct and the Release version is a compiler bug.

If this is correct then you've found a compiler bug and should report it. Reporting it would not be bad either way, it should produce a definitive answer.

CRD
  • 52,522
  • 5
  • 70
  • 86
  • Hm - I just submitted the bug to Apple - we'll see what that results in. – Krease Aug 22 '12 at 19:16
  • Finally heard back from Apple - it's a known open issue and has been reported before as well - I have to take their word for it though, since you can only view bugs you've submitted: http://stackoverflow.com/questions/144873/can-i-browse-other-peoples-apple-bug-reports – Krease Oct 01 '12 at 16:20