How to call an implementation with variable number of arguments?
Asked Answered
S

3

6

To simplify, let's say I have a function like that

void myFunc(id _self, SEL _cmd, id first, ...)
{

}

In that method I wanna call the implementation(imp) on the superclass of _self. I can reach that IMP with this code:

Class class = object_getClass(_self);
Class superclass = class_getSuperClass(class);
IMP superimp = class_getMethodImplementation(superclass, _cmd);

now, how can I do to call that imp ?

Somber answered 3/9, 2012 at 15:20 Comment(3)
Do you expect superimp to also have type signature void (id, SEL, id, ...)? Also, how do you call myFunc?Karilla
@lhunath This question is not clear at all, it don't have a minimal reproducible example, What is id, SEL, id or IMP. In that method what method ?Sexist
@Sexist id, SEL, and IMP are standard parts of the Objective-C runtime. They don't need to be explained in the question.Karilla
C
3

Just call it using variable arguments:

superImp(self, _cmd, argument1, argument2, argument3, etc...)

IMP is already typedef'd as

typedef id (*IMP)(id, SEL, ...);

So you can call it with variable arguments with no issue.

Conclusion answered 3/9, 2012 at 15:34 Comment(5)
Yes, but the problem is: I don't know how many arguments that function takes! the arguments are in the variadic list of myFunc.Somber
@Somber then you are in a bit of a hole. You could try using builtin_apply with GCC, but if you are using clang, then you are in an even bigger hole.Conclusion
It's my thought too... I'm actually looking over libffi, still an hope.Somber
You may also look at Apples runtime source code. They have solved the problem using assembler for every supported architecture to handle the arguments because it's highly architecture dependent where you have to look for them. I for myself used libffi, too.Lelandleler
@TiloPrütz they solve it by doing a jmp call after restoring the stack frame. The issue here is that we don't know the address to jump to it in this scenario until we've messed up all of the registers, by introducing more variables.Conclusion
E
2

So, after a lot of work, actually figured this out myself.

The problem

The reality is that it's impossible to pass varargs to a C vararg function. In order to do this, you really need the API to provide a v-style function, such as vprintf. Now, there actually IS a objc_msgSendv, but it's deprecated. Also, there is no objc_msgSendSuperv. That means the low-level API is completely off-limits to us. This API is only useful if you know the exact message signature at compile time.

The only other option is to use the NSInvocation machinery and build a message rather than invoking implementations directly. A naive solution is fairly trivial but there are many caveats in doing this properly.

There are a few things we need to solve:

  1. Passing arguments of different types (read sizes). We'll copy our arguments to our NSInvocation using va_arg, but this macro depends on us passing it the type of the argument in order for it to determine the size of the bytes to copy and advance.
  2. Invoking an implementation that is overridden (read hidden) by another. Also, since we're now just sending messages, there is no way to select exact implementation of the class. In essence, we can only do objc_msgSend, not objc_msgSendSuper. There is no way to directly invoke a method that was overridden by another.
  3. Retrieving the return value. Our new IMP function needs to have a return type that matches the return type of the function we're replacing. The OP used a void return value, but that's certainly not always the case. Further difficulty is that this return value must be specified at compile time since it's a C function, not runtime.

The Solution

I've actually solved the above problems in code available here:

https://github.com/Lyndir/Pearl/blob/master/Pearl/NSInvocation%2BPearl.h

https://github.com/Lyndir/Pearl/blob/master/Pearl/NSInvocation%2BPearl.m

To address these issues, I've done the following:

  1. For each argument, we look at the method's signature, which encodes the argument type. We determine the byte size of the argument type, and do a series of if's to check if the argument size is equal to that of a known size, which allows us to perform a compile-time fixed value va_arg for that size. This resolves the issue for arguments of types with a size equal to a series of "supported" sizes. In my implementation, I support any type of byte size 1 (eg. char), 2 (eg. short), 4 (eg. int), 8 (eg. id), 16 (eg. CGPoint), 32 (eg. CGRect), 48 (eg. CGAffineTransform) (See - (void)setArguments:(va_list)args).
  2. To invoke the implementation of a method that was overridden by another method, we can look up the hidden implementation using class_getInstanceMethod and method_getImplementation, and then we can install that implementation into a new temporary proxy method on the object's class using class_addMethod. If we then invoke the proxy method instead of the target method, the runtime will invoke the hidden implementation we were looking for, effectively doing the same as a msgSendSuper. However, this would lead to us invoking the implementation with a bad _cmd, so what we do instead is assign the hidden implementation to the top level at the correct method name. Doing so would overwrite our method's override code, though, so we set that aside for a moment and after the invocation, we restore it (See - (void)invokeWithTarget:(id)target superclass:(Class)type).
  3. In order to create a C function with a return type that has the right size to fit our arbitrary IMP's return value, we again look at the method signature to, this time, figure out the size of our return value. We do another series of conditionals to check the size against known sizes and for each known size, we create a C function with that size hardcoded as the function's return value. I've created support for return values of sizes To facilitate this, we use imp_implementationWithBlock. (See PearlForwardIMP)
Epidaurus answered 20/11, 2017 at 15:44 Comment(1)
Great! There are too many new things and need to learn here. Thank you for your awesome answer :)Nuclei
N
1

Actually, i can't find anyway to call IMP with variable number of arguments, but i have another solution for this situation.

By using NSInvocation, we can resolve this problem.

void myFunc(id _self, SEL _cmdS, id first, ...) {
  Class clazz = object_getClass(_self);
  Class superClass = class_getSuperclass(clazz);

  NSMethodSignature *signature = [superClass methodSignatureForSelector:_cmdS];

  if (!signature) {
    return;
  }

  NSInvocation *inv = [NSInvocation invocationWithMethodSignature:signature];
  [inv setSelector:_cmdS];
  [inv setTarget:superClass];

  va_list args;
  va_start(args, first);
  id arg = first;
  // Arguments 0 and 1 are self and _cmd respectively, automatically set 
  // by NSInvocation. So start setting arguments from index 2
  for (int i = 2; i < signature.numberOfArguments; i++) {
    [inv setArgument:&arg atIndex:i];
    if (i < signature.numberOfArguments - 1) {
      arg = va_arg(args, id);
    }
  }
  va_end(args);

  [inv invoke];
}
Nuclei answered 18/11, 2017 at 19:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.