nanoprintf 是针对嵌入式系统的 snprintf 和 vsnprintf 的无阻碍实现,完全启用后,其目标是符合 C11 标准。主要的例外是浮点、科学记数法( %e
、 %g
、 %a
)以及需要wcrtomb
存在的转换。根据 N2630,可选择支持 C23 二进制整数输出。 snprintf 和 vsnprintf 的安全扩展可以选择配置为在缓冲区溢出事件时返回修剪或全空的字符串。
此外,nanoprintf 可用于解析 printf 样式的格式字符串以提取各种参数和转换说明符,而无需执行任何实际的文本格式化。
nanoprintf 不进行内存分配并且使用少于 100 字节的堆栈。它在 Cortex-M0 架构上编译为约 740-2640 字节的目标代码,具体取决于配置。
所有代码均以 C99 的最小方言编写,以实现最大的编译器兼容性,在 clang + gcc + msvc 上以最高警告级别进行干净地编译,不会引发 UBsan 或 Asan 的任何问题,并在 32 位和 64 位架构上进行了详尽的测试。 nanoprintf 确实包含 C 标准标头,但仅将它们用于 C99 类型和参数列表;除了编译器可能发出的任何内部大整数算术调用之外,不会对 stdlib / libc 进行任何调用。与往常一样,如果您要为 msvc 进行本机编译,则需要一些特定于 Windows 的标头。
nanoprintf 是 stb 库风格的单个头文件。存储库的其余部分是测试和脚手架,不需要使用。
nanoprintf 是静态可配置的,因此用户可以在大小、编译器要求和功能集之间找到平衡。浮点转换、“大”长度修饰符和大小写回都是可配置的,并且仅在明确请求时才进行编译,有关详细信息,请参阅配置。
将以下代码添加到源文件之一以编译 nanoprintf 实现:
// define your nanoprintf configuration macros here (see "Configuration" below) #define NANOPRINTF_IMPLEMENTATION #include "path/to/nanoprintf.h"
然后,在任何要使用 nanoprintf 的文件中,只需包含标头并调用 npf_ 函数:
#include "nanoprintf.h" void print_to_uart(void) { npf_pprintf(&my_uart_putc, NULL, "Hello %s%c %d %u %fn", "worl", 'd', 1, 2, 3.f); } void print_to_buf(void *buf, unsigned len) { npf_snprintf(buf, len, "Hello %s", "world"); }
有关更多详细信息,请参阅“直接使用 nanoprintf”和“Wrap nanoprintf”示例。
我想要一个单文件公共域嵌入式 printf,在最小配置(引导加载程序等)下大小小于 1KB,启用浮点功能后大小小于 3KB。
在固件工作中,我通常希望 stdio 的字符串格式化没有系统调用或文件描述符层要求;在您想要登录到小型缓冲区或直接发送到总线的小型系统中,它们几乎从不需要。此外,许多嵌入式 stdio 实现比它们需要的更大或更慢——这对于引导加载程序工作很重要。如果您不需要任何系统调用或 stdio 铃声+口哨,您可以简单地使用 nanoprintf 和nosys.specs
并精简您的构建。
该代码针对大小而不是可读性或结构进行了优化。不幸的是,模块化和“清洁性”(无论这意味着什么)会在如此小的规模下增加开销,因此大多数功能和逻辑都被推入npf_vpprintf
中。这不是正常嵌入式系统代码应有的样子;这是#ifdef
汤,很难理解,如果你必须在实现中深入研究,我深表歉意。希望各种测试能够为您提供参考。
或者,也许你是一个比我更好的程序员!在这种情况下,请帮助我使代码更小、更简洁,而不会使足迹更大,或者将我推向正确的方向。 :)
nanoprintf有4个主要功能:
npf_snprintf
:像 snprintf 一样使用。
npf_vsnprintf
:像 vsnprintf 一样使用( va_list
支持)。
npf_pprintf
:像 printf 一样使用每个字符的写入回调(半主机、UART 等)。
npf_vpprintf
:与npf_pprintf
类似,但采用va_list
。
pprintf
变体采用一个回调来接收要打印的字符和用户提供的上下文指针。
将NULL
或nullptr
传递给npf_[v]snprintf
不写入任何内容,并且仅返回格式化字符串的长度。
nanoprintf本身不提供printf
或putchar
;这些被视为系统级服务,而 nanoprintf 是一个实用程序库。不过,nanoprintf 希望是滚动您自己的printf
的一个很好的构建块。
nanoprintf 函数都返回相同的值:发送到回调(对于 npf_pprintf)的字符数或写入提供足够空间的缓冲区的字符数。空终止符 0 字节不是计数的一部分。
C 标准允许 printf 函数在无法执行字符串或字符编码或输出流遇到 EOF 的情况下返回负值。由于 nanoprintf 不关心文件等操作系统资源,并且不支持wchar_t
支持的l
长度修饰符,因此任何运行时错误要么是内部错误(请报告!),要么是不正确的使用。因此,nanoprintf 仅返回表示格式化字符串包含多少字节的非负值(再次减去空终止符字节)。
nanoprintf 具有以下静态配置标志。
NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS
:设置为0
或1
。启用字段宽度说明符。
NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS
:设置为0
或1
。启用精度说明符。
NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS
:设置为0
或1
。启用浮点说明符。
NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS
:设置为0
或1
。启用超大修饰符。
NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS
:设置为0
或1
。启用二进制说明符。
NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS
:设置为0
或1
。启用%n
进行回写。
NANOPRINTF_VISIBILITY_STATIC
:可选定义。将原型标记为沙盒 nanoprintf static
。
如果未指定配置标志,nanoprintf 将默认为“合理”嵌入值以尝试提供帮助:启用浮点数,但禁用回写、二进制和大型格式化程序。如果显式指定任何配置标志,nanoprintf 要求显式指定所有标志。
如果使用禁用的格式说明符功能,则不会发生转换,而只会打印格式说明符字符串。
nanoprintf 具有以下浮点特定配置定义。
NANOPRINTF_CONVERSION_BUFFER_SIZE
:可选,默认为23
。设置用于存储转换值的字符缓冲区的大小。设置为较大的数字可以打印包含更多字符的浮点数。缓冲区大小包含整数部分、小数部分和小数分隔符,但不包含符号和填充字符。如果该数字不适合缓冲区,则会打印err
。请注意大尺寸,因为转换缓冲区是在堆栈内存上分配的。
NANOPRINTF_CONVERSION_FLOAT_TYPE
:可选,默认为unsigned int
。设置浮点转换算法使用的整数类型,它决定转换的精度。可以设置为任何无符号整数类型,例如uint64_t
或uint8_t
。
默认情况下,npf_snprintf 和 npf_vsnprintf 的行为符合 C 标准:提供的缓冲区将被填充但不会溢出。如果字符串超出缓冲区,则会将空终止符字节写入缓冲区的最后一个字节。如果缓冲区为null
或大小为零,则不会写入任何字节。
如果您定义NANOPRINTF_SNPRINTF_SAFE_EMPTY_STRING_ON_OVERFLOW
并且您的字符串大于缓冲区,则缓冲区的第一个字节将被空终止符字节覆盖。这在本质上与微软的 snprintf_s 类似。
在所有情况下,如果有足够的空间,nanoprintf 将返回将写入缓冲区的字节数。根据 C 标准,该值不考虑空终止符字节。
nanoprintf 仅使用堆栈内存,不使用并发原语,因此在内部它不关心其执行环境。这使得同时从多个执行上下文调用,或者用另一个npf_
调用(例如,ISR 或其他)中断npf_
调用是安全的。如果您将npf_pprintf
与相同的npf_putc
目标同时使用,则需要您确保回调内的正确性。如果您从多个线程npf_snprintf
到同一个缓冲区,则会出现明显的数据竞争。
与printf
一样, nanoprintf
需要以下形式的转换规范字符串:
[flags][field width][.precision][length modifier][conversion specifier]
旗帜
不存在或存在以下多项情况:
0
:用前导零字符填充字段。
-
:将字段中的转换结果左对齐。
+
:有符号转换始终以+
或-
字符开头。
:(空格) 如果第一个转换的字符不是符号,则插入空格字符。
#
:写入额外字符( 0x
表示十六进制, .
表示空浮点数, '0' 表示空八进制等)。
字段宽度(如果启用)
指定转换的总字段宽度的数字,添加填充。如果字段宽度为*
,则从下一个可变参数读取字段宽度。
精度(如果启用)
前缀为.
,一个指定数字或字符串精度的数字。如果精度为*
,则从下一个可变参数读取精度。
长度修饰符
不存在或存在以下多项情况:
h
:使用整型和回写可变参数宽度的short
。
L
:使用long double
作为 float vararg 宽度(注意:然后它将被向下转换为double
)
l
:使用long
、 double
或 Wide 可变参数宽度。
hh
:使用char
来表示整数和回写可变参数宽度。
ll
: (large 说明符) 使用long long
作为整数和回写可变参数宽度。
j
:(大说明符)使用[u]intmax_t
类型作为整数和回写可变参数宽度。
z
:(大说明符)使用size_t
类型作为整数和回写可变参数宽度。
t
:(大说明符)使用ptrdiff_t
类型作为整数和回写可变参数宽度。
转换说明符
正是以下其中一项:
%
:百分号文字
c
:字符
s
:以空字符结尾的字符串
i
/ d
:有符号整数
u
:无符号整数
o
:无符号八进制整数
x
/ X
:无符号十六进制整数
p
:指针
n
:写入写入指针 vararg 的字节数
f
/ F
: 浮点小数
e
/ E
:浮点科学(未实现,打印浮点十进制)
g
/ G
:最短浮点(未实现,打印浮点十进制)
a
/ A
:浮点十六进制(未实现,打印浮点十进制)
b
/ B
: 二进制整数
浮点转换是通过将数字的整数和小数部分提取到两个单独的整数变量中来执行的。对于每个部分,通过迭代地将尾数适当地乘以和除以 2 和 5,将指数从以 2 为底缩放到以 10 为底。动态选择缩放操作的顺序(取决于值)以保留尽可能多的尾数的最高有效位。该值距离小数点分隔符越远,缩放累积的误差就越大。通过平均N
位的转换整数类型宽度,该算法保留N - log2(5)
或N - 2.322
位的精度。此外,小数分隔符后最多2 ^^ N - 1
的整数部分和最多N - 2.322
位的小数部分可以完美转换,不会丢失任何位。
由于 float ->fixed 代码对原始浮点值位进行操作,因此不执行任何浮点操作。这使得 nanoprintf 能够在 Cortex-M0 等软浮点架构上高效地格式化浮点,无论是否经过“快速数学”等优化,其功能都相同,并最大限度地减少代码占用量。
%e
/ %E
、 %a
/ %A
和%g
/ %G
说明符会被解析,但不会被格式化。如果使用,输出将与使用%f
/ %F
时相同。欢迎拉请求! :)
不存在宽字符支持: %lc
和%ls
字段要求将 arg 转换为字符数组,就像调用 wcrtomb 一样。当涉及语言环境和字符集转换时,很难保留“nano”这个名称。因此, %lc
和%ls
行为分别类似于%c
和%s
。
目前唯一支持的浮点转换是小数形式: %f
和%F
。欢迎拉请求!
CI 构建设置为使用 gcc 和 nm 来测量每个拉取请求的编译大小。请参阅最近运行的“提交前检查”“大小报告”作业输出。
以下尺寸测量是针对 Cortex-M0 构建进行的。
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 00000046 00000002 t npf_bufputc_nop 00000048 00000010 t npf_putc_cnt 00000032 00000014 t npf_bufputc 00000270 00000016 T npf_pprintf 000002cc 00000016 T npf_snprintf 00000000 00000032 t npf_utoa_rev 00000286 00000046 T npf_vsnprintf 00000058 00000218 T npf_vpprintf Total size: 0x2e2 (738) 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 00000046 00000002 t npf_bufputc_nop 00000048 00000010 t npf_putc_cnt 00000032 00000014 t npf_bufputc 000002a8 00000016 T npf_pprintf 00000304 00000016 T npf_snprintf 00000000 00000032 t npf_utoa_rev 000002be 00000046 T npf_vsnprintf 00000058 00000250 T npf_vpprintf Total size: 0x31a (794) 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 00000046 00000002 t npf_bufputc_nop 00000048 00000010 t npf_putc_cnt 00000032 00000014 t npf_bufputc 000004fe 00000016 T npf_pprintf 0000055c 00000016 T npf_snprintf 00000000 00000032 t npf_utoa_rev 00000514 00000048 T npf_vsnprintf 00000058 000004a6 T npf_vpprintf Total size: 0x572 (1394) 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 00000046 00000002 t npf_bufputc_nop 00000048 00000010 t npf_putc_cnt 00000032 00000014 t npf_bufputc 00000560 00000016 T npf_pprintf 000005bc 00000016 T npf_snprintf 00000000 00000032 t npf_utoa_rev 00000576 00000046 T npf_vsnprintf 00000058 00000508 T npf_vpprintf Total size: 0x5d2 (1490) 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 00000046 00000002 t npf_bufputc_nop 00000048 00000010 t npf_putc_cnt 00000032 00000014 t npf_bufputc 00000618 00000016 T npf_pprintf 00000674 00000016 T npf_snprintf 00000000 00000032 t npf_utoa_rev 0000062e 00000046 T npf_vsnprintf 00000058 000005c0 T npf_vpprintf Total size: 0x68a (1674) 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 0000005a 00000002 t npf_bufputc_nop 0000005c 00000010 t npf_putc_cnt 00000046 00000014 t npf_bufputc 000009da 00000016 T npf_pprintf 00000a38 00000016 T npf_snprintf 00000000 00000046 t npf_utoa_rev 000009f0 00000048 T npf_vsnprintf 0000006c 0000096e T npf_vpprintf Total size: 0xa4e (2638) bytes
获取环境并运行测试:
克隆或分叉此存储库。
从根目录运行./b
(对于 Windows 用户,从根目录运行py -3 build.py
)
这将为您的主机环境构建所有单元、一致性和编译测试。任何测试失败都将返回非零退出代码。
nanoprintf开发环境使用cmake和ninja。如果您的路径中有这些, ./b
将使用它们。如果没有, ./b
将下载它们并将其部署到path/to/your/nanoprintf/external
中。
nanoprintf 使用 GitHub Actions 进行所有持续集成构建。 GitHub Linux 构建使用我的 Docker 存储库中的这个 Docker 映像。
该矩阵构建 [调试、发布] x [32 位、64 位] x [Mac、Windows、Linux] x [gcc、clang、msvc],减去 32 位 clang Mac 配置。
其中一个测试套件是 printf 测试套件的一个分支,该套件已获得 MIT 许可。它作为许可目的的子模块存在 - nanoprintf 是公共领域,因此这个特定的测试套件是可选的,默认情况下被排除。要构建它,请通过更新子模块来检索它,并将--paland
标志添加到您的./b
调用中。根本不需要使用 nanoprintf。
浮点到整数转换的基本思想受到 Wojciech Muła 的 float -> 64:64 固定算法的启发,并通过 Oskars Rubenis 添加动态缩放和可配置整数宽度进一步扩展。
我将 printf 测试套件移植到 nanoprintf。它最初来自 mpaland printf 项目代码库,但被 Eyal Rozenberg 等人采用和改进。 (Nanoprintf 有很多自己的测试,但这些也非常彻底且非常好!)
二进制实现基于 Jörg Wunsch 的 N2630 提案指定的要求,希望能被 C23 接受!