my_printf
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:
| Format | Type | Note |
|---|---|---|
%d %i | int | Signed |
%u | unsigned int | Unsigned |
%x %X | int β hex | lower/UPPER |
%o | int β octal | Base 8 |
%c | char | Single char |
%s %S | char* | String |
%p | void* | Memory address |
%f %F | double | Floating point |
%e %E | double | Scientific notation |
%a | double | Hex float |
%b | int | Binary (extension) |
%n | int* | 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β