Question

I am trying to call a method that returns a double using NSInvocation. But I found that it does not working in 64 bit iOS apps. It works on on OS X, in the simulator -- both 32-bit and 64 bit -- iPad 2, and iPad Air with a 32-bit build. Only the 64-bit build on an iPad Air device has this problem.

This is the code to demo the problem:

NSMethodSignature *signature = [NSString instanceMethodSignatureForSelector:@selector(doubleValue)];
for (int i = 0; i < 10; i++) {
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    NSString *str = [NSString stringWithFormat:@"%d", i];
    [invocation setTarget:str];
    [invocation setSelector:@selector(doubleValue)];
    [invocation invoke];
    union {
        double d;
        uint64_t u;
    } d;
    [invocation getReturnValue:&d.d];
    NSLog(@"%lf, %llx", d.d, d.u);
}

expected output

2013-11-09 22:34:18.645 test[49075:907] 0.000000, 0
2013-11-09 22:34:18.647 test[49075:907] 1.000000, 3ff0000000000000
2013-11-09 22:34:18.648 test[49075:907] 2.000000, 4000000000000000
2013-11-09 22:34:18.649 test[49075:907] 3.000000, 4008000000000000
2013-11-09 22:34:18.650 test[49075:907] 4.000000, 4010000000000000
2013-11-09 22:34:18.651 test[49075:907] 5.000000, 4014000000000000
2013-11-09 22:34:18.652 test[49075:907] 6.000000, 4018000000000000
2013-11-09 22:34:18.653 test[49075:907] 7.000000, 401c000000000000
2013-11-09 22:34:18.654 test[49075:907] 8.000000, 4020000000000000
2013-11-09 22:34:18.654 test[49075:907] 9.000000, 4022000000000000

Output for the 64-bit build on iPad Air

2013-11-09 22:33:55.846 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.847 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.848 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.848 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.849 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.849 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.850 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.850 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.851 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.851 test[998:60b] 0.000000, 18710a969

This also happen for float value.

2013-11-09 23:51:10.021 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.023 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.024 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.024 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.025 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.026 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.026 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.027 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.027 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.028 test[1074:60b] -0.000000, 80000000
Was it helpful?

Solution

Agreed with @David H that NSInvocation is broken in this case, or possibly the NSString doubleValue method. I was able to force it to work, however.

It appears to me that NSInvocation is broken due to a calling-convention issue / mismatch. Typically, parameters and return values for objective-c methods are passed in registers. (objc_msgSend knows how to perform this type of call.) But if a parameter or return value is a struct or type that doesn't fit in a register then they're passed on the stack. (objc_msgSend_stret performs this type of call.) NSInvocation typically uses the method signature to be able to decide whether it needs to call objc_msgSend or objc_msgSendStret. I'm guessing that now it also needs to know what platform it's operating on, and this is where the bug lies.

I played around with your code a bit and it appears that on arm64 the double return value is being passed as a structure would be, yet NSInvocation is treating it as being passed in a register. I have no idea which side is correct. (I know only enough in this area to be dangerous. My fingers are crossed that someone with more low-level chops than I comes along and reads this, and provides a better explanation!)

That said, it appears to me that there are significant changes in how parameters and results are passed in arm (32bit) vs. arm64. See the Result Return sections in both the ARM Procedure Call Standard for arm64 and ARM Procedure Call Standard (non-64 bit).

I was able to force NSInvocation to treat the call as returning a struct containing a double, and this made it work as expected. To do this I faked out the method signature to a fake signature of a method returning a struct. I placed this in a NSString category but it could live anywhere.

Not knowing what specifically is broken, or what will happen when it's fixed, I wouldn't likely ship code with this 'fix'. I'd find some other workaround.

typedef struct
{
    double d;

} doubleStruct;

@interface NSString (TS)
- (doubleStruct) ts_doubleStructValue;
@end
@implementation NSString (TS)
- (doubleStruct) ts_doubleStructValue
{
    doubleStruct ds;
    return ds;
}
@end


- (void) test
{
    NSMethodSignature *signature = [NSString instanceMethodSignatureForSelector: @selector( ts_doubleStructValue )];
    for (int i = 0; i < 10; i++) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        NSString *str = [NSString stringWithFormat:@"%d", i];
        [invocation setTarget:str];
        [invocation setSelector:@selector(doubleValue)];
        [invocation invoke];

        double d;
        [invocation getReturnValue: &d];

        NSLog(@"%lf", d);
    }
}

OTHER TIPS

You have no hope of getting this to work at the current time. I tried many variations of your code, and tested on an iPhone 5S and the latest Simulator in both 32 and 64 bit mode, but this must be a bug with arm64 since the 64bit simulator works just fine.

So first I modified your code to try all kinds of variations, and it turns out using floatValue fails as well. Since the size of a float is common, this reduces the number of variables between the various trial platforms.

Also, I tried to use a NSNumber target created using an integral and float method: the float method actually results in a crash! I tried other options like retaining the string and setting the invocation retain setting, no change.

I've entered a bug on this: 15441447: NSInvocation fails only on arm64 devices - anyone concerned about it can dup it. I uploaded a demo project as well.

The code I uploaded is:

- (void)test
{
    NSMethodSignature *signature = [NSString instanceMethodSignatureForSelector:@selector(floatValue)];
    for (int i = 0; i < 10; i++) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];

#if 0
        NSString *str = [NSString stringWithFormat:@"%d", i];
        [invocation setTarget:str]; // fails on iPhone 5s
#else
        //[invocation setTarget:[NSNumber numberWithFloat:(float)i]]; // crashes in 'invoke' on iPhone 5s, works fine in simulator
        [invocation setTarget:[NSNumber numberWithInteger:i]]; // fails on iphone 5S
#endif
        [invocation setSelector:@selector(floatValue)];

        [invocation invoke];

        float f;
        [invocation getReturnValue:&f];

        NSLog(@"%f", f);
    }
}

Fix based on @TomSwift answer.

    - (void)testInvocation
    {
        NSInvocation *invocation = [[self class] invocationWithObject:self selector:@selector(getAFloat)];

        [invocation setTarget:self];
        [invocation invoke];

        double d;
        [invocation getReturnValue: &d];
        NSLog(@"d == %f", d);

        return YES;
    }

    + (NSInvocation *)invocationWithObject:(id)object selector:(SEL)selector
    {
        NSMethodSignature *sig = [object methodSignatureForSelector:selector];
        if (!sig) {
            return nil;
        }

    #ifdef __LP64__
        BOOL isReturnDouble = (strcmp([sig methodReturnType], "d") == 0);
        BOOL isReturnFloat = (strcmp([sig methodReturnType], "f") == 0);

        if (isReturnDouble || isReturnFloat) {
            typedef struct {double d;} doubleStruct;
            typedef struct {float f;} floatStruct;

            NSMutableString *types = [NSMutableString stringWithFormat:@"%s@:", isReturnDouble ? @encode(doubleStruct) : @encode(floatStruct)];
            for (int i = 2; i < sig.numberOfArguments; i++) {
                const char *argType = [sig getArgumentTypeAtIndex:i];
                [types appendFormat:@"%s", argType];
            }

            sig = [NSMethodSignature signatureWithObjCTypes:[types UTF8String]];
        }
    #endif

        NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
        [inv setSelector:selector];
        return inv;
    }

Wow this is still apparently broken today and now in the simulator too (where I'm testing it). I got bit by this bug today, found my double arguments were always coming back zero in the selector of the invocation.

In my case it was easy to just send it as an NSNumber with @(myDouble) as the argument and then on the other side unwrap it with myNumber.doubleValue

Nasty.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top