I fixed a rather nasty bug today in AspectObjectiveC. One particular unit
test would crash with EXC_BAD_ACCESS
every time. After learning far more
about registers and ABIs than I ever wanted to know (thanks, Greg Parker),
it dawned on me that performSelector:
was corrupting memory. It was
particularly hard to track down because the crash would happen a couple of
lines after the call to performSelector:
, when the corrupted memory was
actually accessed.
I’ve never had a problem with performSelector:
before, but this time I was
using it a little differently. The return value of the selector was an
NSRect
.
Now for the gory explanation.
Why Returning an NSRect Caused the Memory Corruption
Normally on Intel architectures, the return value for a function is stored in
the register eax
. This is what obj_msgSend
handles.
There is an exception to this rule when the function returns a sufficiently
large struct, such as an NSRect
. In such cases, the function is passed a
secret extra argument: a pointer to some memory that will hold the return
value. The Objective-C runtime has a different message dispatch function for
these exceptional cases: objc_msgSend_stret
(“stret” is short for
“struct return”). Therein lies the problem.
The documentation for performSelector:
states:
For methods that return anything other than an object, use
NSInvocation
.
Apple aren’t kidding when they mention this. This is because performSelector:
uses objc_msgSend
. If you use objc_msgSend
to call a method that requires
objc_msgSend_stret
, the function will think the first parameter is the secret
struct pointer, when in fact it’s the self
pointer. When the function
returns, it corrupts the memory pointed to by self
by overwriting it with the
return value. Next time you try to use the object, you get weird and wonderful
crashes.
The Moral of the Story
Only use performSelector:
when the selector returns an object. If the
selector returns a struct, then you risk corrupting memory, even if you don’t
use the return value. If the method doesn’t return an object, then use
NSInvocation instead, because it is capable of determining the correct message
dispatch function to use.
Wrap Yourself In Cotton Wool
For those of you who are overly cautious, here’s a method that will check the
return type before calling performSelector:
@interface NSObject(SafePerformSelector)
-(id) performSelectorSafely:(SEL)aSelector;
@end
@implementation NSObject(SafePerformSelector)
-(id) performSelectorSafely:(SEL)aSelector;
{
NSParameterAssert(aSelector != NULL);
NSParameterAssert([self respondsToSelector:aSelector]);
NSMethodSignature* methodSig = [self methodSignatureForSelector:aSelector];
if(methodSig == nil)
return nil;
const char* retType = [methodSig methodReturnType];
if(strcmp(retType, @encode(id)) == 0 || strcmp(retType, @encode(void)) == 0){
return [self performSelector:aSelector];
} else {
NSLog(@"-[%@ performSelector:@selector(%@)] shouldn't be used. The selector doesn't return an object or void", [self className], NSStringFromSelector(aSelector));
return nil;
}
}
@end