nanoprintf

repository·main·Indexed 21 days ago

https://github.com/charlesnicholson/nanoprintf

A lightweight, single-header C11 implementation of snprintf and vsnprintf designed for embedded systems. It features a tiny footprint (as low as ~480 bytes of object code), makes no memory allocations, and is thread-safe and re-entrant. It provides buffer-based formatting via npf_snprintf and character write callbacks via npf_pprintf for direct peripheral output. Features include configurable static flags to reduce binary size, support for fixed-width specifiers (wN/wfN), and an optional single-precision float mode for microcontrollers with FPUs.

Tokens
5.4K
Snippets
10
Records
24
Agent score
24%

What's inside nanoprintf

  1. Configure field width and precision

    main

    Field Width

    Specifies the total field width for the conversion, adding padding. If the width is specified as *, the value is read from the next argument in the varargs.

    Precision

    Prefixed with a ., specifies the precision of the number or string. If precision is specified as *, the value is read from the next argument in the varargs.

    Constraints and Behavior

    • Cap: Both field widths and precisions are capped at 65280. This cap applies to both literal numbers in the format string and values provided via *.
    • Negative Width: A negative * field width is left-justified at its magnitude.
    • Negative Precision: A negative * precision is discarded (after applying the cap).
  2. Understand nanoprintf return values

    main

    Unlike the C standard printf functions which may return negative values for errors or EOF, nanoprintf functions only return non-negative values.

    The return value represents:

    • For npf_snprintf: The number of characters that would have been written to the buffer (excluding the null-terminator).
    • For npf_pprintf: The number of characters actually sent to the callback.

    If you pass NULL to npf_snprintf, it returns the length of the formatted string without performing any writes.

  3. Understand nanoprintf floating-point limitations

    main

    When working with floating-point formatting in nanoprintf, be aware of the following:

    • Rounding: The rounding direction is fixed to round to nearest with ties to even (FE_TONEAREST). Other modes like FE_UPWARD, FE_DOWNWARD, or FE_TOWARDZERO are ignored.
    • Accuracy: For a conversion integer type of width N bits, the algorithm retains approximately N - 2.322 bits of accuracy. Integer parts up to 2^N - 1 and fraction parts with up to N - 2.322 bits after the decimal separator are converted perfectly.
    • Wide Characters: There is no wide-character support. %lc and %ls behave like %c and %s respectively.
    • Shared Generator Side-effect: Enabling %e, %g, or %a makes %f share the same internal generator. This can slightly expand the err boundary for %f (e.g., %.25f of 5.5e37 might print digits instead of returning an error), but it will not change the output of any previously working values.
  4. Use fixed-width specifiers (wN and wfN)

    main

    The wN and wfN modifiers allow you to use <stdint.h> types ([u]int_leastN_t and [u]int_fastN_t) directly in your format strings.

    Supported widths for N are 8, 16, 32, or 64. For these widths, nanoprintf resolves the mapping based on the actual range of the type on your target architecture. For example, on a target where int_least8_t is 16 bits wide, %w8d will correctly convert a 16-bit value.

    Note: If N is not one of the supported widths, nanoprintf will not parse it and will print the literal string (e.g., %w24d).

  5. Understand nanoprintf thread safety

    main

    nanoprintf is designed to be thread-safe and re-entrant because it uses only stack memory and no concurrency primitives.

    • Concurrent Calls: It is safe to call npf_ functions from multiple execution contexts (e.g., different threads or an ISR) simultaneously.
    • Shared Targets: If using npf_pprintf with a shared npf_putc target, you must manage synchronization for that target yourself.
    • Shared Buffers: If calling npf_snprintf from multiple threads to the same buffer, you must prevent data races externally.
  6. Write a variadic wrapper for nanoprintf

    main

    If you create your own variadic wrapper (e.g., my_printf) around nanoprintf, you must use the NPF_MAP_ARGS macro at the outermost call site. This ensures that float arguments are wrapped into npf_float_t before they cross the variadic boundary, preventing them from being promoted to double by the compiler before reaching va_start.

    // your_printf.h — the macro wraps args, then calls the real variadic function.
    // fmt is captured inside __VA_ARGS__ so no compiler-specific extensions are needed.
    #define my_printf(buf, sz, ...) \
      my_printf_((buf), (sz), NPF_MAP_ARGS(__VA_ARGS__))
    int my_printf_(char *buf, size_t sz, const char *fmt, ...);
    
    // your_printf.c — the real function receives pre-wrapped args via va_list.
    int my_printf_(char *buf, size_t sz, const char *fmt, ...) {
        va_list val;
        va_start(val, fmt);
        int rv = npf_vsnprintf(buf, sz, fmt, val);
        va_end(val);
        return rv;
    }
  7. Set up the nanoprintf development environment

    main

    To build the project and run the unit, conformance, and compilation tests, follow these steps:

    1. Clone or fork the repository.
    2. Execute the build script from the root directory.

    On Linux or macOS, use:

    ./b

    On Windows (using a Visual Studio Developer Command Prompt), use:

    py -3 build.py

    Note on Dependencies:

    • The build system uses cmake and ninja. If they are not in your system PATH, the ./b script will automatically download and deploy them into the external/ directory.
    • Test failures will result in a non-zero exit code.
  8. Ensure Sprintf safety and overflow handling

    main

    nanoprintf follows C Standard behavior for npf_snprintf and npf_vsnprintf regarding buffer overflows:

    • Standard Behavior: The buffer is filled but not overrun. If an overflow would occur, a null-terminator is written to the final byte of the buffer. If the buffer is NULL or zero-sized, no bytes are written.
    • Return Value: Always returns the number of bytes that would have been written (excluding the null-terminator), as per the C Standard.
    • Enhanced Safety Option: If you define NANOPRINTF_SNPRINTF_SAFE_EMPTY_STRING_ON_OVERFLOW, an overflow will cause the first byte of the buffer to be overwritten with a null-terminator (similar to Microsoft's snprintf_s).
    • NULL Strings: Passing a NULL pointer to a %s specifier will print nothing.
  9. Build the optional printf test suite

    main

    nanoprintf includes an optional test suite (a fork of the printf test suite) that is excluded by default for licensing reasons. To include this suite in your build:

    1. Update your submodules to retrieve the test suite code.
    2. Run the build script with the --paland flag.
    ./b --paland
  10. Install and integrate nanoprintf

    main

    nanoprintf is a single-header library in the style of stb. To use it in your project, follow these steps:

    1. Implementation Step: In exactly one source file in your project, define the implementation macro before including the header:

      #define NANOPRINTF_IMPLEMENTATION
      #include "path/to/nanoprintf.h"
    2. Usage Step: In any other source files where you need formatting capabilities, simply include the header without the implementation macro:

      #include "nanoprintf.h"
    // define your nanoprintf configuration macros here (see "Configuration" below)
    #define NANOPRINTF_IMPLEMENTATION
    #include "path/to/nanoprintf.h"
  11. Configure nanoprintf using a Wrapper Header

    main

    The recommended way to manage nanoprintf configuration is to create a central wrapper header. This ensures that all source files see the same configuration macros (like NANOPRINTF_USE_FLOAT_SINGLE_PRECISION) and that the NPF_MAP_ARGS macro is correctly applied at every call site.

    Avoid including nanoprintf.h directly in your files; include your wrapper header instead. This pattern is similar to how FreeRTOS and lwIP are configured.

    // npf_config.h — your project's wrapper header. Every source file includes this.
    #ifndef NPF_CONFIG_H
    #define NPF_CONFIG_H
    
    #define NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS 1
    #define NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS 1
    #define NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS 1
    #define NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS 0
    #define NANOPRINTF_USE_SMALL_FORMAT_SPECIFIERS 0
    #define NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS 0
    #define NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS 0
    #define NANOPRINTF_USE_ALT_FORM_FLAG 1
    #define NANOPRINTF_USE_FLOAT_SINGLE_PRECISION 1
    
    #include "nanoprintf.h"
    #endif
    
    // npf_config.c — one source file compiles the implementation.
    #define NANOPRINTF_IMPLEMENTATION
    #include "npf_config.h"
    
    // any_other_file.c — all other files just include the wrapper header.
    #include "npf_config.h"
    
    void example(void) {
        char buf[64];
        npf_snprintf(buf, sizeof(buf), "%d %.2f", 42, 3.14f);  // just works
    }