SkillAgentSearch skills...

Nanoprintf

The smallest public printf implementation for its feature set.

Install / Use

/learn @charlesnicholson/Nanoprintf

README

nanoprintf

Presubmit Checks

nanoprintf is an unencumbered implementation of snprintf and vsnprintf for embedded systems that, when fully enabled, aim for C11 standard compliance. The primary exceptions are scientific notation (%e, %g), and locale conversions that require wcrtomb to exist. C23 binary integer output is optionally supported as per N2630. Safety extensions for snprintf and vsnprintf can be optionally configured to return trimmed or fully-empty strings on buffer overflow events.

Additionally, nanoprintf can be used to parse printf-style format strings to extract the various parameters and conversion specifiers, without doing any actual text formatting.

nanoprintf makes no memory allocations and uses less than 100 bytes of stack. It compiles to between ~610-3160 bytes of object code on a Cortex-M4 architecture, depending on configuration.

All code is written in a minimal dialect of C99 for maximal compiler compatibility, compiles cleanly at the highest warning levels on clang + gcc + msvc, raises no issues from UBsan or Asan, and is exhaustively tested on 32-bit and 64-bit architectures. nanoprintf does include C standard headers but only uses them for C99 types and argument lists; no calls are made into stdlib / libc, with the exception of any internal large integer arithmetic calls your compiler might emit. As usual, some Windows-specific headers are required if you're compiling natively for msvc.

nanoprintf is a single header file in the style of the stb libraries. The rest of the repository is tests and scaffolding and not required for use.

nanoprintf is statically configurable so users can find a balance between size, compiler requirements, and feature set. Floating-point conversion, "large" length modifiers, and size write-back are all configurable and are only compiled if explicitly requested, see Configuration for details.

Usage

Add the following code to one of your source files to compile the nanoprintf implementation:

// define your nanoprintf configuration macros here (see "Configuration" below)
#define NANOPRINTF_IMPLEMENTATION
#include "path/to/nanoprintf.h"

Then, in any file where you want to use nanoprintf, simply include the header and call the npf_ functions:

#include "nanoprintf.h"

void print_to_uart(void) {
  npf_pprintf(&my_uart_putc, NULL, "Hello %s%c %d %u %f\n", "worl", 'd', 1, 2, 3.f);
}

void print_to_buf(void *buf, unsigned len) {
  npf_snprintf(buf, len, "Hello %s", "world");
}

See the "Use nanoprintf directly" and "Wrap nanoprintf" examples for more details.

Motivation

I wanted a single-file public-domain drop-in printf that came in at under 1KB in the minimal configuration (bootloaders etc), and under 3KB with the floating-point bells and whistles enabled.

In firmware work, I generally want stdio's string formatting without the syscall or file descriptor layer requirements; they're almost never needed in tiny systems where you want to log into small buffers or emit directly to a bus. Also, many embedded stdio implementations are larger or slower than they need to be- this is important for bootloader work. If you don't need any of the syscalls or stdio bells + whistles, you can simply use nanoprintf and nosys.specs and slim down your build.

Philosophy

This code is optimized for size, not readability or structure. Unfortunately modularity and "cleanliness" (whatever that means) adds overhead at this small scale, so most of the functionality and logic is pushed together into npf_vpprintf. This is not what normal embedded systems code should look like; it's #ifdef soup and hard to make sense of, and I apologize if you have to spelunk around in the implementation. Hopefully the various tests will serve as guide rails if you hack around in it.

Alternately, perhaps you're a significantly better programmer than I! In that case, please help me make this code smaller and cleaner without making the footprint larger, or nudge me in the right direction. :)

API

nanoprintf has 4 main functions:

  • npf_snprintf: Use like snprintf.
  • npf_vsnprintf: Use like vsnprintf (va_list support).
  • npf_pprintf: Use like printf with a per-character write callback (semihosting, UART, etc).
  • npf_vpprintf: Use like npf_pprintf but takes a va_list.

The pprintf variations take a callback that receives the character to print and a user-provided context pointer.

Pass NULL or nullptr to npf_[v]snprintf to write nothing, and only return the length of the formatted string.

nanoprintf does not provide printf or putchar itself; those are seen as system-level services and nanoprintf is a utility library. nanoprintf is hopefully a good building block for rolling your own printf, though.

Return Values

The nanoprintf functions all return the same value: the number of characters that were either sent to the callback (for npf_pprintf) or the number of characters that would have been written to the buffer provided sufficient space. The null-terminator 0 byte is not part of the count.

The C Standard allows for the printf functions to return negative values in case string or character encodings can not be performed, or if the output stream encounters EOF. Since nanoprintf is oblivious to OS resources like files, and does not support the l length modifier for wchar_t support, any runtime errors are either internal bugs (please report!) or incorrect usage. Because of this, nanoprintf only returns non-negative values representing how many bytes the formatted string contains (again, minus the null-terminator byte).

Configuration

Features

nanoprintf has the following static configuration flags.

  • NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS: Set to 0 or 1. Enables field width specifiers.
  • NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS: Set to 0 or 1. Enables precision specifiers.
  • NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS: Set to 0 or 1. Enables floating-point specifiers (%f/%F).
  • NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER: Set to 0 or 1. Enables hex float specifier (%a/%A). Requires NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=1.
  • NANOPRINTF_USE_SMALL_FORMAT_SPECIFIERS: Set to 0 or 1. Enables small modifiers.
  • NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS: Set to 0 or 1. Enables oversized modifiers.
  • NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS: Set to 0 or 1. Enables binary specifiers.
  • NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS: Set to 0 or 1. Enables %n for write-back.
  • NANOPRINTF_USE_ALT_FORM_FLAG: Set to 0 or 1. Enables the # modifier for alternate print forms.
  • NANOPRINTF_USE_FLOAT_SINGLE_PRECISION: Set to 0 or 1. Uses float instead of double for all float math. Requires NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=1 and NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=1.
  • NANOPRINTF_VISIBILITY_STATIC: Optional define. Marks prototypes as static to sandbox nanoprintf.
  • NANOPRINTF_CONFIG_FILE: Optional define. When set (e.g. -DNANOPRINTF_CONFIG_FILE="\"my_npf_config.h\"" or -DNANOPRINTF_CONFIG_FILE="<my_npf_config.h>"), nanoprintf will #include the specified file at the top of nanoprintf.h, before any configuration-dependent code. This provides a FreeRTOS-style mechanism to ensure every translation unit sees the same configuration without requiring a wrapper header.

If no configuration flags are specified, nanoprintf will default to "reasonable" embedded values in an attempt to be helpful: floats are enabled, but writeback, binary, and large formatters are disabled. If any configuration flags are explicitly specified, nanoprintf requires that all flags are explicitly specified.

If a disabled format specifier feature is used, no conversion will occur and the format specifier string simply will be printed instead.

Floating-Point Conversion

nanoprintf has the following floating-point specific configuration defines.

  • NANOPRINTF_CONVERSION_BUFFER_SIZE: Optional, defaults to 23. Sets the size of a character buffer used for storing the converted value. Set to a larger number to enable printing of floating-point numbers with more characters. The buffer size does include the integer part, the fraction part and the decimal separator, but does not include the sign and the padding characters. If the number does not fit into buffer, an err is printed. Be careful with large sizes as the conversion buffer is allocated on stack memory.
  • NANOPRINTF_CONVERSION_FLOAT_TYPE: Optional, defaults to unsigned int. Sets the integer type used for float conversion algorithm, which determines the conversion accuracy. Can be set to any unsigned integer ty
View on GitHub
GitHub Stars805
CategoryDevelopment
Updated11h ago
Forks72

Languages

C++

Security Score

85/100

Audited on Mar 25, 2026

No findings