printf() has been called from every shell, every language, every terminal you've ever opened.
This is what happens when I stop treating it like magic.
📜 Context • 🧩 Modules • 🏗️ Architecture • 🧪 Testing • 💻 Usage
"printf() looks like one function. Underneath, it's a dispatcher pretending to be simple."
ft_printf picks up where libft's fixed-parameter functions stop. Every call can carry a different number of arguments, of different types, in an order only the format string knows — and it's the parser's job to make sense of that at runtime, one % at a time.
The mission: rebuild printf from the format string up — with malloc, free, write, and the va_ macros as the only libc functions on the table. Every digit converted, every hex value formatted, every pointer address printed in hexadecimal: built from raw arithmetic, because there's nothing else to borrow it from.
Two phases, two different problems to solve:
- Specifier Resolution (Mandatory): Parsing the format string and handling the nine standard conversion specifiers — the baseline every
printfcall assumes works. - Flag & Precision Handling (Bonus): Turning the parser into a state machine that handles field width, precision, and layout flags (
-0. # +) without breaking on conflicting combinations.
Click on each module below to view conversion catalogs and technical specifications.
🔹 Module I — Specifier Resolution (Mandatory)
Nine specifiers, one dispatcher — no format string breaks the loop.
Each conversion specifier extracts its argument from the variadic list via va_arg and passes it to a dedicated rendering handler — enforcing single-responsibility design and preventing cross-type side effects.
Detection order is critical: while %i and %d share output behavior, they represent distinct specifiers in the POSIX formatting standard. Furthermore, %% requires no argument extraction — misinterpreting it would advance the variadic pointer into undefined memory.
📂 View Specifier Catalog
| Specifier | Technical Description | Expected Data Type |
|---|---|---|
%c |
Prints a single ASCII character. | int (promoted from char) |
%s |
Prints a null-terminated string. | char * |
%p |
Prints a memory address (pointer) in hexadecimal format. | void * |
%d |
Prints a signed decimal (base 10) integer. | int |
%i |
Prints a signed integer (same behavior as %d in this context). |
int |
%u |
Prints an unsigned decimal (base 10) integer. | unsigned int |
%x |
Prints an unsigned number in lowercase hexadecimal (base 16). | unsigned int |
%X |
Prints an unsigned number in uppercase hexadecimal (base 16). | unsigned int |
%% |
Prints a literal percent sign. | None |
🔸 Module II — Flag & Precision Handling (Bonus)
🚧 Currently in development — and by 42's own rules, it can't even be graded until the mandatory build is flawless.
Field width and precision look like formatting details. They're actually where the state machine either holds or breaks.
While mandatory conversion resolves baseline data types, this module transitions the parser into a robust finite-state machine capable of resolving intricate format combinations.
Current engineering focus:
- 🛠️ Flag parsing: Buffering modifier flags (
-,0,.,#,+,) prior to specifier resolution. - 📏 Field width & precision: Dynamically computing padding, truncation, and zero-fill boundaries.
- 🧠 Flag precedence & conflict resolution: Gracefully handling conflicting flags (e.g.,
-overriding0) according to POSIX standards.
Full flag routing tables land once this module compiles clean.
The project separates string parsing from data conversion on purpose — the parser never needs to know how a hex number gets built, and the conversion functions never need to know where in the string they were called from.
- Parse: Scans the format string character by character.
- Detect: Intercepts
%tokens and dispatches to the specifier resolver. - Extract & Render: Retrieves the variadic argument via
va_argand formats the representation. - Output & Count: Writes buffered bytes to
stdoutwhile tracking total character count for the return value.
ft_printf/
├── inc/
│ └── ft_printf.h # Single header, every prototype declared once
├── src/
│ ├── ft_printf.c # Entry point and parsing loop
│ ├── ft_check_specifier.c
│ ├── ft_c.c # One file per specifier — no exceptions
│ ├── ft_s.c
│ ├── ft_p.c
│ ├── ft_d.c
│ ├── ft_i.c
│ ├── ft_u.c
│ ├── ft_x.c
│ └── ft_x_upper.c
├── config/
│ ├── colors.mk # Same ANSI Makefile styling as libft
│ └── banners.mk
├── tests/
└── Makefile
- One specifier per file: Each conversion handler (
ft_c.c,ft_d.c,ft_x.c, etc.) is isolated in its own compilation unit, adhering to the Single Responsibility Principle and keeping codebase modifications localized. - Decoupled detection and rendering:
ft_check_specifier.cpurely routes conversion tokens without performing formatting, ensuring parsing logic remains modular and independently testable. - Unified tooling with libft: Shared
config/colors.mkandconfig/banners.mkestablish a consistent build pipeline and visual interface across projects. - Strict return-value contract: Accurately matching
printfreturn values is just as critical as displaying characters. Each handler tracks byte emissions to guarantee exact return-value parity. - Native archive generation: Built strictly using
ar rcsrather than high-level tooling, adhering to standard static library construction.
A
printfclone that "looks right" isn't the same as one that returns the right byte count.
Beyond Norminette compliance, this project ships with a dedicated test suite validating output against the real printf — same input, same format string, byte-for-byte comparison.
tests/
└── test_printf.c # Validates every mandatory specifier against libc's printf
🚧 Bonus flag coverage isn't in the suite yet — it lands alongside Module II.
make testThe suite prints color-coded (ANSI) pass/fail output per specifier, checking output and return value against the real printf side by side.
- Static Analysis: Validated by Norminette (42's internal linter).
- Compiler Flags: Compiled strictly with
-Wall -Wextra -Werror.
Make sure you have a C compiler (gcc or clang) and make installed, then clone the repository:
git clone https://github.com/joolibar/ft_printf.git
cd ft_printfCompile the library:
makeCompiling generates libftprintf.a. Include the header in your .c files:
#include "ft_printf.h"
int main(void)
{
ft_printf("System boot: %s\n", "OK");
ft_printf("Memory address: %p\n", &main);
return (0);
}Link the archive when compiling your project, pointing the compiler to inc/:
gcc -Wall -Wextra -Werror your_file.c -Ift_printf/inc -Lft_printf -lftprintf -o your_program"Variadic parsing behind a simple interface —
dispatching types one format specifier at a time." 🖨️✨
Crafted by joolibar
Creative Developer building digital experiences at 42
© 2026 joolibar • Validated by Moulinette 🤖, crafted by Humans 🧠.