[本文最初发布于 1989 年 6 月 4 日。为便于网页浏览,我稍作修改。]
来自: scs@adam.pika.mit.edu (Steve Summit)
Newsgroups: comp.unix.wizards,comp.lang.c
主题: Re: 需求:一个(可移植的)设置参数栈的方法
关键词: 1/varargs, callg
Message-ID: <11830@bloom-beacon.MIT.EDU>
日期: 4 Jun 89 16:43:52 GMT
参考: <708@mitisft.Convergent.COM> <32208@apple.Apple.COM> <10354@smoke.BRL.MIL>
在文章 <708@mitisft.Convergent.COM> 中 Gregory Kemnitz 撰写道:
> 我想知道 *NIX(System V.3)是否有能力让
> 在调用函数之前为其设置一个参数栈。我
> 有数百个函数指针,它们都从同一个地方被调用,
> 并且每个函数都有不同数量的参数。
这是一个不错的问题。Doug Gwyn 的建议对于最大程度的可移植性是正确的,但它限制了被调用子例程的形式,以及任何不通过“逆向可变参数机制”进行的调用。(也就是说,你不真正地调用这些子例程,就无法建立那个小参数向量。)
为了透明性(但会牺牲一些可移植性),我使用了一个名为“callg”的例程,它以 VAX 指令的同名命名。(这等同于 Peter Desnoyers 的“va_call”例程;回想起来,我更喜欢他的名字。)
va_call在 VAX 上,它可以在一行汇编语言中实现;在其他机器上,它通常需要十到二十行,将参数从向量复制到实际的栈(或者参数实际传递的地方)。我为 PDP11、NS32000、68000 和 80x86 准备了实现。(这是一个特定于机器的问题,而不是特定于操作系统的。)一个像va_call 必须 用汇编语言编写;它是我知道的少数几个不可能用 C 编写的函数之一。
并非所有机器都使用栈;有些使用寄存器传递或其他约定。因此,为了最大程度的可移植性,像va_call这样的例程的接口应该允许显式指定每个参数的类型,并隐藏参数向量构造的细节。我一直在考虑一个类似于以下示例所示的接口:
#include "varargs2.h" extern printf(); main() { va_stack(stack, 10); /* declare vector which holds up to 10 args */ va_push(stack, "%d %f %s\n", char *); va_push(stack, 12, int); va_push(stack, 3.14, double); va_push(stack, "Hello, world!", char *); va_call(printf, stack); }
请注意,这会调用标准的printf; printf不需要采取任何特殊预防措施,事实上,它甚至无法知道它没有被正常调用。(这就是我所说的“透明性”。)
在“传统的”基于栈的机器上,va_stack会声明一个包含 10 个int的数组(假设int是该机器的自然字大小),并且va_push会通过指针操作将字复制到其中,这类似于当前va_arg宏在varargs和stdarg实现中的操作。(因此,“声明一个可以容纳最多 10 个参数的向量”的说法是具有误导性的;该向量最多容纳 10 个字,程序员需要留出足够的空间用于多字类型,如long和double。当有人建议提供一个va_nargs()宏时,“字”和“参数”之间的区别就会浮现;我们不必再讨论这个问题了。)
对于寄存器传递的机器,寄存器的选择可能取决于参数的类型。因此,接口必须允许类型信息保留在参数向量中供va_call例程检查。如果使用符号常量而不是 C 类型名,这将更容易实现
va_push(stack, 12, VA_INT); va_push(stack, 3.14, VA_DOUBLE); va_push(stack, "Hello, world!", VA_POINTER);
由于在va_push宏中“switch”这些常量来决定要为向量预留多少字会很棘手,独立的 push 宏可能更可取
va_push_int(stack, 12); va_push_double(stack, 3.14); va_push_pointer(stack, "Hello, world!");
(此选项比单一的va_push具有额外的优势,因为它不需要第二个宏参数是可变类型。)然而,这里仍然有一个主要困难,那就是不能假设存在一种通用的指针。
对于“最糟糕”的机器,C 类型名的完全通用性(如第一个示例所示)可能才是必需的。不幸的是,要用类型名做所有您想做的事情,您可能不得不在编译器中特别处理它们。(另一方面,那些在处理va_push时会有困难的机器,很可能也是那些已经需要编译器识别varargs或stdarg机制的机器。)
以免您认为va_call如果可以实现的话,就能解决所有问题,别太快松气:返回值应该是什么?在最普遍的情况下,被间接调用的例程可能会返回不同的类型。而va_call的返回值则不能,可以说,以封闭形式表示。
最后一个难题(除了可变参数传递之外还有可变返回类型)是 C 解释器允许解释代码和编译代码混合使用的核心。我知道我是如何解决的;我很想知道 Saber C 是如何解决的。(我是通过另外两个汇编语言例程解决的,同样无法用 C 编写。一个更好的解决方案,至少是问题的一半,是为va_call提供第三个参数,一个某种类型的联合指针,用于存储返回值。)
我刚刚匆忙完成了第一个示例的实现,我已附上供您参考和娱乐,只要您有 VAX 即可。
Steve Summit