Perform Selector Casting
Asked Answered
W

2

5

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)];
Watteau answered 28/5, 2014 at 3:2 Comment(5)
The first ones "work?" performSelector is supposed to return an id... perhaps casting id as a long is safer than casting it as a float. See the NSObject protocol reference. I still wonder why the protocol is separate from the class, but whatever.Cockneyfy
@Cockneyfy They work and I've got unit tests to prove it. The accepted answer explains why it works.Watteau
@JulianJ.Tejera - the answer wholly justifies the airquotes around "work!"Cockneyfy
casting id to BOOL and NSInteger should not be allowed in ARC. Is this MRC then?Casta
I'm using ARC and Xcode didn't complain.Watteau
I
6

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

Isaacson answered 28/5, 2014 at 3:45 Comment(1)
I guess I have to read a little bit more about registers. I suppressed the compiler warnings with #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks"Watteau
C
3

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.

Casta answered 28/5, 2014 at 22:50 Comment(1)
It's really interesting to learn new ways of doing things but I think I'll stick to the NSInvocation solution because the syntax is objective-c like, and cleaner (at least to me). Thanks!Watteau

© 2022 - 2024 — McMap. All rights reserved.