Pioneering Software

Innovation, quality, care.

Out Parameters When ARCing

| Comments

Be careful when assigning values to out-parameters when Automatic Reference Counting. Ensure that there is no auto-release pool in-between where you assign the parameter and where you use the out-parameter. Easily overlooked when using C blocks, especially when passing blocks to Cocoa APIs which can enclose your handler within an auto-release pool without you realising it.

Problem

See example below. It represents the kind of situation not-uncommonly found within Cocoa software: a message argument passes an error by reference, a pointer to a pointer. Please note, the example is entirely fictional. The program below does nothing useful, except to demonstrate the principle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*
 * Demonstrates EXC_BAD_ACCESS across an auto-release pool boundary.
 */

#import <Foundation/Foundation.h>

@interface Something : NSObject

- (void)doWithError:(NSError **)outError;

@end

@implementation Something

- (void)doWithError:(NSError **)outError
{
    @autoreleasepool
    {
        *outError = [NSError errorWithDomain:@"Emergency" code:999 userInfo:nil];
    }
}

@end

int main(int argc, const char *argv[])
{
    @autoreleasepool
    {
        NSError *__autoreleasing error = nil;
        // The argument type implicitly becomes
        //
        //  (NSError *__autoreleasing *)
        //
        [[[Something alloc] init] doWithError:&error];

        // At this point, the main thread gives EXC_BAD_ACCESS.
        NSLog(@"%@", error);
    }
    return 0;
}

@interface NSError(AutomaticReferenceCounting)

@end

@implementation NSError(AutomaticReferenceCounting)

- (void)dealloc
{
    // Place a breakpoint here to see when the error deallocates. You will see
    // that the error deallocates at the next autorelease pool, naturally.
    NSLog(@"dealloc for %@ at %p", NSStringFromClass([self class]), self);
}

@end

Under the Automatic Reference Counting using the LLVM compiler, the outError argument implicitly adds __autoreleasing to its type. In other words

NSError **

actually becomes

NSError *__autoreleasing *

See indirect parameters for the rationale. This additional implicit qualifier implies that the NSError pointer exists on the stack frame subject to release by an auto-release pool.

No problem so far.

Looking at line 19, you would never deliberately enclose the out-parameter assignment within an autorelease pool. The situation might arise however inadvertently when you attempt to do the same thing from within a completion handler block sent to one of Apple’s APIs, e.g. sending a synchronous URL requestor running a block with a Core Data context. The handler block makes the assignment but while unwinding the stack, a hidden auto-release pool deallocates and reclaims the error object leaving its auto-releasing pointer sitting on the enclosing stack frame dangling in space. When you attempt to access the error, you of course see EXC_BAD_ACCESS; the error has disappeared.

So look out for that!

Solution

The solution is relatively straightforward. Temporarily assign the out-parameter’s value to an automatic (stack frame) variable with __strong or no qualifier. No qualifier is the same as strong. Then just before returning, assign the out-parameter, e.g.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)doWithError:(NSError **)outError
{
    NSError *error = nil;
    @autoreleasepool
    {
        error = [NSError errorWithDomain:@"Emergency" code:999 userInfo:nil];
    }

    // The autorelease pool did not deallocate the error because the automatic
    // "error" variable retains a strong reference.
    if (outError && *outError == nil)
    {
        *outError = error;
    }
}

Comments