NSInvocation & NSError - __autoreleasing & memory crasher
Asked Answered
A

1

5

In learning about NSInvocations it seems like I've got a gap in my understanding about memory management.

Here is a sample project:

@interface DoNothing : NSObject
@property (nonatomic, strong) NSInvocation *invocation;
@end

@implementation DoNothing
@synthesize invocation = _invocation;

NSString *path = @"/Volumes/Macintosh HD/Users/developer/Desktop/string.txt";

- (id)init
{
    self = [super init];
    if (self) {

        SEL selector = @selector(stringWithContentsOfFile:encoding:error:);
        NSInvocation *i = [NSInvocation invocationWithMethodSignature:[NSString methodSignatureForSelector:selector]];

        Class target = [NSString class];
        [i setTarget:target];
        [i setSelector:@selector(stringWithContentsOfFile:encoding:error:)];

        [i setArgument:&path atIndex:2];

        NSStringEncoding enc = NSASCIIStringEncoding;
        [i setArgument:&enc atIndex:3];

        __autoreleasing NSError *error;
        __autoreleasing NSError **errorPointer = &error;
        [i setArgument:&errorPointer atIndex:4];

        // I understand that I need to declare an *error in order to make sure
        // that **errorPointer points to valid memory. But, I am fuzzy on the
        // __autoreleasing aspect. Using __strong doesn't prevent a crasher.

        [self setInvocation:i];
    }

    return self;
}

@end

Of course, all I'm doing here is building up an invocation object as a property for the NSString class method

+[NSString stringWithContentsOfFile:(NSString \*)path encoding:(NSStringEncoding)enc error:(NSError \**)error]

It makes sense, especially after reading this blog post, as to why I need to handle the NSError object by declaring and assigning the address to **errorPointer. What is a little difficult to grasp is the __autoreleasing and memory management what is happening here.

The **errorPointer variable isn't an object, so it does not have a retain count. It simply is memory that stores a memory address which points to an NSError object. I understand that the stringWith... method will alloc, init, and autorelease an NSError object, and set the *errorPointer = the allocated memory. As you'll see later, the NSError object becomes inaccessible. Is this...

  • ...because an autorelease pool has drained?
  • ...because ARC filled in the "release" call to stringWith...'s alloc + init?

So let's take a look at how the invocation "works"

int main(int argc, const char * argv[])
{
    @autoreleasepool {

        NSError *regularError = nil;
        NSString *aReturn = [NSString stringWithContentsOfFile:path
                                                      encoding:NSASCIIStringEncoding
                                                         error:&regularError];

        NSLog(@"%@", aReturn);

        DoNothing *thing = [[DoNothing alloc] init];
        NSInvocation *invocation = [thing invocation];

        [invocation invoke];

        __strong NSError **getErrorPointer;
        [invocation getArgument:&getErrorPointer atIndex:4];
        __strong NSError *getError = *getErrorPointer;  // CRASH! EXC_BAD_ACCESS

        // It doesn't really matter what kind of attribute I set on the NSError
        // variables; it crashes. This leads me to believe that the NSError
        // object that is pointed to is being deallocated (and inspecting with
        // NSZombies on, confirms this).

        NSString *bReturn;
        [invocation getReturnValue:&bReturn];
    }
    return 0;
}

This has been eye opening (a bit disconcerting) for me, as I thought I knew what the hell I was doing when it came to memory management!

The best I could do to resolve my crasher, is to pull the NSError *error variable out from the init method, and make it global. This required me to change the attribute from __autoreleasing to __strong on **errorPointer. But, clearly that fix is less than ideal, especially given that one is likely to reuse NSInvocations many times in an operation queue. It also only kinda confirms my suspicion that *error is being dealloc'd.

As a final WTF, I tried played around a bit with the __bridge casts, but 1. I'm not sure if that's what I need here and 2. after permuting I couldn't find one that worked.

I'd love some insight that might help me better understand why this all isn't clicking.

Allanallana answered 3/4, 2012 at 22:46 Comment(0)
R
7

This is actually a very simple error that has nothing to do with Automatic Reference Counting.

In -[DoNothing init], you're initializing the error parameter of the invocation with a pointer to a stack variable:

__autoreleasing NSError *error;
__autoreleasing NSError **errorPointer = &error;
[i setArgument:&errorPointer atIndex:4];

And in main, you're grabbing that same pointer and dereferencing it:

__strong NSError **getErrorPointer;
[invocation getArgument:&getErrorPointer atIndex:4];
__strong NSError *getError = *getErrorPointer;

But of course by this point all the local variables that live in -[DoNothing init] no longer exist, and attempting to read from one produces a crash.

Reisfield answered 3/4, 2012 at 23:34 Comment(4)
Gotcha! so really, I should have declared the NSError *error as __strong static NSError *error (making it a heap variable? is that even reasonable to say? I thought stack / heap was an implementation detail...)Allanallana
Aah, no, don't make it static. Just don't make the NSInvocation a public part of your class. The bug only arises 'cause you can access it after it ceases to be useful. If you must, make the error variable a property of your class. (Incidentally, stack vs. heap isn't an implementation detail, they're fundamental object lifetime concepts.)Reisfield
The error argument is only useful after the invocation has fired. You're likely to stick an invocation on a timer or in an operation queue, so the error variable is almost certain to be out of scope. But, the general gist as to why it is crashing is now crystal clear - thank you!Allanallana
@Allanallana Ah, well, if this whole object is a wrapper around an NSInvocation, it makes sense for an error variable to run around with it, and that'd solve the lifetime issue. I revoke my disapproval.Reisfield

© 2022 - 2024 — McMap. All rights reserved.