转发C中可变参数函数的调用
在C语言中,是否可以转发可变参数函数的调用?就像
int my_printf(char *fmt, ...) { fprintf(stderr, "Calling printf with fmt %s", fmt); return SOMEHOW_INVOKE_LIBC_PRINTF; }
在这种情况下,显然不必严格按照上述方式转发调用(因为我们可以通过其他方式记录调用,或者使用vfprintf),但是我正在使用的代码库要求包装程序执行一些实际的工作,并且没有(也没有增加)类似于vfprintf的辅助函数。
[更新:根据到目前为止提供的答案,似乎有些困惑。用另一种方式来表达这个问题:通常,我们可以在不修改函数定义的情况下包装任意可变参数函数吗?
解决方案
使用vfprintf:
int my_printf(char *fmt, ...) { va_list va; int ret; va_start(va, fmt); ret = vfprintf(stderr, fmt, va); va_end(va); return ret; }
几乎,使用<stdarg.h>
中提供的功能:
#include <stdarg.h> int my_printf(char *format, ...) { va_list args; va_start(args, format); int r = vprintf(format, args); va_end(args); return r; }
注意,我们将需要使用vprintf版本而不是普通的printf版本。在这种情况下,没有一种方法可以不使用va_list
来直接调用可变参数函数。
无法转发此类函数调用,因为可以检索原始堆栈元素的唯一位置是在" my_print()"中。包装这样的调用的通常方法是具有两个函数,一个仅将参数转换为各种varargs
结构,而另一个实际上对这些结构进行操作。使用这样的双重功能模型,我们可以(例如)通过用va_start()
初始化my_printf()
中的结构,然后将它们传递给vfprintf()
,来包装printf()
。
如果我们没有类似于vfprintf的函数,而该函数却需要一个va_list而不是可变数量的参数,那么我们就不能这样做。请参阅http://c-faq.com/varargs/handoff.html。
例子:
void myfun(const char *fmt, va_list argp) { vfprintf(stderr, fmt, argp); }
可变参数函数与varargs
风格的替代函数成对出现,这不是直接的,但是很普遍(在标准库中,我们几乎会普遍找到这种情况)。例如printf
/vprintf
v ...函数采用va_list参数,通常通过特定于编译器的"宏魔术"来实现该参数,但是可以确保从像这样的可变参数函数中调用v ... style函数可以正常工作:
#include <stdarg.h> int m_printf(char *fmt, ...) { int ret; /* Declare a va_list type variable */ va_list myargs; /* Initialise the va_list variable with the ... after fmt */ va_start(myargs, fmt); /* Forward the '...' to vprintf */ ret = vprintf(fmt, myargs); /* Clean up the va_list */ va_end(myargs); return ret; }
这应该给我们想要的效果。
如果我们正在考虑编写可变参数的库函数,则还应考虑使va_list样式的同伴作为库的一部分可用。正如我们从问题中看到的那样,它可以证明对用户有用。
C99支持带有可变参数的宏;根据编译器,我们也许可以声明一个可以执行所需操作的宏:
#define my_printf(format, ...) \ do { \ fprintf(stderr, "Calling printf with fmt %s\n", format); \ some_other_variadac_function(format, ##__VA_ARGS__); \ } while(0)
通常,最好的解决方案是使用要包装的函数的va_list形式(如果存在的话)。
抱歉,话题不在话下,但:
元问题是,C语言中的varargs接口从一开始就已经从根本上被破坏了。这是对缓冲区溢出和无效内存访问的邀请,因为如果没有显式的结束信号(没有人真正出于懒惰而使用),则无法找到参数列表的末尾。而且,它始终依赖于深奥的实现特定宏,而重要的va_copy()宏仅在某些体系结构上受支持。
是的,我们可以执行此操作,但是它有点难看,我们必须知道最大数量的参数。此外,如果我们所处的体系结构没有像x86那样在堆栈上传递参数(例如PowerPC),则必须知道是否使用了"特殊"类型(双精度,浮点型,altivec等),以及是否因此,请相应地处理它们。可能很快会很痛苦,但是如果我们使用的是x86,或者原始功能的边界明确且受限制,则可以正常使用。
仍然会是一个hack,将其用于调试目的。不要围绕此构建软件。
无论如何,这是x86上的一个有效示例:
#include <stdio.h> #include <stdarg.h> int old_variadic_function(int n, ...) { va_list args; int i = 0; va_start(args, n); if(i++<n) printf("arg %d is 0x%x\n", i, va_arg(args, int)); if(i++<n) printf("arg %d is %g\n", i, va_arg(args, double)); if(i++<n) printf("arg %d is %g\n", i, va_arg(args, double)); va_end(args); return n; } int old_variadic_function_wrapper(int n, ...) { va_list args; int a1; int a2; int a3; int a4; int a5; int a6; int a7; int a8; /* Do some work, possibly with another va_list to access arguments */ /* Work done */ va_start(args, n); a1 = va_arg(args, int); a2 = va_arg(args, int); a3 = va_arg(args, int); a4 = va_arg(args, int); a5 = va_arg(args, int); a6 = va_arg(args, int); a7 = va_arg(args, int); va_end(args); return old_variadic_function(n, a1, a2, a3, a4, a5, a6, a7, a8); } int main(void) { printf("Call 1: 1, 0x123\n"); old_variadic_function(1, 0x123); printf("Call 2: 2, 0x456, 1.234\n"); old_variadic_function(2, 0x456, 1.234); printf("Call 3: 3, 0x456, 4.456, 7.789\n"); old_variadic_function(3, 0x456, 4.456, 7.789); printf("Wrapped call 1: 1, 0x123\n"); old_variadic_function_wrapper(1, 0x123); printf("Wrapped call 2: 2, 0x456, 1.234\n"); old_variadic_function_wrapper(2, 0x456, 1.234); printf("Wrapped call 3: 3, 0x456, 4.456, 7.789\n"); old_variadic_function_wrapper(3, 0x456, 4.456, 7.789); return 0; }
由于某些原因,我们不能将浮点数与va_arg一起使用,gcc表示它们会转换为double值,但程序会崩溃。仅此一点就表明该解决方案是黑客,并且没有通用解决方案。
在我的示例中,我假设参数的最大数量为8,但是我们可以增加该数量。包装函数也仅使用整数,但它与其他"常规"参数的工作方式相同,因为它们始终转换为整数。目标函数将知道它们的类型,但中间包装器不需要。包装程序也不需要知道正确数量的参数,因为目标函数也可以知道。
为了进行有用的工作(除了仅记录呼叫),我们可能必须同时了解两者。