thecodingidiot.com

The VoiceVariadic Arguments

Variadic Arguments

printf takes any number of arguments of different types. The function signature does not enumerate them — it cannot. The C language handles this through variadic functions[1]: functions that accept a variable argument list declared with ....

The mechanism

Four macros in <stdarg.h> manage the list:

  • va_list — a struct that holds a cursor into the argument list. You never look inside it — the layout is implementation-defined — but it is just a struct holding a position in memory plus platform bookkeeping.
  • va_start(args, last) — fills in the struct so the cursor points at the first variadic argument. No memory is allocated; it is setting up a pointer into the call stack that already exists.
  • va_arg(args, type) — reads sizeof(type) bytes at the cursor position, interprets them as type, and advances the cursor by that amount.
  • va_end(args) — closes the cursor. Required by the standard for portability — some architectures did allocate memory internally, so the rule is the same as free: you opened it with va_start, you close it with va_end. On x86-64 Linux it expands to nothing, but the call must still be there.

There is no stored type information in the struct — the cursor holds position, not type. The only source of type information is the format string. Asking va_arg for the wrong type reads the wrong bytes from the wrong position; the compiler cannot catch it and the result is undefined behaviour.

A concrete example first

Before layering variadic arguments onto format string parsing, write a simpler function: sum_ints, which adds count integers passed as variadic arguments.

Create sum_test.c:

#include <stdarg.h>
#include <stdio.h>
 
int     sum_ints(int count, ...)
{
    va_list  args;  /* holds the position in the argument list */
    int      total;
    int      i;
 
    va_start(args, count);  /* position list just past the last named param */
    total = 0;
    i = 0;
    while (i < count) {
        total += va_arg(args, int);  /* read next arg as int, advance position */
        i++;
    }
    va_end(args);   /* release any resources the va_list may hold */
    return (total);
}
 
int     main(void)
{
    printf("%d\n", sum_ints(3, 10, 20, 30));   /* 60 */
    printf("%d\n", sum_ints(1, 7));            /* 7  */
    printf("%d\n", sum_ints(0));               /* 0  */
    return (0);
}

Compile and run:

gcc -Wall -Wextra -g -std=c99 -o sum_test sum_test.c
./sum_test

sum_ints knows how many arguments to read because the caller passes count explicitly. printf uses the format string instead — it reads one argument per % specifier it encounters. The mechanism is the same; the bookkeeping differs.

The tci_printf prototype

tci_printf takes a format string as its first (and only named) parameter, followed by ...:

int     tci_printf(const char *fmt, ...);

va_start(args, fmt) positions the argument list just past fmt. Each call to va_arg inside the function reads the next argument from the list. The format string tells the function which type to use for each read.

Run man 3 stdarg — the manual documents va_copy as well, which is needed if you ever pass a va_list into a function that must traverse it twice. tci_printf does not need it.

Passing va_list to helpers

The format string dispatcher will call a helper for each specifier. That helper needs access to the argument list. There are two options: pass the va_list by value, or pass a pointer to it.

Pass a pointer. When you pass va_list by value, the copy advances independently of the original — the caller's list position does not update. A va_list * passes by reference: the helper advances the list and the caller sees the new position.

static int  dispatch(char spec, va_list *args)
{
    /* calls va_arg(*args, ...) — advances the caller's list */
}

The next page builds the output primitives before any specifier logic is added.

Footnotes

  1. Variadic function - Wikipedia