← Back to projects

my_printf

πŸ“…
C

Rewriting printf. Not using it, rebuilding it from scratch.

This kind of project forces you to understand what’s really happening under the hood. How can a function accept a variable number of arguments? How does it know what to print when you throw "%d %s %x" at it?

How it works

printf is a variadic function. It takes a format string, then any number of arguments. The thing is, C doesn’t give you that for free - you have to handle everything with stdarg.h and its macros: va_list, va_start, va_arg, va_end.

Parsing the format string is where it gets interesting. You need to detect each %, identify the specifier, grab the right argument with the correct type, convert it to a string, and print it. Without segfaulting.

Supported specifiers

Everything standard, plus a few extras:

FormatTypeNote
%d %iintSigned
%uunsigned intUnsigned
%x %Xint β†’ hexlower/UPPER
%oint β†’ octalBase 8
%ccharSingle char
%s %Schar*String
%pvoid*Memory address
%f %FdoubleFloating point
%e %EdoubleScientific notation
%adoubleHex float
%bintBinary (extension)
%nint*Chars written
%%-Literal %

Flags too: -, +, , #, 0. Plus width and precision handling.

Structure

my_printf/
β”œβ”€β”€ my_printf.c      # Entry point, parsing
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ flags/       # Flag handling (-, +, 0, #, space)
β”‚   β”œβ”€β”€ specifiers/  # One file per specifier
β”‚   └── utils/       # Helper functions
β”œβ”€β”€ lib/my/          # Custom mini libc
β”œβ”€β”€ include/
β”‚   └── my.h
└── tests/           # Criterion tests

Each specifier has its own file. Modular, easy to debug, easy to extend.

Build & usage

git clone https://github.com/Akinator31/my_printf.git
cd my_printf
make
#include "my.h"

int main(void) {
    my_printf("Hello %s, you have %d unread messages.\n", "Pavel", 42);
    my_printf("Address: %p | Hex: %#x | Binary: %b\n", &main, 255, 255);
    return 0;
}

Tests

make tests

Written with Criterion. Compares behavior against the real printf to make sure outputs match.

Takeaways

  • Variadic functions are powerful but dangerous - no type checking at compile time
  • String parsing requires precision, one off-by-one error and everything blows up
  • Converting floats to strings is surprisingly complex (thanks IEEE 754)
  • Writing exhaustive tests is what separates β€œit works” from β€œit actually works”