42 Common Core β€” ft_printf: Variadic Functions and Format Parsing
42 Common Core β€” ft_printf: Variadic Functions and Format Parsing

42 Common Core β€” ft_printf: Variadic Functions and Format Parsing

Systems & Low-Level Deep dive CLinux
Created on:
Team Size: 1
Tool Used: C/GCC/Make/stdarg

Part of the project CodeTester β€” An Interactive C Tester for 42 Projects

Part 2 of 3 in 42 Common Core
  1. 42 Common Core β€” Libft: Rebuilding the C Standard Library
  2. 42 Common Core β€” ft_printf: Variadic Functions and Format Parsing
  3. 42 Common Core β€” get_next_line: Reading a File One Line at a Time

ft_printf asks you to recreate printf: the conversions %c %s %p %d %i %u %x %X %%, and in the bonus part the flags - 0 . # space + and a field width. It must return the number of characters printed, like the original, and it is packaged as libftprintf.a on top of libft.

The project is mostly an exercise in two things: variadic functions and parsing.

Variadic functions in C

A function declared with ... reads its extra arguments through a va_list:

/* condensed: the Norm forbids the ternary, the original uses an if */
int ft_printf(const char *format, ...)
{
    t_data data;

    ft_initialize_data(&data, format);
    va_start(data.args, format);
    while (*data.str && !data.error)
    {
        if (*data.str == '%')
            ft_handle_conversion(&data);
        else
            ft_put_char(&data, *data.str++);
    }
    va_end(data.args);
    return (data.error ? -1 : data.char_count);
}

Two rules to remember:

  • va_arg needs the promoted type: a char is passed as an int, so %c reads va_arg(args, int).
  • Nothing tells you how many arguments there are. The format string is the only contract, which is why a wrong format is undefined behaviour.

One state struct

Instead of passing half a dozen parameters around, everything lives in one struct: the cursor in the format string, the va_list, the running character count, an error flag (a failed write makes ft_printf return -1), and the parsed format of the current conversion.

typedef struct s_format
{
    int  minus, zero, hash, space, plus;
    int  width;
    int  precision;     /* -1 means "no precision given" */
    char specifier;
}   t_format;

Using -1 for β€œno precision” matters: %.0d with the value 0 prints nothing, while %d prints 0.

Parsing a conversion

The parser reads the pieces of a conversion in the order the C standard defines them: flags, width, precision, specifier. It also applies the precedence rules once, so the printers never have to:

parse_flags(data);                      /* any of "-0# +" */
data->format.width = parse_number(data);
if (*data->str == '.')
{
    data->str++;
    data->format.precision = parse_number(data);
}
if (data->format.minus)
    data->format.zero = 0;              /* '-' overrides '0' */
if (data->format.plus)
    data->format.space = 0;             /* '+' overrides ' ' */

A dispatcher then reads the argument with the right type and calls the matching printer.

One layout function for every number

%d, %u, %x, %X and %p all produce the same shape:

[spaces] [prefix] [zeros] [digits] [spaces]

The prefix is the sign (-, + or space) or 0x / 0X. Precision adds leading zeros; width adds padding, with spaces on the left by default, zeros after the prefix with 0, or spaces on the right with -. Once the digits are converted into a small buffer, a single function lays everything out:

zeros = precision - dlen;               /* zeros required by the precision */
pad   = width - prefix_len - zeros - dlen;
if (!minus && !zero) put_pad(' ', pad);
put(prefix);
if (zero)            put_pad('0', pad);
put_pad('0', zeros);
put(digits);
if (minus)           put_pad(' ', pad);

One more rule: when a precision is given, the 0 flag is ignored for numbers. With this in place, %-8.3x, %+05d and %#x all reuse the same code instead of each conversion reimplementing padding.

Platform differences

printf isn’t identical everywhere. A NULL pointer printed with %p gives (nil) with glibc and 0x0 on macOS, so the header detects the platform with __linux__ and matches the local libc.

How CodeTester checks it

The CodeTester printf suites never hard-code expected output. Every case calls snprintf with the same arguments and compares:

  • the exact bytes written to stdout (captured by redirecting the file descriptor);
  • the return value.

Suites cover each conversion, every flag pair (-0, #, space, +, .), mixed combinations, and INT_MIN, UINT_MAX and NULL values. Behaviour the subject leaves undefined, such as unknown specifiers or contradictory flags, sits in an informational category that reports differences from the real printf without counting them as failures.

Result: 125/125 (mandatory and bonus).

ft_printf on GitHub
© 2026 Samuel Styles