转发C中可变参数函数的调用

时间:2020-03-06 14:53:43  来源:igfitidea点击:

在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,但是我们可以增加该数量。包装函数也仅使用整数,但它与其他"常规"参数的工作方式相同,因为它们始终转换为整数。目标函数将知道它们的类型,但中间包装器不需要。包装程序也不需要知道正确数量的参数,因为目标函数也可以知道。
为了进行有用的工作(除了仅记录呼叫),我们可能必须同时了解两者。