lib: add cbprintf capability

This commit adds a C99 stdio value formatter capability where
generated text is emitted through a callback.  This allows generation
of arbitrarily long output without a buffer, functionality that is
core to printk, logging, and other system and application needs.

The formatter supports most C99 specifications, excluding:
* %Lf long double conversion
* wide character output

Kconfig options allow disabling features like floating-point
conversion if they are not necessary.  By default most conversions are
enabled.

The original z_vprintk() implementation is adapted to meet the
interface requirements of cbvprintf, and made available as an opt-in
feature for space-constrained applications that do not need full
formatting support.

Signed-off-by: Peter Bigot <peter.bigot@nordicsemi.no>
This commit is contained in:
Peter Bigot 2020-11-08 06:24:22 -06:00 committed by Anas Nashif
commit 33103828dc
9 changed files with 2584 additions and 0 deletions

View file

@ -17,6 +17,7 @@ API Reference
drivers/index.rst
display/index.rst
file_system/index.rst
misc/formatted_output.rst
kernel/index.rst
logging/index.rst
misc/index

View file

@ -0,0 +1,55 @@
.. _formatted_output:
Formatted Output
################
Applications as well as Zephyr itself requires infrastructure to format
values for user consumption. The standard C99 library ``*printf()``
functionality fulfills this need for streaming output devices or memory
buffers, but in an embedded system devices may not accept streamed data
and memory may not be available to store the formatted output.
Internal Zephyr API traditionally provided this both for
:c:func:`printk` and for Zephyr's internal minimal libc, but with
separate internal interfaces. Logging, tracing, shell, and other
applications made use of either these APIs or standard libc routines
based on build options.
The :c:func:`cbprintf` public APIs convert C99 format strings and
arguments, providing output produced one character at a time through a
callback mechanism, replacing the original internal functions and
providing support for almost all C99 format specifications. Existing
use of ``s*printf()`` C libraries in Zephyr can be converted to
:c:func:`snprintfcb()` to avoid pulling in libc implementations.
Several Kconfig options control the set of features that are enabled,
allowing some control over features and memory usage:
* :option:`CONFIG_CBPRINTF_FULL_INTEGRAL`
or :option:`CONFIG_CBPRINTF_REDUCED_INTEGRAL`
* :option:`CONFIG_CBPRINTF_FP_SUPPORT`
* :option:`CONFIG_CBPRINTF_FP_A_SUPPORT`
* :option:`CONFIG_CBPRINTF_FP_ALWAYS_A`
* :option:`CONFIG_CBPRINTF_N_SPECIFIER`
:option:`CONFIG_CBPRINTF_LIBC_SUBSTS` can be used to provide functions
that behave like standard libc functions but use the selected cbprintf
formatter rather than pulling in another formatter from libc.
In addition :option:`CONFIG_CBPRINTF_NANO` can be used to revert back to
the very space-optimized but limited formatter used for :c:func:`printk`
before this capability was added.
.. warning::
If :option:`CONFIG_MINIMAL_LIBC` is selected in combination with
:option:`CONFIG_CBPRINTF_NANO` formatting with C standard library
functions like ``printf`` or ``snprintf`` is limited. Among other
things the ``%n`` specifier, most format flags, precision control, and
floating point are not supported.
API Reference
*************
.. doxygengroup:: cbprintf_apis
:project: Zephyr

177
include/sys/cbprintf.h Normal file
View file

@ -0,0 +1,177 @@
/*
* Copyright (c) 2020 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef ZEPHYR_INCLUDE_SYS_CBPRINTF_H_
#define ZEPHYR_INCLUDE_SYS_CBPRINTF_H_
#include <stdarg.h>
#include <stddef.h>
#include <toolchain.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @defgroup cbprintf_apis Formatted Output APIs
* @ingroup support_apis
* @{
*/
/** @brief Signature for a cbprintf callback function.
*
* This function expects two parameters:
*
* * @p c a character to output. The output behavior should be as if
* this was cast to an unsigned char.
* * @p ctx a pointer to an object that provides context for the
* output operation.
*
* The declaration does not specify the parameter types. This allows a
* function like @c fputc to be used without requiring all context pointers to
* be to a @c FILE object.
*
* @return the value of @p c cast to an unsigned char then back to
* int, or a negative error code that will be returned from
* cbprintf().
*/
typedef int (*cbprintf_cb)(/* int c, void *ctx */);
/** @brief *printf-like output through a callback.
*
* This is essentially printf() except the output is generated
* character-by-character using the provided @p out function. This allows
* formatting text of unbounded length without incurring the cost of a
* temporary buffer.
*
* All formatting specifiers of C99 are recognized, and most are supported if
* the functionality is enabled.
*
* @note The functionality of this function is significantly reduced
* when `CONFIG_CBPRINTF_NANO` is selected.
*
* @param out the function used to emit each generated character.
*
* @param ctx context provided when invoking out
*
* @param format a standard ISO C format string with characters and conversion
* specifications.
*
* @param ... arguments corresponding to the conversion specifications found
* within @p format.
*
* @return the number of characters printed, or a negative error value
* returned from invoking @p out.
*/
__printf_like(3, 4)
int cbprintf(cbprintf_cb out, void *ctx, const char *format, ...);
/** @brief Calculate the number of words required for arguments to a cbprintf
* format specification.
*
* This can be used in cases where the arguments must be copied off the stack
* into separate storage for processing the conversion in another context.
*
* @note The length returned does not count bytes. It counts native words
* defined as the size of an `int`.
*
* @note If `CONFIG_CBPRINTF_NANO` is selected the count will be incorrect if
* any passed arguments require more than one `int`.
*
* @param format a standard ISO C format string with characters and conversion
* specifications.
*
* @return the number of `int` elements required to provide all arguments
* required for the conversion.
*/
size_t cbprintf_arglen(const char *format);
/** @brief varargs-aware *printf-like output through a callback.
*
* This is essentially vsprintf() except the output is generated
* character-by-character using the provided @p out function. This allows
* formatting text of unbounded length without incurring the cost of a
* temporary buffer.
*
* @note This function is available only when `CONFIG_CBPRINTF_LIBC_SUBSTS` is
* selected.
*
* @note The functionality of this function is significantly reduced when
* `CONFIG_CBPRINTF_NANO` is selected.
*
* @param out the function used to emit each generated character.
*
* @param ctx context provided when invoking out
*
* @param format a standard ISO C format string with characters and conversion
* specifications.
*
* @param ap a reference to the values to be converted.
*
* @return the number of characters generated, or a negative error value
* returned from invoking @p out.
*/
int cbvprintf(cbprintf_cb out, void *ctx, const char *format, va_list ap);
/** @brief snprintf using Zephyrs cbprintf infrastructure.
*
* @note The functionality of this function is significantly reduced
* when `CONFIG_CBPRINTF_NANO` is selected.
*
* @param str where the formatted content should be written
*
* @param size maximum number of chaacters for the formatted output,
* including the terminating null byte.
*
* @param format a standard ISO C format string with characters and
* conversion specifications.
*
* @param ... arguments corresponding to the conversion specifications found
* within @p format.
*
* return The number of characters that would have been written to @p
* str, excluding the terminating null byte. This is greater than the
* number actually written if @p size is too small.
*/
__printf_like(3, 4)
int snprintfcb(char *str, size_t size, const char *format, ...);
/** @brief vsnprintf using Zephyrs cbprintf infrastructure.
*
* @note This function is available only when `CONFIG_CBPRINTF_LIBC_SUBSTS` is
* selected.
*
* @note The functionality of this function is significantly reduced when
* `CONFIG_CBPRINTF_NANO` is selected.
*
* @param str where the formatted content should be written
*
* @param size maximum number of chaacters for the formatted output, including
* the terminating null byte.
*
* @param format a standard ISO C format string with characters and conversion
* specifications.
*
* @param ... arguments corresponding to the conversion specifications found
* within @p format.
*
* @param ap a reference to the values to be converted.
*
* return The number of characters that would have been written to @p
* str, excluding the terminating null byte. This is greater than the
* number actually written if @p size is too small.
*/
int vsnprintfcb(char *str, size_t size, const char *format, va_list ap);
/**
* @}
*/
#ifdef __cplusplus
}
#endif
#endif /* ZEPHYR_INCLUDE_SYS_CBPRINTF_H_ */

View file

@ -3,6 +3,7 @@
zephyr_sources_ifdef(CONFIG_BASE64 base64.c)
zephyr_sources(
cbprintf.c
crc32_sw.c
crc16_sw.c
crc8_sw.c
@ -23,6 +24,9 @@ zephyr_sources(
heap-validate.c
)
zephyr_sources_ifdef(CONFIG_CBPRINTF_COMPLETE cbprintf_complete.c)
zephyr_sources_ifdef(CONFIG_CBPRINTF_NANO cbprintf_nano.c)
zephyr_sources_ifdef(CONFIG_JSON_LIBRARY json.c)
zephyr_sources_ifdef(CONFIG_RING_BUFFER ring_buffer.c)

View file

@ -72,4 +72,6 @@ config PRINTK_SYNC
interleaving with concurrent usage from another CPU or an
preempting interrupt.
rsource "Kconfig.cbprintf"
endmenu

117
lib/os/Kconfig.cbprintf Normal file
View file

@ -0,0 +1,117 @@
# Copyright (c) 2020 Nordic Semiconductor ASA
# SPDX-License-Identifier: Apache-2.0
choice CBPRINTF_IMPLEMENTATION
prompt "Capabilities of cbprintf implementation"
default CBPRINTF_COMPLETE
config CBPRINTF_COMPLETE
bool "All selected features"
help
Select this for an implementation that supports all potential
conversions, with Kconfig options to control availability at build
time.
# 80: -53% / 982 B (80 / 00)
config CBPRINTF_NANO
bool "Space-optimized but feature-limited"
# nano needs to count characters if it's the formatter for libc
select CBPRINTF_LIBC_SUBSTS if MINIMAL_LIBC
help
If selected a completely different implementation of the core
formatting capability is substituted. This has a much smaller code
footprint, but provides fewer capabilities.
endchoice # CBPRINTF_IMPLEMENTATION
choice CBPRINTF_INTEGRAL_CONV
prompt "Control range of convertible integer values"
default CBPRINTF_FULL_INTEGRAL
# 01: 0% / 0 B (01 / 00)
config CBPRINTF_FULL_INTEGRAL
bool "Convert the full range of integer values"
help
Build cbprintf with buffers sized to support converting the full
range of all integral and pointer values.
Selecting this has no effect on code size, but will increase call
stack size by a few words.
# 00:
config CBPRINTF_REDUCED_INTEGRAL
bool "Convert only integer values that fit in 32 bits"
help
Build cbprintf with buffers sized to support converting integer
values with no more than 32 bits.
This will decrease stack space, but affects conversion of any type
with more than 32 bits. This includes not only intmax_t but any
type that can be converted to an integral represention including
size_t and pointers.
With CBPRINTF_COMPLETE conversions that may result in value-specific
truncation are not supported, and the generated text will be the
specification (e.g. %jd).
With CBPRINTF_NANO all conversions will be attempted but values that
cannot fit will be silently truncated.
endchoice
# 02: 82% / 1530 B (02 / 00)
config CBPRINTF_FP_SUPPORT
bool "Enable floating point formatting in cbprintf"
default y if FPU
depends on CBPRINTF_COMPLETE
help
Build the cbprintf utility function with support for floating
point format specifiers. Selecting this increases stack size
requirements slightly, but increases code size significantly.
# 04: 13% / 456 B (07 / 03)
config CBPRINTF_FP_A_SUPPORT
bool "Enable floating point %a conversions"
depends on CBPRINTF_FULL_INTEGRAL
select CBPRINTF_FP_SUPPORT
help
The %a hexadecimal format for floating point value conversion was
added in C99, but the output is not easily understood so it rarely
appears in application code.
Selecting this adds support for the conversion, but increases the
overall code size related to FP support.
# 40: -15% / -508 B (46 / 06)
config CBPRINTF_FP_ALWAYS_A
bool "Select %a format for all floating point specifications"
select CBPRINTF_FP_A_SUPPORT
help
The %a format for floats requires significantly less code than the
standard decimal representations (%f, %e, %g). Selecting this
option implicitly uses %a (or %A) for all decimal floating
conversions. The precision of the original specification is
ignored.
Selecting this decreases code size when FP_SUPPORT is enabled.
# 08: 3% / 60 B (08 / 00)
config CBPRINTF_N_SPECIFIER
bool "Support %n specifications"
depends on CBPRINTF_COMPLETE
default y
help
If selected %n can be used to determine the number of characters
emitted. If enabled there is a small increase in code size.
# 180: 18% / 138 B (180 / 80) [NANO]
config CBPRINTF_LIBC_SUBSTS
bool "Generate C-library compatible functions using cbprintf"
help
If selected wrappers are generated for various C library functions
using the cbprintf formatter underneath. The wrappers use the C
library function name with a cb suffix; e.g. printfcb() or
vsnprintfcb().
When used with CBPRINTF_NANO this increases the implementation code
size by a small amount.

78
lib/os/cbprintf.c Normal file
View file

@ -0,0 +1,78 @@
/*
* Copyright (c) 2020 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdarg.h>
#include <stddef.h>
#include <sys/cbprintf.h>
int cbprintf(cbprintf_cb out, void *ctx, const char *format, ...)
{
va_list ap;
int rc;
va_start(ap, format);
rc = cbvprintf(out, ctx, format, ap);
va_end(ap);
return rc;
}
#if defined(CONFIG_CBPRINTF_LIBC_SUBSTS)
/* Context for sn* variants is the next space in the buffer, and the buffer
* end.
*/
struct str_ctx {
char *dp;
char *const dpe;
};
static int str_out(int c,
void *ctx)
{
struct str_ctx *scp = ctx;
/* s*printf must return the number of characters that would be
* output, even if they don't all fit, so conditionally store
* and unconditionally succeed.
*/
if (scp->dp < scp->dpe) {
*(scp->dp++) = c;
}
return c;
}
int snprintfcb(char *str, size_t size, const char *format, ...)
{
va_list ap;
int rc;
va_start(ap, format);
rc = vsnprintfcb(str, size, format, ap);
va_end(ap);
return rc;
}
int vsnprintfcb(char *str, size_t size, const char *format, va_list ap)
{
struct str_ctx ctx = {
.dp = str,
.dpe = str + size,
};
int rv = cbvprintf(str_out, &ctx, format, ap);
if (ctx.dp < ctx.dpe) {
ctx.dp[0] = 0;
} else {
ctx.dp[-1] = 0;
}
return rv;
}
#endif /* CONFIG_CBPRINTF_LIBC_SUBSTS */

1824
lib/os/cbprintf_complete.c Normal file

File diff suppressed because it is too large Load diff

326
lib/os/cbprintf_nano.c Normal file
View file

@ -0,0 +1,326 @@
/*
* Copyright (c) 2010, 2013-2014 Wind River Systems, Inc.
* Copyright (c) 2020 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdarg.h>
#include <sys/cbprintf.h>
#include <toolchain.h>
#include <linker/sections.h>
#include <syscall_handler.h>
#include <logging/log.h>
#include <sys/types.h>
enum pad_type {
PAD_NONE,
PAD_ZERO_BEFORE,
PAD_SPACE_BEFORE,
PAD_SPACE_AFTER,
};
#ifdef CONFIG_CBPRINTF_FULL_INTEGRAL
typedef intmax_t int_value_type;
typedef uintmax_t uint_value_type;
#else
typedef int32_t int_value_type;
typedef uint32_t uint_value_type;
#endif
/* Maximum number of digits in a printed decimal value (hex is always
* less, obviously). Funny formula produces 10 max digits for 32 bit,
* 21 for 64. It may be incorrect for other value lengths.
*/
#define DIGITS_BUFLEN (11U * (sizeof(uint_value_type) / 4U) - 1U)
BUILD_ASSERT(sizeof(uint_value_type) <= 8U,
"DIGITS_BUFLEN formula may be incorrect");
static void print_digits(cbprintf_cb out, void *ctx, uint_value_type num,
unsigned int base, bool pad_before, char pad_char,
int min_width, size_t *countp)
{
size_t count = 0;
char buf[DIGITS_BUFLEN];
unsigned int i;
/* Print it backwards into the end of the buffer, low digits first */
for (i = DIGITS_BUFLEN - 1U; num != 0U; i--) {
buf[i] = "0123456789abcdef"[num % base];
num /= base;
}
if (i == DIGITS_BUFLEN - 1U) {
buf[i] = '0';
} else {
i++;
}
int pad = MAX(min_width - (int)(DIGITS_BUFLEN - i), 0);
for (/**/; pad > 0 && pad_before; pad--) {
out(pad_char, ctx);
if (IS_ENABLED(CONFIG_CBPRINTF_LIBC_SUBSTS)) {
++count;
}
}
for (/**/; i < DIGITS_BUFLEN; i++) {
out(buf[i], ctx);
if (IS_ENABLED(CONFIG_CBPRINTF_LIBC_SUBSTS)) {
++count;
}
}
for (/**/; pad > 0; pad--) {
out(pad_char, ctx);
if (IS_ENABLED(CONFIG_CBPRINTF_LIBC_SUBSTS)) {
++count;
}
}
if (IS_ENABLED(CONFIG_CBPRINTF_LIBC_SUBSTS)) {
*countp += count;
}
}
static void print_hex(cbprintf_cb out, void *ctx, uint_value_type num,
enum pad_type padding, int min_width, size_t *count)
{
print_digits(out, ctx, num, 16U, padding != PAD_SPACE_AFTER,
padding == PAD_ZERO_BEFORE ? '0' : ' ', min_width,
count);
}
static void print_dec(cbprintf_cb out, void *ctx, uint_value_type num,
enum pad_type padding, int min_width, size_t *count)
{
print_digits(out, ctx, num, 10U, padding != PAD_SPACE_AFTER,
padding == PAD_ZERO_BEFORE ? '0' : ' ', min_width,
count);
}
static bool ok64(cbprintf_cb out, void *ctx, long long val, size_t *count)
{
if (sizeof(int_value_type) < 8U && val != (int_value_type) val) {
out('E', ctx);
out('R', ctx);
out('R', ctx);
if (IS_ENABLED(CONFIG_CBPRINTF_LIBC_SUBSTS)) {
*count += 3;
}
return false;
}
return true;
}
static bool negative(uint_value_type val)
{
const uint_value_type hibit = ~(((uint_value_type) ~1) >> 1U);
return (val & hibit) != 0U;
}
/**
* @brief Printk internals
*
* See printk() for description.
* @param fmt Format string
* @param ap Variable parameters
*
* @return N/A
*/
int cbvprintf(cbprintf_cb out, void *ctx, const char *fmt, va_list ap)
{
size_t count = 0;
int might_format = 0; /* 1 if encountered a '%' */
enum pad_type padding = PAD_NONE;
int min_width = -1;
char length_mod = 0;
#define OUTC(_c) do { \
out((int)(_c), ctx); \
if (IS_ENABLED(CONFIG_CBPRINTF_LIBC_SUBSTS)) { \
++count; \
} \
} while (false)
/* fmt has already been adjusted if needed */
while (*fmt) {
if (!might_format) {
if (*fmt != '%') {
OUTC(*fmt);
} else {
might_format = 1;
min_width = -1;
padding = PAD_NONE;
length_mod = 0;
}
} else {
switch (*fmt) {
case '-':
padding = PAD_SPACE_AFTER;
goto still_might_format;
case '0':
if (min_width < 0 && padding == PAD_NONE) {
padding = PAD_ZERO_BEFORE;
goto still_might_format;
}
__fallthrough;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
if (min_width < 0) {
min_width = *fmt - '0';
} else {
min_width = 10 * min_width + *fmt - '0';
}
if (padding == PAD_NONE) {
padding = PAD_SPACE_BEFORE;
}
goto still_might_format;
case 'h':
case 'l':
case 'z':
if (*fmt == 'h' && length_mod == 'h') {
length_mod = 'H';
} else if (*fmt == 'l' && length_mod == 'l') {
length_mod = 'L';
} else if (length_mod == 0) {
length_mod = *fmt;
} else {
OUTC('%');
OUTC(*fmt);
break;
}
goto still_might_format;
case 'd':
case 'i':
case 'u': {
uint_value_type d;
if (length_mod == 'z') {
d = va_arg(ap, ssize_t);
} else if (length_mod == 'l') {
d = va_arg(ap, long);
} else if (length_mod == 'L') {
long long lld = va_arg(ap,
long long);
if (!ok64(out, ctx, lld, &count)) {
break;
}
d = (uint_value_type) lld;
} else if (*fmt == 'u') {
d = va_arg(ap, unsigned int);
} else {
d = va_arg(ap, int);
}
if (*fmt != 'u' && negative(d)) {
OUTC('-');
d = -d;
min_width--;
}
print_dec(out, ctx, d, padding, min_width,
&count);
break;
}
case 'p':
case 'x':
case 'X': {
uint_value_type x;
if (*fmt == 'p') {
const char *cp;
x = (uintptr_t)va_arg(ap, void *);
if (x == 0) {
cp = "(nil)";
} else {
cp = "0x";
}
while (*cp) {
OUTC(*cp++);
}
if (x == 0) {
break;
}
} else if (length_mod == 'l') {
x = va_arg(ap, unsigned long);
} else if (length_mod == 'L') {
x = va_arg(ap, unsigned long long);
} else {
x = va_arg(ap, unsigned int);
}
print_hex(out, ctx, x, padding, min_width,
&count);
break;
}
case 's': {
char *s = va_arg(ap, char *);
char *start = s;
while (*s) {
OUTC(*s++);
}
if (padding == PAD_SPACE_AFTER) {
int remaining = min_width - (s - start);
while (remaining-- > 0) {
OUTC(' ');
}
}
break;
}
case 'c': {
int c = va_arg(ap, int);
OUTC(c);
break;
}
case '%': {
OUTC('%');
break;
}
default:
OUTC('%');
OUTC(*fmt);
break;
}
might_format = 0;
}
still_might_format:
++fmt;
}
return count;
}
size_t cbprintf_arglen(const char *format)
{
size_t rv = 0;
bool last_pct = false;
while (*format != 0) {
if (*format == '%') {
last_pct = !last_pct;
} else if (last_pct) {
++rv;
last_pct = false;
} else {
}
++format;
}
return rv;
}