nanoprintf is an implementation of snprintf and vsnprintf for embedded systems that, when fully enabled, aims for C11 standard compliance.
nanoprintf makes no memory allocations and uses less than 100 bytes of stack. Compiling with all optional features disabled yields ~2.4KB of ARM Cortex-M object code, and compiling with all optional features enabled is closer to 5KB. This is all larger on x64, but why are you using nanoprintf on x64? :)
nanoprintf is written in C89 for maximal compiler compatibility. C99 or C++11 compilers are required (for
uint64_t and other types) if floating point conversion or large modifiers are enabled. nanoprintf does include standard headers but only uses them for types and argument lists; no calls are made into stdlib / libc, with the exception of any internal double-to-float conversion ABI calls your compiler might emit.
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.
tinyprintf doesn't print floating point values.
printf defines the actual standard library
printf symbol, which isn't always what you want. It stores the final converted string (with padding and precision) in a temporary buffer, which makes supporting longer strings more costly. It also doesn't support the
%n "write-back" specifier.
No other embedded-friendly printf projects that I could fine are in the public domain and have single-file implementations. Really though, I've just wanted to try my hand at a really small printf system for a while now.
nanoprintf.hinto your codebase somewhere.
Add the following code to one of your
.cppfiles to compile the nanoprintf implementation:
#define NANOPRINTF_IMPLEMENTATION #include "path/to/nanoprintf.h"
To call, just
#include "path/to/nanoprintf.h"as usual and call the functions.
Compile your code with your nanoprintf configuration flags. Alternately, wrap
nanoprintf.hin your own header that defines all of your configuration flags, and use that everywhere in steps 2-3.
nanoprintf has 4 main functions:
npf_snprintf: Use like snprintf.
npf_vsnprintf: Use like vsnprintf (
npf_pprintf: Use like printf with a per-character write callback (semihosting, UART, etc).
npf_vpprintf: Use like
npf_pprintfbut takes a
pprintf variations take a callback that receives the character to print and a user-provided context pointer.
npf_[v]snprintf to write nothing, and only return the length of the formatted string.
nanoprintf does not provide
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
nanoprintf has the following static configuration flags. You can either inject them into your compiler (usually
-D flags) or wrap
nanoprintf.h in your own header that sets them up, and then
#include your header instead of
nanoprintf.h in your application.
If no configuration flags are specified, nanoprintf will default to "reasonable" embedded values in an attempt to be helpful: floats enabled, writeback and large formatters disabled. If any configuration flags are explicitly specified, nanoprintf requires that all flags are explicitly specified.
NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS: Set to
1. Enables field width specifiers.
NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS: Set to
1. Enables precision specifiers.
NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS: Set to
1. Enables floating-point specifiers.
NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS: Set to
1. Enables oversized modifiers.
NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS: Set to
NANOPRINTF_VISIBILITY_STATIC: Optional define. Marks prototypes as
staticto sandbox nanoprintf.
If a disabled format specifier feature is used, no conversion will occur and the format specifier string simply will be printed instead.
nanoprintf expects a conversion specification string of the following form:
[flags][field width][.precision][length modifier][conversion specifier]
None or more of the following:
0: Pad the field with leading zero characters.
-: Left-justify the conversion result in the field.
+: Signed conversions always begin with
#: Writes extra characters (
.for empty floats, '0' for empty octals, etc).
Field width (if enabled)
A number that specifies the total field width for the conversion, adds padding. If field width is
*, the field width is read from the next vararg.
Precision (if enabled)
Prefixed with a
., a number that specifies the precision of the number or string. If precision is
*, the precision is read from the next vararg.
None or more of the following:
shortfor integral and write-back vararg width.
long doublefor float vararg width (note: it will then be casted down to
double, or wide vararg width.
charfor integral and write-back vararg width.
ll: (large specifier) Use
long longfor integral and write-back vararg width.
j: (large specifier) Use the
[u]intmax_ttypes for integral and write-back vararg width.
z: (large specifier) Use the
size_ttypes for integral and write-back vararg width.
t: (large specifier) Use the
ptrdiff_ttypes for integral and write-back vararg width.
Exactly one of the following:
%%: Percent-sign literal
%s: Null-terminated strings
%d: Signed integers
%u: Unsigned integers
%o: Unsigned octal integers
%X: Unsigned hexadecimal integers
%n: Write the number of bytes written to the pointer vararg
%F: Floating-point values
Floating point conversion is performed by extracting the value into 64:64 fixed-point with an extra field that specifies the number of leading zero fractional digits before the first nonzero digit. No rounding is currently performed; values are simply truncated at the specified precision. This is done for simplicity, speed, and code footprint.
nano in the name, there's no way to do away with double entirely, since the C language standard says that floats are promoted to double any time they're passed into variadic argument lists. nanoprintf casts all doubles back down to floats before doing any conversions.
To get the environment and run tests (linux / mac only for now):
- Clone or fork this repository.
./bfrom the root.
This will build all of the unit, conformance, and compilation tests for your host environment. Any test failures will return a non-zero exit code.
No wide-character support exists: the
%ls fields require that the arg be converted to a char array as if by a call to wcrtomb. When locale and character set conversions get involved, it's hard to keep the name "nano". Accordingly,
%ls behave like
Currently the only supported float conversions are the decimal forms:
%F. All other float conversions (exponent form, hexadecimal exponent form, dynamic precision form) behave like
%F. Pull requests welcome!
This code is optimized for size, not readability or structure. Unfortunately modularity and "cleanliness" even in C 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. :)