格式化输出
应用程序以及 Zephyr 本身都需要基础设施来将数值格式化为用户可读的形式。标准 C99 库的 *printf() 功能可以满足流式输出设备或内存缓冲区的需求,但在嵌入式系统中,设备可能不接受流式数据,也可能没有足够的内存来存放格式化输出。
Zephyr 内部 API 过去同时为 printk() 和 Zephyr 内部的最小化 libc 提供该功能,但两者使用各自独立的内部接口。日志、跟踪、shell 及其他应用会根据构建选项选用这些 API 或标准 libc 例程。
cbprintf() 公共 API 负责转换 C99 格式字符串和参数,并通过回调机制逐字符产生输出,取代了原先的内部函数,同时支持几乎所有 C99 格式说明符。Zephyr 中原有对 s*printf() C 库的使用可以改为 snprintfcb(),从而避免引入 libc 实现。
有若干 Kconfig 选项控制所启用的特性集合,从而可以在特性和内存占用之间进行一定程度的取舍:
CONFIG_CBPRINTF_LIBC_SUBSTS 可用于提供一组函数,其行为与标准 libc 函数相同,但使用选定的 cbprintf 格式化器,而不会从 libc 引入另一个格式化器。
此外,还可以使用 CONFIG_CBPRINTF_NANO,回退到本功能加入之前用于 printk() 的极省空间但功能有限的格式化器。
Cbprintf 打包
通常,调用 printf 系列函数时会同步完成字符串格式化。但在某些情况下,延迟格式化会更有优势。此时必须捕获一个状态(格式字符串和参数)。该状态构成一个自包含的包,其中包含格式字符串和参数。此外,包中还可以包含格式字符串中各个字符串的副本(格式字符串本身或任意 %s 参数)。包的主要内容类似于 va_list 栈帧,因此使用标准格式化函数来处理包。由于包中包含按 va_list 帧处理的数据,必须保持严格的对齐。受所需填充的影响,包的大小取决于对齐方式。复制包时,应将其复制到与原位置具有相同对齐方式的内存块中。
包可以有以下几种变体:
自包含 - 非只读字符串追加到包中。只要可以访问只读字符串的位置,就能从这样的包中格式化出字符串。包中可以包含只读字符串在包内位置的信息。该信息可用于把包转换为完全自包含的包。
完全自包含 - 所有字符串都追加到包中。无需任何外部数据即可从这样的包中格式化出字符串。
瞬态 - 只存储参数。包中包含非只读字符串的指针在包内位置的信息。也可以选择包含只读字符串的位置信息。只要非只读字符串仍然有效且只读字符串可以访问,就能从这样的包中格式化出字符串。另外,如果包中包含只读字符串位置的信息,还可以把包转换为 自包含 包或 完全自包含 包。
可以使用两种方法创建包:
运行时 - 使用
cbprintf_package()或cbvprintf_package()。此方法扫描格式字符串,并根据检测到的格式说明符构建包。静态 - 参数类型由预处理器在编译期检测,包以对给定内存的简单赋值方式创建。此方法比运行时方法快得多(15 倍以上),但有一个明显的限制:如果使用 char 指针,它无法区分
%p和%s。它把所有(无符号)char 指针都当作%s,因此会尝试把字符串追加到包中。在使用CBPRINTF_PACKAGE_CONVERT_PTR_CHECK标志把 瞬态 包转换为 自包含 包时,可以正确处理这种情况。但这需要访问格式字符串,而格式字符串并非总是可以访问,因此建议为%p使用的 char 指针强制转换为void *。当 char 指针与%p一起使用时,带CBPRINTF_PACKAGE_CONVERT_PTR_CHECK标志调用的cbprintf_package_convert()会产生一条日志警告。
有若干 Kconfig 选项控制打包行为:
Cbprintf 包转换
可以把包转换为包含更多信息的变体,例如可以把 瞬态 包转换为 自包含 包。如果在创建包时使用了 CBPRINTF_PACKAGE_ADD_RO_STR_POS 标志,还可以转换为 完全自包含 包。
cbprintf_package_copy() 用于计算新包所需的空间,并复制和转换包。
Cbprintf 包格式
包的格式中包含与平台相关的填充。包由一个头部组成,其中存放包的大小(不含追加的字符串)和追加字符串的数量。其后是参数部分,其中包含对齐填充,类似于 va_list 栈帧。再后面是与字符串使用的字符指针参数相关联的数据,这些字符串没有追加到包中(但之后可能由 cbprinf_package_convert() 追加)。最后,包中可选地包含追加的字符串。每个字符串前都有 1 字节的头部,其中存放地址参数所在位置的索引。打包期间该地址被设为 null,在字符串格式化之前会被更新为指向包内当前字符串的位置。由于每次复制包时该地址都会变化,更新地址参数必须紧接在字符串格式化之前进行。
头部 sizeof(void *) |
1 字节:参数列表大小,包含头部和 fmt (以 32 位字为单位) |
1 字节:追加到包中的字符串数量 |
|
1 字节:只读字符串参数位置的数量 |
|
1 字节:瞬态字符串参数位置的数量 |
|
平台相关的填充,补齐到 sizeof(void *) |
|
参数 |
指向 fmt 的指针(如果 fmt 被追加到包中则为 null) |
(可选的填充,用于平台相关的对齐) |
|
参数 0 |
|
(可选的填充,用于平台相关的对齐) |
|
参数 1 |
|
... |
|
字符串位置信息(可选) |
包内只读字符串所在位置的字索引 |
瞬态字符串所在位置的参数索引与参数位置索引对 |
|
追加的字符串(可选) |
1 字节:包内指向关联参数位置的索引 |
以 null 结尾的字符串 |
|
... |
警告
如果同时选择 CONFIG_MINIMAL_LIBC 和 CONFIG_CBPRINTF_NANO,则使用 printf 或 snprintf 等 C 标准库函数进行格式化时会受到限制。除其他限制外,不支持 %n 说明符、大多数格式标志、精度控制以及浮点数。
限制与建议
建议为
%p格式说明符使用的字符指针强制转换为其他指针类型(例如void *)。如果无法访问格式字符串,则只能进行静态打包,此时会追加所有检测到的字符串。用于%p的字符指针会被视为字符串指针。从非预期位置复制数据可能造成严重后果(例如内存故障或安全违规)。