I was migrating a block of code to automatic reference counting (ARC), and had the ARC migrator throw the error
NSInvocation\'s setArgument is not sa
An NSInvocation
by default does not retain or copy given arguments for efficiency, so each object passed as argument must still live when the invocation is invoked. That means the pointers passed to -setArgument:atIndex:
are handled as __unsafe_unretained
.
The two lines of MRR code you posted got away with this: testNumber1
was never released. That would have lead to a memory leak, but would have worked. In ARC though, testNumber1
will be released anywhere between its last use and the end of the block in which it is defined, so it will be deallocated. By migrating to ARC, the code may crash, so the ARC migration tool prevents you from migrating:
NSInvocation's setArgument is not safe to be used with an object with ownership other than __unsafe_unretained
Simply passing the pointer as __unsafe_unretained won't fix the problem, you have to make sure that the argument is still around when the invocation gets called. One way to do this is call -retainArguments
as you did (or even better: directly after creating the NSInvocation
). Then the invocation retains all its arguments, and so it keeps everything needed for being invoked around. That may be not as efficient, but it's definitely preferable to a crash ;)
This is a complete guess, but might it be something to do with the argument being passed in by reference as a void*
?
In the case you've mentioned, this doesn't really seem a problem, but if you were to call, eg. getArgument:atIndex:
then the compiler wouldn't have any way of knowing whether the returned argument needed to be retained.
From NSInvocation.h:
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
Given that the compiler doesn't know whether the method will return by reference or not (these two method declarations have identical types and attributes), perhaps the migrator is being (sensibly) cautious and telling you to avoid void pointers to strong pointers?
Eg:
NSDecimalNumber* val;
[anInvocation getArgument:&val atIndex:2];
anInvocation = nil;
NSLog(@"%@", val); // kaboom!
__unsafe_unretained NSDecimalNumber* tempVal;
[anInvocation getArgument:&tempVal atIndex:2];
NSDecimalNumber* val = tempVal;
anInvocation = nil;
NSLog(@"%@", val); // fine
Throwing in my complete guess here.
This is likely directly related to retainArguments
existing at all on the invocation. In general all methods describe how they will handle any arguments sent to them with annotations directly in the parameter. That can't work in the NSInvocation
case because the runtime doesn't know what the invocation will do with the parameter. ARC's purpose is to do its best to guarantee no leaks, without these annotations it is on the programmer to verify there isn't a leak. By forcing you to use __unsafe_unretained
its forcing you to do this.
I would chalk this up to one of the quirks with ARC (others include some things not supporting weak references).
According to Apple Doc NSInvocation:
This class does not retain the arguments for the contained invocation by default. If those objects might disappear between the time you create your instance of NSInvocation and the time you use it, you should explicitly retain the objects yourself or invoke the retainArguments method to have the invocation object retain them itself.
Why is it preventing you from doing this? It seems just as bad to use __unsafe_unretained objects as arguments.
The error message could be improved but the migrator is not saying that __unsafe_unretained
objects are safe to be used with NSInvocation
(there's nothing safe with __unsafe_unretained
, it is in the name). The purpose of the error is to get your attention that passing strong/weak objects to that API is not safe, your code can blow up at runtime, and you should check the code to make sure it won't.
By using __unsafe_unretained
you are basically introducing explicit unsafe points in your code where you are taking control and responsibility of what happens. It is good hygiene to make these unsafe points visible in the code when dealing with NSInvocation
, instead of being under the illusion that ARC will correctly handle things with that API.
The important thing here is the standard behaviour of NSInvocation: By default, arguments are not retained and C string arguments are not being copied. Therefore under ARC your code can behave as follows:
// Creating the testNumber
NSDecimalNumber *testNumber1 = [[NSDecimalNumber alloc] initWithString:@"1.0"];
// Set the number as argument
[theInvocation setArgument:&testNumber1 atIndex:2];
// At this point ARC can/will deallocate testNumber1,
// since NSInvocation does not retain the argument
// and we don't reference testNumber1 anymore
// Calling the retainArguments method happens too late.
[theInvocation retainArguments];
// This will most likely result in a bad access since the invocation references an invalid pointer or nil
[theInvocation invoke];
Therefore the migrator tells you: At this point you have to explicitly ensure that your object is being retained long enough. Therefore create an unsafe_unretained variable (where you have to keep in mind that ARC won't manage it for you).