How do varargs work in C?

When I was taught C, functions had a fixed number of arguments. Here’s an example:

int sum_squares(int a, int b) {
  return a*a + b*b;
}

Function arguments are placed on the stack. So executing sum_squares(2,3) means placing 2 and 3 (amongst some other things) on the stack. Putting these two values on the stack means advancing the stack pointer enough for two int values, placing our two values in the free space.

How does something like printf("Some values: %d, %s, %c!", 4, "foo", 'z') fit into this scheme?

To my shame, I always assumed printf was implemented through macro magic. It’s not! It’s an ordinary function definition, and you can define it yourself. Here’s its declaration:

void my_printf(char* format, ...);

What is this ...? It’s a special token which marks the function as variadic, meaning that call sites can pass an arbitrary list of values following the named parameters.

The function definition follows the declaration syntax:

void my_printf(char* format, ...)
{
  // ...
}

How do we access the extra arguments passed in the call? With three “special macros” in stdarg.h. First up is va_start (read: variable-arguments start):

void my_printf(char* format, ...)
{
  va_list argp;
  va_start(argp, format);
  // ...
}

What is this va_list? It’s effectively a pointer to an arguments in the var-args array. After calling va_start, argp points at the first var-argument.

The second macro is va_arg. You call it with a va_list and a type, and it takes value pointed at by the va_list as a value of the given type, then increment the pointer by the size of that pointer. For example, va_arg(argp, int) will return (int) *argp, and increment the pointer, so argp += sizeof int.

How do we know when we reach the end of the var-arguments array? Simple: we don’t! Or rather, this API doesn’t tell us, so we have to know in some other way. In the case of printf, it assumes there are at least as many var-arguments as there are format specifications (e.g. %s) in the format string.

At the point that we’ve stopped consuming arguments, we must call va_end(argp). This does nothing in GNU C Lib, but the ISO C standard requires us to call it.

#include <stdarg.h>

void my_printf(char* format, ...)
{
  va_list argp;
  va_start(argp, format);
  while (*format != '\0') {
    if (*format == '%') {
      format++;
      if (*format == '%') {
        putchar('%');
      } else if (*format == 'c') {
        char char_to_print = va_arg(argp, int);
        putchar(char_to_print);
      } else {
        fputs("Not implemented", stdout);
      }
    } else {
      putchar(*format);
    }
    format++;
  }
  va_end(argp);
}

Notice something odd: char char_to_print = va_arg(argp, int). We use int as the type in the macro because types narrower than int are promoted to be of size int.


I wrote this because I felt like it. This post is my own, and not associated with my employer.

Jim. Friends. Vidrio.