Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Perform Selector Casting

There's a really strange behavior going on with float / double / CGFloat casting on the result of performSelector:

Why does this work?

BOOL property = (BOOL)[self.object performSelector:@selector(boolProperty)];
NSInteger property = (NSInteger) [self.object performSelector:@selector(integerProperty)];

And this doesn't

CGFloat property = (CGFloat) [self.object performSelector:@selector(floatProperty)];

At first I tried to do this:

CGFloat property = [[self.object performSelector:@selector(floatProperty)] floatValue];

But I ended up getting an EXC_BAD_ACCESS runtime error. I already figured out a hack to solve this issue but I would like to understand why it works with Integer and Bool and not with floating point types.

My hack:

@implementation NSObject (AddOn)
-(CGFloat)performFloatSelector:(SEL)aSelector
{
  NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[self class] instanceMethodSignatureForSelector:aSelector]];
  [invocation setSelector:aSelector];
  [invocation setTarget:self];
  [invocation invoke];
  CGFloat f = 0.0f;
  [invocation getReturnValue:&f];
  return f;
}
@end

Then:

CGFloat property = [self.object performFloatSelector:@selector(floatProperty)];
like image 970
Julian J. Tejera Avatar asked May 28 '14 03:05

Julian J. Tejera


2 Answers

The performSelector method only supports methods that return nothing or an object. This is covered in its documentation where it states:

For methods that return anything other than an object, use NSInvocation.

Your "hack" is just the correct way of calling a selector which returns a non-object.

The reason why it appears to work for integer and boolean methods is to do with the way values are returned by methods. The return type of performSelector is id, an object pointer type. Integer-like values; which includes NSInteger, BOOL and object pointers; are often returned in a general purpose register, however floating point values are usually returned in a floating point register. The compiler will always load the result from the register used for returning id values and then perform any action required by the cast. For integer and boolean values the loaded value is probably correct and the cast is a no-op in those cases, so everything (appears to) work. For floating point values the loaded value is incorrect - it is not the value the called method has returned as that is in a different register.

Note calling non-object, non-void, selectors with performSelector can cause memory issues under ARC - the compiler will usually warn if this might be the case.

The NSInvocation way of calling a selector works for non-object return types as one of its arguments is the method signature itself. Using the signature NSInvocation is able to determine the type of the return value and how it is returned, and therefore correctly return it to the caller.

HTH

like image 115
CRD Avatar answered Oct 28 '22 12:10

CRD


If you don't want to use NSInvocation, you can use the objc_msgSend functions directly (this is what the compiler translates message calls to):

BOOL (*f)(id, SEL) = (BOOL (*)(id, SEL))objc_msgSend;
BOOL property = f(self.object, @selector(boolProperty));

NSInteger (*f)(id, SEL) = (NSInteger (*)(id, SEL))objc_msgSend;
NSInteger property = f(self.object, @selector(integerProperty));

CGFloat (*f)(id, SEL) = (CGFloat (*)(id, SEL))objc_msgSend;
CGFloat property = f(self.object, @selector(floatProperty));

Note that you must cast it to a function pointer with the exact signature of the method to call it correctly; here the methods take no parameters, so the function only has the 2 "hidden" parameters, self, and _cmd. If the methods took parameters, you would have to add more parameters to the function pointer type.

Also, note that for struct returns, you will need to use objc_msgSend_stret in place of objc_msgSend, but everything else exactly the same as above.

like image 40
newacct Avatar answered Oct 28 '22 10:10

newacct