【问题标题】:How to call an implementation with variable number of arguments?如何调用具有可变数量参数的实现?
【发布时间】:2012-09-03 15:20:44
【问题描述】:

为了简化,假设我有一个这样的函数

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

}

在那个方法中,我想在 _self 的超类上调用实现(imp)。 我可以使用以下代码访问该 IMP:

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

现在,我该怎么称呼那个小鬼?

【问题讨论】:

  • 您是否希望superimp 也有类型签名void (id, SEL, id, ...)?还有,你怎么称呼myFunc
  • @lhunath 这个问题一点都不清楚,它没有minimal reproducible exampleidSELidIMP 是什么。 In that method什么方法?
  • @Stargateur idSELIMP 是 Objective-C 运行时的标准部分。它们不需要在问题中解释。

标签: objective-c objective-c-runtime


【解决方案1】:

只需使用可变参数调用它:

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

IMP 已经是typedef'd as

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

所以你可以毫无问题地使用可变参数调用它。

【讨论】:

  • 是的,但问题是:我不知道该函数需要多少个参数!参数在 myFunc 的可变参数列表中。
  • @iSofTom 那么你有点坑了。您可以尝试将 builtin_apply 与 GCC 一起使用,但如果您使用的是 clang,那么您将陷入更大的困境。
  • 这也是我的想法……我其实在看libffi,还是有希望的。
  • 您还可以查看 Apple 的运行时源代码。他们已经解决了这个问题,使用汇编程序为每个支持的架构来处理参数,因为它高度依赖于架构,你必须在哪里寻找它们。我自己也用过 libffi。
  • @TiloPrütz 他们在恢复堆栈帧后通过调用jmp 来解决它。这里的问题是,在我们通过引入更多变量来搞乱所有寄存器之前,我们不知道在这种情况下跳转到它的地址。
【解决方案2】:

所以,经过大量的工作,我自己终于想通了。

问题

现实情况是不可能将可变参数传递给 C 可变参数函数。为此,您确实需要 API 提供v 风格的函数,例如vprintf。现在,实际上有一个objc_msgSendv,但它已被弃用。此外,没有objc_msgSendSuperv。这意味着低级 API 对我们来说是完全禁止的。如果您在编译时知道确切的消息签名,则此 API有用。

唯一的其他选择是使用NSInvocation 机制并构建消息,而不是直接调用实现。一个简单的解决方案相当简单,但正确执行此操作有很多注意事项。

我们需要解决一些问题:

  1. 传递不同类型的参数(读取大小)。我们将使用va_arg 将我们的参数复制到我们的NSInvocation,但是这个宏依赖于我们将参数的类型传递给它,以便它确定要复制和推进的字节大小。
  2. 调用一个被另一个覆盖(读取隐藏)的实现。此外,由于我们现在只是发送消息,因此无法选择类的确切实现。本质上,我们只能做objc_msgSend,不能做objc_msgSendSuper。无法直接调用被另一个方法覆盖的方法。
  3. 正在检索返回值。我们的新 IMP 函数需要有一个与我们要替换的函数的返回类型相匹配的返回类型。 OP 使用了 void 返回值,但情况肯定并非总是如此。进一步的困难是这个返回值必须在编译时指定,因为它是一个 C 函数,而不是运行时。

解决方案

我实际上已经在此处提供的代码中解决了上述问题:

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

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

为了解决这些问题,我做了以下工作:

  1. 对于每个参数,我们查看方法的签名,该签名对参数类型进行编码。我们确定参数类型的字节大小,并执行一系列if 来检查参数大小是否等于已知大小,这允许我们执行编译时固定值va_arg那个尺寸。这解决了大小等于一系列“支持”大小的类型参数的问题。在我的实现中,我支持任何类型的字节大小1(例如char)、2(例如short)、4(例如int)、8(例如id)、16 (例如 CGPoint)、32(例如 CGRect)、48(例如 CGAffineTransform)(参见- (void)setArguments:(va_list)args)。
  2. 要调用被另一个方法覆盖的方法的实现,我们可以使用class_getInstanceMethodmethod_getImplementation 查找隐藏的实现,然后我们可以将该实现安装到对象类的新临时代理方法中使用class_addMethod。如果我们随后调用代理方法而不是目标方法,运行时将调用我们正在寻找的隐藏实现,实际上与msgSendSuper 相同。但是,这会导致我们使用错误的_cmd 调用实现,因此我们要做的是将隐藏的实现分配给正确方法名称的顶层。不过,这样做会覆盖我们方法的覆盖代码,因此我们将其搁置一会,在调用后将其恢复(参见- (void)invokeWithTarget:(id)target superclass:(Class)type)。
  3. 为了创建一个 C 函数,其返回类型的大小适合我们任意 IMP 的返回值,我们再次查看方法签名,这一次,确定返回值的大小。我们执行另一系列条件来检查已知大小的大小,对于每个已知大小,我们创建一个 C 函数,将该大小硬编码为函数的返回值。我已经创建了对尺寸返回值的支持为了方便这一点,我们使用imp_implementationWithBlock。 (见PearlForwardIMP

【讨论】:

  • 太棒了!这里有太多新东西,需要学习。谢谢你的精彩回答:)
【解决方案3】:

实际上,我无论如何都找不到使用可变数量的参数调用IMP,但我有另一种解决方案。

通过使用NSInvocation,我们可以解决这个问题。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-01-07
    • 2011-02-13
    • 2011-12-24
    • 1970-01-01
    • 2011-01-21
    • 2014-11-14
    • 2020-01-01
    • 2018-07-20
    相关资源
    最近更新 更多