4

I'm trying to receive parameters in runtime from some random method that is invoked on my class. Before arm64 (on armv7 and armv7s) it can be done with following code:

@interface MyClass
// It does not matter what method, we declare it for compiler only
- (id)methodWithFirstParameter:(id)firstParam secondParameter:(id)secondParam;
@end

@implementation MyClass

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    [self addDynamicCallForSelector:sel];
    return YES;
}

+ (void)addDynamicCallForSelector:(const SEL)selector {
    const char *encoding;
    IMP implementation;
    implementation = [self instanceMethodForSelector:@selector(dynamicMethod:)];
    Method newMethod = class_getInstanceMethod([self class], @selector(dynamicMethod:));
    encoding = method_getTypeEncoding(newMethod);
    class_addMethod([self class], selector, implementation, encoding);
}

- (id)dynamicMethod:(id)obj1, ... {
    int parameterCount = [[NSStringFromSelector(_cmd) componentsSeparatedByString:@":"] count] - 1;
    NSMutableArray *parameterList = [[NSMutableArray alloc] initWithCapacity:parameterCount];
    va_list arguments;
    va_start(arguments, obj1);
    for (int i = 0; i < parameterCount; i++) {
        id parameter = (i == 0) ? obj1 : va_arg(arguments, id);
        if (!parameter) {
            parameter = [NSNull null];
        }
        [parameterList addObject:parameter];
    }
    va_end(arguments);
    return parameterList;
}

It's pretty easy and clean. We just pass all incoming invocation to one single implementation that can gather parameters from it and return them.

In arm64 however, va_list works good, but in such context, the first parameter from va_arg(arguments, id) is current instance of class (self). After second call it's stopped with EXC_BAD_ACCESS. So I think it did not even find first parameter (with va_start(arguments, obj1)).

Also notice that va_list functionality works fine on arm64 in case I invoke dynamicMethod: directly (and manually set number of arguments). My wild guess that it does not work because of wrong method encoding (it does not magically convert one method into another with different number of parameters on arm64 like it was before).

You can look all code here, it's basically web service part of this solution.

Ossir
  • 3,109
  • 1
  • 34
  • 52

3 Answers3

7

The reason your code is failing is likely because the calling convention between arm (32bit) and arm64 is different. That is to say, different rules are being applied as to how parameters are passed to the function, and how values are returned.

There was no "magic conversion" going on before. You got lucky that the calling convention for variadic functions was the same as for non-variadic - at least in your use cases.

See the Parameter Passing sections in both the ARM Procedure Call Standard for arm64 and ARM Procedure Call Standard (non-64 bit).

Good luck solving this; you'll likely have to have two separate code paths.

EDIT

I believe the "correct" way to achieve what you're after is to implement a number of functions with all of the possible permutations of arguments you expect to handle, and resolve to those dynamically based on the selector signature. JSCocoa does this using what they call a "Burks Pool" (I believe named for Tim Burks)

Also check out libffi for iOS: https://github.com/roupam/Objective-C-NuREPL-for-iOS/tree/master/Remote/libffi

Lastly, a related post: -[NSInvocation getReturnValue:] with double value produces 0 unexpectedly

Community
  • 1
  • 1
TomSwift
  • 39,369
  • 12
  • 121
  • 149
  • Thanks for the answer, I'm okay with adding separate code path for `arm64` (it will generate `fat binary` anyway). I will check all the links and write here what path I choose. – Ossir Apr 11 '14 at 07:00
  • arm64 uses the stack to pass all variadic parameters (on Apple platforms) so this is exactly what is happening... the original selector is causing arguments to be put in registers and so on, while va_start is expecting them on the stack. – russbishop Apr 11 '14 at 09:30
  • Despite the fact your answer does not lead me to solution(majority of links, that you provided, I've read before posting question on SO), I want to reward you for your research, thank you! – Ossir Apr 13 '14 at 12:37
  • Thanks. I think the "Burks Pool" is the way to go. You should be able to map a given selector signature to one of n methods that you implement based on the signature itself. – TomSwift Apr 13 '14 at 15:00
  • @TomSwift I know it should work faster, but it looks like sh.. . We don't have restrictions for methods, so let's say I implement all methods till 10 parameters and some one eventually write method with 11 parameters and get error. – Ossir Apr 14 '14 at 07:13
4

Unexpectedly, I've got decent solution from PR on Github so all credits go to @sandor-gazdag. Here's solution:

- (void)forwardInvocation:(NSInvocation *)inv {
    NSUInteger n = [[inv methodSignature] numberOfArguments];

    NSMutableArray *parameterList = [[NSMutableArray alloc] init];
    for (NSUInteger i = 0; i < n - 2; i++) {
        id __unsafe_unretained arg;
        [inv getArgument:&arg atIndex:(int)(i + 2)];
        if (!arg) {
            arg = [NSNull null];
        }
        [parameterList addObject:arg];
    }
    [self dynamicWebServiceCallWithArguments:parameterList forInvocation:inv];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSUInteger numArgs = [[NSStringFromSelector(aSelector) componentsSeparatedByString:@":"] count] - 1;
    return [NSMethodSignature signatureWithObjCTypes:[[@"@@:@" stringByPaddingToLength:numArgs + 3 withString:@"@" startingAtIndex:0] UTF8String]];
}

- (void)dynamicWebServiceCallWithArguments:(NSMutableArray *)parameterList forInvocation:(NSInvocation *)invocation {
   ... 
   id result = [self executeDynamicInstanceMethodForSelector:invocation.selector parameters:parameterList prepareToLoadBlock:prepareToLoadBlock success:successBlock failure:failureBlock];
   [invocation setReturnValue:&result];
}

So simple and still so powerful. Works for any processor architecture, because it's high-level solution. I blame myself that I've not find it myself=)

Ossir
  • 3,109
  • 1
  • 34
  • 52
0

A found another way of dynamically invoking of a function. Have a look at this piece of code:

- (void)requestSucceeded 
{
    NSLog(@"requestSucceeded");
    id owner = [fbDelegate class];
    SEL selector = NSSelectorFromString(@"OnFBSuccess");
    NSMethodSignature *sig = [owner instanceMethodSignatureForSelector:selector];
    _callback = [NSInvocation invocationWithMethodSignature:sig];
    [_callback setTarget:owner];
    [_callback setSelector:selector];
    [_callback retain];       // <------ See the partial doc attached

    [_callback invokeWithTarget:fbDelegate];
}

A part from NSInvocation document:

This class does not retain the arguments for the contained invocation by default. If those objects might disappear between the time you create your instance of NSInvocation and the time you use it, you should explicitly retain the objects yourself or invoke the retainArguments method to have the invocation object retain them itself.

Also, the arguments you passes would get indices 2 or greater, see why: (same above link)

Indices 0 and 1 indicate the hidden arguments self and _cmd, respectively; these values can be retrieved directly with the target and selector methods. Use indices 2 and greater for the arguments normally passed in a message.

For a completed running exmaple, if have already added a facebook invite friend code in this thread.

This code dynamically calls the delegate function by reading it from the delegate's interface and invoked with/without arguments.

The idea behind this solution is that it does not required any external reference of Encoding, plus you should retain your _callback to avoid EXC_BAD_ACCESS and yes it is a bit simpler.

Hope it helps!

Community
  • 1
  • 1
NeverHopeless
  • 11,077
  • 4
  • 35
  • 56
  • NeverHopeless, I can't figure out how I can use this code(I checked out linked answer too). The main reason, I can't change the way how it works right now for end user. I can only change what `resolveInstanceMethod:` method does. This method should attach proper method to class for gathering all parameters and selector that was invoked. If you did not get something, feel free to ask any question. Also take a look at web service call snippet in README.md from the link in the question, it should help you understand how it works now. – Ossir Apr 07 '14 at 11:52