GithubHelp home page GithubHelp logo

charlesnicholson / nanoprintf Goto Github PK

View Code? Open in Web Editor NEW
575.0 15.0 39.0 511 KB

The smallest public printf implementation for its feature set.

License: Other

CMake 1.92% Shell 0.04% C 9.02% C++ 86.53% Python 2.48%
embedded embedded-c embedded-systems cortex-m c printf snprintf mpaland firmware tinyprintf

nanoprintf's Introduction

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 double (they get casted to float), scientific notation (%e, %g, %a), and the 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 ~760-2500 bytes of object code on a Cortex-M0 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 double-to-float conversion ABI 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.
  • 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_VISIBILITY_STATIC: Optional define. Marks prototypes as static to sandbox nanoprintf.

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.

Sprintf Safety

By default, npf_snprintf and npf_vsnprintf behave according to the C Standard: the provided buffer will be filled but not overrun. If the string would have overrun the buffer, a null-terminator byte will be written to the final byte of the buffer. If the buffer is null or zero-sized, no bytes will be written.

If you define NANOPRINTF_SNPRINTF_SAFE_EMPTY_STRING_ON_OVERFLOW and your string is larger than your buffer, the first byte of the buffer will be overwritten with a null-terminator byte. This is similar in spirit to Microsoft's snprintf_s.

In all cases, nanoprintf will return the number of bytes that would have been written to the buffer, had there been enough room. This value does not account for the null-terminator byte, in accordance with the C Standard.

Thread Safety

nanoprintf uses only stack memory and no concurrency primitives, so internally it is oblivious to its execution environment. This makes it safe to call from multiple execution contexts concurrently, or to interrupt a npf_ call with another npf_ call (say, an ISR or something). If you use npf_pprintf concurrently with the same npf_putc target, it's up to you to ensure correctness inside your callback. If you npf_snprintf from multiple threads to the same buffer, you will have an obvious data race.

Formatting

Like printf, nanoprintf expects a conversion specification string of the following form:

[flags][field width][.precision][length modifier][conversion specifier]

  • Flags

    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 + or - characters.
    • : (space) A space character is inserted if the first converted character is not a sign.
    • #: Writes extra characters (0x for hex, . 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.

  • Length modifier

    None or more of the following:

    • h: Use short for integral and write-back vararg width.
    • L: Use long double for float vararg width (note: it will then be casted down to float)
    • l: Use long, double, or wide vararg width.
    • hh: Use char for integral and write-back vararg width.
    • ll: (large specifier) Use long long for integral and write-back vararg width.
    • j: (large specifier) Use the [u]intmax_t types for integral and write-back vararg width.
    • z: (large specifier) Use the size_t types for integral and write-back vararg width.
    • t: (large specifier) Use the ptrdiff_t types for integral and write-back vararg width.
  • Conversion specifier

    Exactly one of the following:

    • %: Percent-sign literal
    • c: Character
    • s: Null-terminated strings
    • i/d: Signed integers
    • u: Unsigned integers
    • o: Unsigned octal integers
    • x / X: Unsigned hexadecimal integers
    • p: Pointers
    • n: Write the number of bytes written to the pointer vararg
    • f/F: Floating-point decimal
    • e/E: Floating-point scientific (unimplemented, prints float decimal)
    • g/G: Floating-point shortest (unimplemented, prints float decimal)
    • a/A: Floating-point hex (unimplemented, prints float decimal)
    • b/B: Binary integers

Floating Point

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. This is done for simplicity, speed, and code footprint.

Because the float -> fixed code operates on the raw float value bits, no floating point operations are performed. This allows nanoprintf to efficiently format floats on soft-float architectures like Cortex-M0, and to function identically with or without optimizations like "fast math". Despite 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. No other single- or double- precision operations are performed.

The %e/%E, %a/%A, and %g/%G specifiers are parsed but not formatted. If used, the output will be identical to if %f/%F was used. Pull requests welcome! :)

Limitations

No wide-character support exists: the %lc and %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, %lc and %ls behave like %c and %s, respectively.

Currently the only supported float conversions are the decimal forms: %f and %F. Pull requests welcome!

Measurement

The CI build is set up to use gcc and nm to measure the compiled size of every pull request. See the Presubmit Checks "size reports" job output for recent runs.

The following size measurements are taken against the Cortex-M0 build.

Configuration "Minimal":
arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 -
arm-none-eabi-nm --print-size --size-sort npf.o
00000014 00000002 t npf_bufputc_nop
00000016 00000010 t npf_putc_cnt
00000000 00000014 t npf_bufputc
00000298 00000016 T npf_pprintf
000002e0 00000016 T npf_snprintf
000002ae 00000032 T npf_vsnprintf
00000026 00000272 T npf_vpprintf
Total size: 0x2f6 (758) bytes

Configuration "Binary":
arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 -
arm-none-eabi-nm --print-size --size-sort npf.o
00000014 00000002 t npf_bufputc_nop
00000016 00000010 t npf_putc_cnt
00000000 00000014 t npf_bufputc
000002de 00000016 T npf_pprintf
00000328 00000016 T npf_snprintf
000002f4 00000034 T npf_vsnprintf
00000026 000002b8 T npf_vpprintf
Total size: 0x33e (830) bytes

Configuration "Field Width + Precision":
arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 -
arm-none-eabi-nm --print-size --size-sort npf.o
00000014 00000002 t npf_bufputc_nop
00000016 00000010 t npf_putc_cnt
00000000 00000014 t npf_bufputc
00000546 00000016 T npf_pprintf
00000590 00000016 T npf_snprintf
0000055c 00000034 T npf_vsnprintf
00000026 00000520 T npf_vpprintf
Total size: 0x5a6 (1446) bytes

Configuration "Field Width + Precision + Binary":
arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 -
arm-none-eabi-nm --print-size --size-sort npf.o
00000014 00000002 t npf_bufputc_nop
00000016 00000010 t npf_putc_cnt
00000000 00000014 t npf_bufputc
00000590 00000016 T npf_pprintf
000005d8 00000016 T npf_snprintf
000005a6 00000032 T npf_vsnprintf
00000026 0000056a T npf_vpprintf
Total size: 0x5ee (1518) bytes

Configuration "Float":
arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 -
arm-none-eabi-nm --print-size --size-sort npf.o
00000014 00000002 t npf_bufputc_nop
00000016 00000010 t npf_putc_cnt
00000000 00000014 t npf_bufputc
0000059c 00000016 T npf_pprintf
000005e4 00000016 T npf_snprintf
000005b2 00000032 T npf_vsnprintf
00000026 00000576 T npf_vpprintf
Total size: 0x5fa (1530) bytes

Configuration "Everything":
arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=1 -
arm-none-eabi-nm --print-size --size-sort npf.o
00000014 00000002 t npf_bufputc_nop
00000016 00000010 t npf_putc_cnt
00000000 00000014 t npf_bufputc
00000934 00000016 T npf_pprintf
0000097c 00000016 T npf_snprintf
0000094a 00000032 T npf_vsnprintf
00000026 0000090e T npf_vpprintf
Total size: 0x992 (2450) bytes

Development

To get the environment and run tests:

  1. Clone or fork this repository.
  2. Run ./b from the root (or py -3 build.py from the root, for Windows users)

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.

The nanoprintf development environment uses cmake and ninja. If you have these in your path, ./b will use them. If not, ./b will download and deploy them into path/to/your/nanoprintf/external.

nanoprintf uses GitHub Actions for all continuous integration builds. The GitHub Linux builds use this Docker image from my Docker repository.

The matrix builds [Debug, Release] x [32-bit, 64-bit] x [Mac, Windows, Linux] x [gcc, clang, msvc], minus the 32-bit clang Mac configurations.

One test suite is a fork from the printf test suite, which is MIT licensed. It exists as a submodule for licensing purposes- nanoprintf is public domain, so this particular test suite is optional and excluded by default. To build it, retrieve it by updating submodules and add the --paland flag to your ./b invocation. It is not required to use nanoprintf at all.

Acknowledgments

I implemented Float-to-int conversion using the ideas from Wojciech Muła's float -> 64:64 fixed algorithm.

I ported the printf test suite to nanoprintf. It was originally from the mpaland printf project codebase but adopted and improved by Eyal Rozenberg and others. (Nanoprintf has many of its own tests, but these are also very thorough and very good!)

The binary implementation is based on the requirements specified by Jörg Wunsch's N2630 proposal, hopefully to be accepted into C23!

nanoprintf's People

Contributors

akaeba avatar amakukha avatar charlesnicholson avatar ddz avatar deanoburrito avatar ideal avatar jimktrains avatar mateoconlechuga avatar okarss avatar sgraham avatar shreyasbharath avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

nanoprintf's Issues

Reject unsupported float types

The code's sloppy here, npf__parse_format_spec should just return failure for unsupported float types (exponent, dynamic, hexponent).

Better documentation for putc callback

The readme doesn't give an example of the actual npf_putc callback, just how to call nanoprintf with a callback.

Add an actual callback, with use of the context pointer, to the readme to make it clearer.

Recursive include

I was trying to bundle nanoprintf in a standalone big C file, but I am unable to do this because nanoprintf.h requires it self in this line:

#include "nanoprintf.h"

Could the code be refactored so nanoprintf does not need to recursively include itself? This will give more freedom to save it under a different name, or along with many other single headers libraries as a big single header bundle.

NANOPRINTF_INTERFACE_STATIC compilation test

Compile nanoprintf with NANOPRINTF_INTERFACE_STATIC in one C file, then create another that redefines one of the nanoprintf symbols. Link them together and make sure there are no redefinition errors.

Handle negative field width star args

If * is used with field width, and is negative, it should add the left-justify flag and set field-width to the absolute value of the * arg.

char buf[32];
npf_snprintf(buf, sizeof(buf), "<%*u>", -3, 1);
// buf should be "<1  >" (1 space space)

npf_putc => npf_putcs, print runs

nanoprintf checks for EOF after every single character. It would be better if it scanned for runs: instead of stopping when the putc callback returns NPF_EOF, it could stop any time the function doesn't return the number of provided characters.

This would make the core loop performance better, since it would only check for exhaustion per-run, not per-character. It would also allow NPF_EOF to be deleted.

Clang address/undefined behaviour sanitizer errors

Thanks for creating a great library!

Was running into a couple of errors when compiled with Clang's address/undefined behaviour sanitizers. A couple of examples below -

nanoprintf.h:893:57: runtime error: load of value 18036456, which is not a valid value for type 'npf__format_spec_conversion_case_t'

nanoprintf.h:555:14: runtime error: load of value 18036456, which is not a valid value for type 'npf__format_spec_conversion_case_t'

These are not controlled by the user directly so I presume it's some sort of initialisation error in the library?

Push * parsing into npf__parse_format_spec

npf__parse_format_spec doesn't take the va_list so it can't parse star arguments, but star parsing is really part of parsing the format spec.

Pass the va_list to npf__parse_format_spec so it can extract star args as needed.

Support floats without C99/C++11

Right now C99/C++11 are required when NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS is enabled because the float conversion algorithm uses the stdint standard uint64_t type to create the 64:64 fixed-point representation of the value.

If the user wants to tell nanoprintf what a 64-bit type is, then C99/C++11 would only be required for the NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS flag, but that's a lot less surprising; you asked for C99-only type specifiers...

Explore smaller float code with uint32_t

Right now the code that splits floats up into [integer portion | number of leading 0's after decimal | decimal portion] stores the decomposed integer + decimal portions as uint64_t. This is very very expensive (in time and space) on 8-bit platforms like the eZ80.

Nanoprintf captures full floating-point precision, but truncates instead of rounds as a performance optimization.

Explore a compile-time flag that intentionally sacrifices precision for size/speed by storing the integer + decimal portions as uint32_t's instead.

cc @mateoconlechuga

Odd prints, likely my configuration

Prints out

abc
abc=536875404
abc=2000118c

From

printfx_("abc\r\n");
printfx_("abc=%d\r\n",5);
printfx_("abc=%x\r\n",50);

Code

#define NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS 1

// Compile nanoprintf in this translation unit.
#define NANOPRINTF_IMPLEMENTATION

#include "External/nanoprintf-0.3.2/nanoprintf.h"

void putchar_(char c, void *ctx) {
    char data[] = {c};
    SERCOM3_USART_Write((void*)data, 1);
}

void printf_(char const *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    npf_pprintf(putchar_, NULL, fmt, args);
    va_end(args);
}
uint8_t buffer[128];

void printfx_(char const *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    int len = npf_snprintf(buffer, 128, fmt, args);
    va_end(args);
    for(int i = 0; i < len; i++) {
        putchar_(buffer[i], NULL);
    }
}

Is my setup wrong here? thx

npf_vsnprintf writes beyond bufsz

When calling npf_vsnprintf() in order to write a limited number of characters (bufsz parameter) into the destination buffer, the function writes beyond the bufsz offset into the destination buffer.
The issue was found when using npf_vsnprintf() to write small pieces (50 chars) of a larger text buffer (>500 chars) to a FIFO before sending it to a UART. No format specifiers, just characters.

I would expect the function to write no more than bufsz chars into the destination buffer, because that buffer could be limited in size (the very reason for calling this function).

Currently npf_vsnprintf() simply calls npf_vpprintf(), which does not seem to consider the size of the destination buffer and just processes the entire format string. The option of putting a terminating 0 at the beginning or end (bufsz-1) of the buffer in case of overflow is not sufficient, because the overflow has already occurred and the memory following the destination buffer is corrupted. Also the return value n is still the result of the entire format string, but it should be at most bufsz.

Ignore negative precision * args

Negative precision * args should be ignored:

char buf[32];
npf_snprintf(buf, sizeof(buf), "%.*i", -100, 1);
// buf should be "1"

Invalid output when both the 0 flag and a precision appear

With the following code:

#include <stdio.h>

#ifdef USE_NANOPRINTF
#define NANOPRINTF_IMPLEMENTATION
#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 1
#define NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS 1
#include "nanoprintf.h"
#define snprintf npf_snprintf
#endif

int main()
{
    char output[100];
    snprintf(output, sizeof(output), "%02.1d", 0);
    printf("%s\n", output);
}

when compiled without USE_NANOPRINTF, I get an output of 0, whereas when using nanoprintf, 00 is output:

$ gcc test.c -o default
$ gcc -DUSE_NANOPRINTF test.c -o nanoprintf
$ ./default; ./nanoprintf 
 0
00

Nanoprintf's behavior is invalid because the C standard states that:

For d, i, o, u, x, and X conversions, if a precision is specified, the 0 flag is ignored.

The current trunk version of nanoprintf fails to do so.

Define Guard: error multiple definition npf_vpprintf, npf_pprintf, npf_snprintf,npf_vsnprintf

Hi Charles,

at the moment i try to inculde the nanoptintf.h into several files, they are also included. Following example:

  • main.c
    • #include "nanoprintf.h"
    • #include "sub.h"
      • #include "nanoprintf.h"

There i receive the compiler errors of multiple definition of npf_vpprintf, npf_pprintf, npf_snprintf,npf_vsnprintf. In your code it looks like an define guard, but i have no idea why i receive this eros.

./include/nanoprintf/nanoprintf.h:1045: multiple definition of `npf_vsnprintf'; ./main.c.o:./include/nanoprintf/nanoprintf.h:1045: first defined here

Thanks for your help.

BR,
Andreas

IAR Workbench support

Make sure it compiles cleanly, has correct compiler support ifdefs + pragmas, etc

strlen on string that might not be null-terminated

It's common to print out the ascii part of a binary blob by doing:

printf("%.10s", my_binary_buf);

In this case, my_binary_buf doesn't need to be null-terminated; the explicit precision of 10 bounds the string. Nanoprintf does this as well, but also does a strlen loop before computing the printing length:

      case NPF_FMT_SPEC_CONV_STRING: {
        cbuf = va_arg(args, char *);
        for (char const *s = cbuf; *s; ++s, ++cbuf_len); // strlen
#if NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS == 1
        if (fs.prec_opt == NPF_FMT_SPEC_OPT_LITERAL) {
          cbuf_len = npf_min(fs.prec, cbuf_len); // prec truncates strings
        }
#endif
      } break;

The strlen loop will run wild off the end of the "string" if it's not null-terminated, which can be a valid scenario when explicit precision is used (instead of a null terminator) as an upper bound.

We probably want two paths here; if precision specifiers aren't enabled, or of the prec_opt is none, just do the strlen loop. If the prec_opt exists, though, we want to stop the strlen loop early, as soon as it's >= fs.prec.

"Large" Length Modifiers

Support ll, j, z, t length modifiers:

  1. Make sure the right type is used in va_arg
  2. Add a 64-bit npf__llutoa_rev
  3. Figure out whether to call npf__llutoa_rev or npf__utoa_rev on the casted type.

Version in nanoprintf.h

Hi,

thank you a lot for your work.

I'm using nanoprintf at work by copying and pasting the nanoprintf.h file inside my cpp project.

I noticed that inside the nanoprintf.h the version is not indicated. If I open the file inside my project I don't know what version nanoprintf is.

Would it be possible to add the version in the comment section of the nanopritnf.h file for future releases?

Thanks in advance for your time

C89 length modifiers

The basics:

h: short for numbers
l: long for numbers
L: long double for floats

Configuration flag for emitting NUL

Figure out how to configure npf_vpprintf to not emit a NUL terminator.

It could be runtime at a cost of a few more bytes and more parameters, but npf_[v]snprintf would still work as expected.

Or, it could be a configuration-time thing with a new NANOPRINTF_TERMINATE_STRINGS flag, but [v]snprintf might surprise people. Maybe that's ok since they asked for unterminated strings? It feels hostile to users...

Windows support

batch / psh file that pulls down cmake / ninja and builds the test suite with MSVC.

Alternately rewrite b in python or something and write cross-platform logic once and have b just run python build.py.

Harness on CircleCI once they launch Windows

Use in submodule https instead of ssh

Hi Charles,

the nanoprintf is really a nice module for embedded systems. Thanks for sharing the code. When i try to build my system in a CICD environment my runners struggle to clone your test submodule. Therefore, could you adjust your .gitmodules that you replace:

url = [email protected]:charlesnicholson/nanoprintf-paland-conformance.git

this line by:

https://github.com/charlesnicholson/nanoprintf-paland-conformance.git

This would solve my clone issue and should have no influence - because it's a public repo. What do you think? If you like i can also prepare a PR.

Thanks for your help!

BR,
Andreas

Float hexponent support: "%a" / "%A"

Does anyone actually use this? I don't get it.

converts floating-point number to the hexadecimal exponent notation.

For the a conversion style [-]0xh.hhhp±d is used.
For the A conversion style [-]0Xh.hhhP±d is used.
The first hexadecimal digit is not 0 if the argument is a normalized floating point value. 
If the value is ​0​, the exponent is also ​0​. Precision specifies the minimum number of digits 
to appear after the decimal point character. The default precision is sufficient for exact 
representation of the value. In the alternative implementation decimal point character is
 written even if no digits follow it. For infinity and not-a-number conversion style see notes.

Support multiple inclusion

Currently nanoprintf doesn't support being included as an interface and then an implementation, e.g.

#include "nanoprintf.h"
#define NANOPRINTF_IMPLEMENTATION
...
#include "nanoprintf.h"

Set up the header guards inside nanoprintf.h so that it can be included many times in either mode.

Add examples

Add a top-level directory called "examples" to demonstrate different types of integrations.

  1. "my_project_printf.h", which exposes a printf-like api backed by a hermetic instantiation of nanoprintf in "my_project_printf.c".

  2. "my_project_nanoprintf.h", which sets up nanoprintf flags and includes the nanoprintf header directly, with "my_project_nanoprintf.c" simply compiling npf.

Missing break in switch-case?

Hello,
is it intended, that there is no break for each case here?

nanoprintf/nanoprintf.h

Lines 402 to 407 in 64333b3

case 'i':
case 'd': tmp_conv = NPF_FMT_SPEC_CONV_SIGNED_INT;
case 'o': if (tmp_conv == -1) { tmp_conv = NPF_FMT_SPEC_CONV_OCTAL; }
case 'u': if (tmp_conv == -1) { tmp_conv = NPF_FMT_SPEC_CONV_UNSIGNED_INT; }
case 'X': if (tmp_conv == -1) { out_spec->case_adjust = 0; }
case 'x': if (tmp_conv == -1) { tmp_conv = NPF_FMT_SPEC_CONV_HEX_INT; }

Stricter warnings

gcc:
-Wconversion
-Wsign-conversion
-Wswitch-default
-Wswitch-enum
-Wshadow

compile-time flags for emitting public npf functions

If the static implementation flag is set, aggressive warnings settings can yell about unused top-level npf functions (npf_snprintf). Maybe top-level flags for things like

#define NANOPRINTF_SNPRINTF_ENABLED
#define NANOPRINTF_VPPRINTF_ENABLED

so that static-implementation users can only enable the functions they care about

Wrong value for floats on atmega328p (arduino).

Here is my example program. The nanoprintf directory is a git clone of this repository.

typedef size_t ssize_t;

#define NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS 0

// Compile nanoprintf in this translation unit.
#define NANOPRINTF_IMPLEMENTATION
#include "nanoprintf/nanoprintf.h"

int my_snprintf(char *buffer, size_t bufsz, char const *fmt, ...) {
  va_list val;
  va_start(val, fmt);
  int const rv = npf_vsnprintf(buffer, bufsz, fmt, val);
  va_end(val);
  return rv;
}


void setup() {
  Serial.begin(115200);
  float f = 5.4;
  char buf[15];
  my_snprintf(buf, 15, "% 6.2f", f);
  Serial.println(buf);
}

void loop() {
}

This gives:

 517.40

(The value is wrong, but it's also placing a leading space to make the whole thing 7 char long instead of 6?)

I have a test program I compiled on my laptop(Linux lamb 5.15.0-76-generic #83-Ubuntu SMP Thu Jun 15 19:16:32 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux via g++ -o test test.cc), which works fine.

#define NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS 0

// Compile nanoprintf in this translation unit.
#define NANOPRINTF_IMPLEMENTATION
#include "nanoprintf/nanoprintf.h"

#include <iostream>

int my_snprintf(char *buffer, size_t bufsz, char const *fmt, ...) {
  va_list val;
  va_start(val, fmt);
  int const rv = npf_vsnprintf(buffer, bufsz, fmt, val);
  va_end(val);
  return rv;
}


int main() {
  float f = 5.4;
  char buf[15];
  my_snprintf(buf, 15, "% 6.2f", f);
  std::cout << buf << std::endl;
  return 0;
}

This produces:

  5.40

This is the correct value and correct width.

Some compiler versions.

% avr-c++ -v
Using built-in specs.
Reading specs from /usr/lib/gcc/avr/5.4.0/device-specs/specs-avr2
COLLECT_GCC=avr-c++
COLLECT_LTO_WRAPPER=/usr/lib/gcc/avr/5.4.0/lto-wrapper
Target: avr
Configured with: ../gcc/configure -v --enable-languages=c,c++ --prefix=/usr/lib --infodir=/usr/share/info --mandir=/usr/share/man --bindir=/usr/bin --libexecdir=/usr/lib --libdir=/usr/lib --enable-shared --with-system-zlib --enable-long-long --enable-nls --without-included-gettext --disable-libssp --disable-libcc1 --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=avr ASFLAGS= CFLAGS='-g -O2 -ffile-prefix-map=/build/gcc-avr-VxQ9yL/gcc-avr-5.4.0+Atmel3.6.2=. -flto=auto -ffat-lto-objects -flto=auto -ffat-lto-objects -fstack-protector-strong -Wformat ' CPPFLAGS='-Wdate-time -D_FORTIFY_SOURCE=2' CXXFLAGS='-g -O2 -ffile-prefix-map=/build/gcc-avr-VxQ9yL/gcc-avr-5.4.0+Atmel3.6.2=. -flto=auto -ffat-lto-objects -flto=auto -ffat-lto-objects -fstack-protector-strong -Wformat ' DFLAGS=-frelease FCFLAGS='-g -O2 -ffile-prefix-map=/build/gcc-avr-VxQ9yL/gcc-avr-5.4.0+Atmel3.6.2=. -flto=auto -ffat-lto-objects -flto=auto -ffat-lto-objects -fstack-protector-strong' FFLAGS='-g -O2 -ffile-prefix-map=/build/gcc-avr-VxQ9yL/gcc-avr-5.4.0+Atmel3.6.2=. -flto=auto -ffat-lto-objects -flto=auto -ffat-lto-objects -fstack-protector-strong' GCJFLAGS='-g -O2 -ffile-prefix-map=/build/gcc-avr-VxQ9yL/gcc-avr-5.4.0+Atmel3.6.2=. -flto=auto -ffat-lto-objects -fstack-protector-strong' LDFLAGS='-Wl,-Bsymbolic-functions -flto=auto -ffat-lto-objects -flto=auto -Wl,-z,relro' OBJCFLAGS='-g -O2 -ffile-prefix-map=/build/gcc-avr-VxQ9yL/gcc-avr-5.4.0+Atmel3.6.2=. -flto=auto -ffat-lto-objects -flto=auto -ffat-lto-objects -fstack-protector-strong -Wformat ' OBJCXXFLAGS='-g -O2 -ffile-prefix-map=/build/gcc-avr-VxQ9yL/gcc-avr-5.4.0+Atmel3.6.2=. -flto=auto -ffat-lto-objects -flto=auto -ffat-lto-objects -fstack-protector-strong -Wformat '
Thread model: single
gcc version 5.4.0 (GCC) 
% g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/11/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 11.3.0-1ubuntu1~22.04.1' --with-bugurl=file:///usr/share/doc/gcc-11/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-11 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-11-aYxV0E/gcc-11-11.3.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-11-aYxV0E/gcc-11-11.3.0/debian/tmp-gcn/usr --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 11.3.0 (Ubuntu 11.3.0-1ubuntu1~22.04.1)

I haven't reached too far into the code to figure out what's going on, but if I find anything I'll update the ticket. Also, apologies if this has come up before. I didn't see a ticket that looked similar and didn't notice anything in the README.

Conformance tests in C and C++

Rework conformance + mpaland tests to compile nanoprintf as a separate .o file and link in. That allows for a full dimension of the conformance tests to run against C and C++ compilations. This would hopefully tease out any subtle C-vs-C++ language difference bugs.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.