- [NSInvocation getReturnValue:] с двойным значением производит 0 неожиданно

Я пытаюсь вызвать метод, который возвращает double с помощью NSInvocation. Но я обнаружил, что он не работает в 64-битных приложениях iOS. Он работает на OS X, в симуляторе - и 32-битном, и 64-битном - iPad 2, и iPad Air с 32-битной сборкой. Эта проблема связана только с 64-разрядной версией устройства iPad Air.

Это код для демонстрации проблемы:

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);
}

ожидаемый выход

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

Выход для 64-битной сборки на 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

Это также происходит при значении float.

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

Ответы

Ответ 1

Согласовано с @David H, которое в этом случае разбивается на NSInvocation или, возможно, на метод NSString doubleValue. Однако я смог заставить его работать.

Мне кажется, что NSInvocation нарушается из-за проблемы/несоответствия вызова. Как правило, параметры и возвращаемые значения для методов objective-c передаются в регистры. (objc_msgSend знает, как выполнить этот тип вызова.) Но если параметр или возвращаемое значение является структурой или типом, который не вписывается в регистр, они передаются в стек. (objc_msgSend_stret выполняет этот тип вызова.) NSInvocation обычно использует подпись метода, чтобы иметь возможность решить, нужно ли ему звонить objc_msgSend или objc_msgSendStret. Я предполагаю, что теперь он также должен знать, на какой платформе он работает, и вот где ошибка.

Я немного поиграл с вашим кодом, и кажется, что на arm64 двойное возвращаемое значение передается как структура, но NSInvocation рассматривает его как переданный в регистр. Я понятия не имею, какая из сторон правильная. (Я знаю, что достаточно только в этой области, чтобы быть опасным. Мои пальцы пересекаются с кем-то с более низкоуровневыми отбивными, чем я, и читаю это, и дает лучшее объяснение!)

Тем не менее, мне кажется, что есть значительные изменения в том, как параметры и результаты передаются в руке (32 бит) против arm64. См. Разделы Возврат результата в ARM Procedure Call Standard для arm64 и Стандарт обработки вызовов ARM (не 64 бит).

Мне удалось заставить NSInvocation обрабатывать вызов как возвращающий структуру, содержащую double, и это заставило ее работать должным образом. Для этого я подделал сигнатуру метода поддельной подписи метода, возвращающего структуру. Я поместил это в категорию NSString, но он мог жить где угодно.

Не зная, что конкретно нарушено, или что произойдет, когда оно будет исправлено, я вряд ли отправил бы код с этим "исправлением". Я найду другое решение.

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);
    }
}

Ответ 2

У вас нет надежды заставить это работать в настоящее время. Я пробовал много вариантов вашего кода и тестировал на iPhone 5S и новейший симулятор в режиме 32 и 64 бит, но это должно быть ошибкой с arm64, поскольку 64-битный симулятор работает просто отлично.

Итак, сначала я изменил свой код, чтобы попробовать всевозможные варианты, и получается, что с помощью floatValue также не работает. Поскольку размер поплавка является общим, это уменьшает количество переменных между различными пробными платформами.

Кроме того, я попытался использовать цель NSNumber, созданную с использованием интегрального метода и метода float: метод float фактически приводит к сбою! Я пробовал другие параметры, такие как сохранение строки и установка настройки сохранения вызова, без изменений.

Я ввел ошибку об этом: 15441447: NSInvocation fails only on arm64 devices - любой, кто ее беспокоит, может его обмануть. Я также загрузил демонстрационный проект.

Загруженный мной код:

- (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);
    }
}

Ответ 3

Исправлено на основе ответа @TomSwift.

    - (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:@"%[email protected]:", 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;
    }