...
由于 varargs 的实际实现方式以及 C 语言的限制,如果没有-taking函数来调用,则不可能将 args 传递到调用链va_list
中,除非您:
- 使用适用于您的代码可能运行的每个平台的汇编语言
- 了解编译器如何实现
va_list
等的详细信息,或
- 尝试编写一个函数,以某种方式计算参数类型的每种可能组合并手动传递它们。
在这些选项中,(3) 在任何实际情况下显然是不切实际的,(2) 可能随时更改,恕不另行通知。这给我们留下了(1),用于您的代码运行的每个平台的汇编语言。
在内部,可变参数以特定于 ABI 的方式为每个架构实现。从概念上讲,...
“我将传递我想要的所有参数,就好像我正在调用一个接受这些参数的函数一样,由你决定从哪里获取每个参数。” 让我们以在堆栈上传递其所有参数的架构为例,例如i386
在 OS X 和 iOS 模拟器上。
给定以下函数原型和调用:
void f(const char * const format, ...);
/* ... */
f("lUf", 0L, 1ULL, 1.0);
编译器将生成以下程序集(由我编写;真正的编译器可能会产生一些不同的调用序列,但效果相同):
leal L_str, %eax
pushl %eax
movl $0x3f800000, %eax
pushl %eax
movl $0x00000000, %eax
pushl %eax
movl $0x00000001, %eax
pushl %eax
movl $0x00000000, %eax
pushl %eax
call _f
这样做的效果是将每个参数以相反的顺序压入堆栈。这是秘诀:如果这样声明,编译器也会做同样的事情f()
:
void f(const char * const format, long arg1, unsigned long long arg2, float arg3);
这意味着如果您的函数可以复制堆栈的参数区域并调用 vararg-taking 函数,则 args 将简单地通过。问题:没有通用的方法来计算这个参数区域有多大!On i386
,在具有帧指针的函数中,该函数也从具有帧指针的函数调用,您可以作弊和复制rbp - *rbp
字节,但这效率低下并且不适用于所有情况(尤其是带struct
参数或返回struct
s 的函数)。
然后你有像armv6
and这样的架构armv7
,其中大多数参数在必须小心保存的x86_64
寄存器中传递,其中参数在寄存器中传递并且xmm
寄存器计数被传递%al
,并且ppc
,其中堆栈位置和寄存器都映射到参数!
在不使用 a 的情况下转发参数的唯一防弹方法va_list
是使用每个架构的程序集在代码中重新实现整个架构 ABI 逻辑,就像编译器一样。
这也是本质上objc_msgSend()
解决的相同问题。
“所以等等!” 你现在说。“我为什么不能直接打电话objc_msgSend
而不是这样乱组装?!”
回答:因为你无法告诉编译器,“不要破坏堆栈上的任何东西,也不要清除任何你看不到我使用的寄存器”。您仍然必须编写一个汇编例程,将调用转发到超类实现 -在您的子类实现中执行任何工作之前- 然后返回到您的,同时注意相同的事情objc_msgSend()
,例如需要_stret
和_fpret
变体和在至少三种架构上实现(armv7
, i386
, x86_64
- 并且取决于您对向后和向前兼容性的需要,也可能ppc
是 , ppc64
,armv6
和armv7s
)。
对于普通的可变参数,编译器在创建va_list
. C 不能直接访问任何此类信息。并且objc_msgSend()
是 Objective-C 编译器和运行时再次重做这一切,因此您可以编写方法调用而无需va_list
一直使用。(此外,在某些架构上,能够将参数传递给已知调用列表比使用可变参数约定更有效)。
因此,不幸的是,如果不付出比可能值得付出更多的努力,您就无法做到这一点。类实现者,让这成为你的一个教训 -每当你提供一个接受可变参数的方法时,也提供一个接受 ava_list
代替...
的相同方法的版本。NSString
是一个很好的例子,使用initWithFormat:
and initWithFormat:arguments:
。