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)— readssizeof(type)bytes at the cursor position, interprets them astype, 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 asfree: you opened it withva_start, you close it withva_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_testsum_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.