lib: cbprintf: add support for deferred formatting

In applications like logging the call site where arguments to
formatting are available may not be suitable for performing the
formatting, e.g. when the output operation can sleep.  Add API that
supports capturing data that may be transient into a buffer that can
be saved, and API that then produces the output later using the
packaged arguments.

[ Documentation and commit log from Peter Bigot. ]

Signed-off-by: Nicolas Pitre <npitre@baylibre.com>
Signed-off-by: Peter Bigot <peter.bigot@nordicsemi.no>
This commit is contained in:
Nicolas Pitre 2021-03-02 23:14:24 -05:00 committed by Carles Cufí
parent 4118ed1d4d
commit 14e5e98822
4 changed files with 673 additions and 0 deletions

View File

@ -464,6 +464,7 @@
/lib/gui/ @vanwinkeljan
/lib/open-amp/ @arnopo
/lib/os/ @dcpleung @nashif @andyross
/lib/os/cbprintf_packaged.c @npitre
/lib/posix/ @pfalcon
/lib/cmsis_rtos_v2/ @nashif
/lib/cmsis_rtos_v1/ @nashif

View File

@ -9,6 +9,7 @@
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <toolchain.h>
#ifdef CONFIG_CBPRINTF_LIBC_SUBSTS
@ -44,6 +45,102 @@ extern "C" {
*/
typedef int (*cbprintf_cb)(/* int c, void *ctx */);
/** @brief Capture state required to output formatted data later.
*
* Like cbprintf() but instead of processing the arguments and emitting the
* formatted results immediately all arguments are captured so this can be
* done in a different context, e.g. when the output function can block.
*
* In addition to the values extracted from arguments this will ensure that
* copies are made of the necessary portions of any string parameters that are
* not confirmed to be stored in read-only memory (hence assumed to be safe to
* refer to directly later).
*
* @param packaged pointer to where the packaged data can be stored. Pass a
* null pointer to store nothing but still calculate the total space required.
* The data stored here is relocatable, that is it can be moved to another
* contiguous block of memory. The pointer must be aligned to a multiple of
* the largest element in the argument list.
*
* @param len this must be set to the number of bytes available at @p packaged.
* Ignored if @p packaged is NULL.
*
* @param format a standard ISO C format string with characters and conversion
* specifications.
*
* @param ... arguments corresponding to the conversion specifications found
* within @p format.
*
* @retval nonegative the number of bytes successfully stored at @p packaged.
* This will not exceed @p len.
* @retval -EINVAL if @p format is not acceptable
* @retval -ENOSPC if @p packaged was not null and the space required to store
* exceed @p len.
*/
__printf_like(3, 4)
int cbprintf_package(void *packaged,
size_t len,
const char *format,
...);
/** @brief Capture state required to output formatted data later.
*
* Like cbprintf() but instead of processing the arguments and emitting the
* formatted results immediately all arguments are captured so this can be
* done in a different context, e.g. when the output function can block.
*
* In addition to the values extracted from arguments this will ensure that
* copies are made of the necessary portions of any string parameters that are
* not confirmed to be stored in read-only memory (hence assumed to be safe to
* refer to directly later).
*
* @param packaged pointer to where the packaged data can be stored. Pass a
* null pointer to store nothing but still calculate the total space required.
* The data stored here is relocatable, that is it can be moved to another
* contiguous block of memory. The pointer must be aligned to a multiple of
* the largest element in the argument list.
*
* @param len this must be set to the number of bytes available at @p packaged.
* Ignored if @p packaged is NULL.
*
* @param format a standard ISO C format string with characters and conversion
* specifications.
*
* @param ap captured stack arguments corresponding to the conversion
* specifications found within @p format.
*
* @retval nonegative the number of bytes successfully stored at @p packaged.
* This will not exceed @p len.
* @retval -EINVAL if @p format is not acceptable
* @retval -ENOSPC if @p packaged was not null and the space required to store
* exceed @p len.
*/
int cbvprintf_package(void *packaged,
size_t len,
const char *format,
va_list ap);
/** @brief Generate the output for a previously captured format
* operation.
*
* @param out the function used to emit each generated character.
*
* @param ctx context provided when invoking out
*
* @param packaged the data required to generate the formatted output, as
* captured by cbprintf_package() or cbvprintf_package(). The alignment
* requirement on this data is the same as when it was initially created.
*
* @note Memory indicated by @p packaged will be modified in a non-destructive
* way, meaning that it could still be reused with this function again.
*
* @return the number of characters printed, or a negative error value
* returned from invoking @p out.
*/
int cbpprintf(cbprintf_cb out,
void *ctx,
void *packaged);
/** @brief *printf-like output through a callback.
*
* This is essentially printf() except the output is generated

View File

@ -4,6 +4,7 @@ zephyr_sources_ifdef(CONFIG_BASE64 base64.c)
zephyr_sources(
cbprintf.c
cbprintf_packaged.c
crc32c_sw.c
crc32_sw.c
crc16_sw.c

574
lib/os/cbprintf_packaged.c Normal file
View File

@ -0,0 +1,574 @@
/*
* Copyright (c) 2021 BayLibre, SAS
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <errno.h>
#include <stdarg.h>
#include <stdint.h>
#include <string.h>
#include <sys/cbprintf.h>
#include <sys/types.h>
#include <sys/util.h>
#include <sys/__assert.h>
/**
* @brief Check if address is in read only section.
*
* @param addr Address.
*
* @return True if address identified within read only section.
*/
static inline bool ptr_in_rodata(const char *addr)
{
#if defined(CBPRINTF_VIA_UNIT_TEST)
/* Unit test is X86 (or other host) but not using Zephyr
* linker scripts.
*/
#define RO_START 0
#define RO_END 0
#elif defined(CONFIG_ARC) || defined(CONFIG_ARM) || defined(CONFIG_X86) \
|| defined(CONFIG_RISCV)
extern char _image_rodata_start[];
extern char _image_rodata_end[];
#define RO_START _image_rodata_start
#define RO_END _image_rodata_end
#elif defined(CONFIG_NIOS2) || defined(CONFIG_RISCV)
extern char _image_rom_start[];
extern char _image_rom_end[];
#define RO_START _image_rom_start
#define RO_END _image_rom_end
#elif defined(CONFIG_XTENSA)
extern char _rodata_start[];
extern char _rodata_end[];
#define RO_START _rodata_start
#define RO_END _rodata_end
#else
#define RO_START 0
#define RO_END 0
#endif
return ((addr >= (const char *)RO_START) &&
(addr < (const char *)RO_END));
}
/*
* va_list creation
*/
#if defined(__aarch64__)
/*
* Reference:
*
* Procedure Call Standard for the ARM 64-bit Architecture
*/
struct __va_list {
void *__stack;
void *__gr_top;
void *__vr_top;
int __gr_offs;
int __vr_offs;
};
BUILD_ASSERT(sizeof(va_list) == sizeof(struct __va_list),
"architecture specific support is wrong");
static int cbprintf_via_va_list(cbprintf_cb out, void *ctx,
const char *fmt, void *buf)
{
union {
va_list ap;
struct __va_list __ap;
} u;
/* create a valid va_list with our buffer */
u.__ap.__stack = buf;
u.__ap.__gr_top = NULL;
u.__ap.__vr_top = NULL;
u.__ap.__gr_offs = 0;
u.__ap.__vr_offs = 0;
return cbvprintf(out, ctx, fmt, u.ap);
}
#define VA_STACK_MIN_ALIGN 8
#elif defined(__x86_64__)
/*
* Reference:
*
* System V Application Binary Interface
* AMD64 Architecture Processor Supplement
*/
struct __va_list {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
};
BUILD_ASSERT(sizeof(va_list) == sizeof(struct __va_list),
"architecture specific support is wrong");
static int cbprintf_via_va_list(cbprintf_cb out, void *ctx,
const char *fmt, void *buf)
{
union {
va_list ap;
struct __va_list __ap;
} u;
/* create a valid va_list with our buffer */
u.__ap.overflow_arg_area = buf;
u.__ap.reg_save_area = NULL;
u.__ap.gp_offset = (6 * 8);
u.__ap.fp_offset = (6 * 8 + 16 * 16);
return cbvprintf(out, ctx, fmt, u.ap);
}
#define VA_STACK_MIN_ALIGN 8
#elif defined(__xtensa__)
/*
* Reference:
*
* gcc source code (gcc/config/xtensa/xtensa.c)
* xtensa_build_builtin_va_list(), xtensa_va_start(),
* xtensa_gimplify_va_arg_expr()
*/
struct __va_list {
void *__va_stk;
void *__va_reg;
int __va_ndx;
};
BUILD_ASSERT(sizeof(va_list) == sizeof(struct __va_list),
"architecture specific support is wrong");
static int cbprintf_via_va_list(cbprintf_cb out, void *ctx,
const char *fmt, void *buf)
{
union {
va_list ap;
struct __va_list __ap;
} u;
/* create a valid va_list with our buffer */
u.__ap.__va_stk = (char *)buf - 32;
u.__ap.__va_reg = NULL;
u.__ap.__va_ndx = (6 + 2) * 4;
return cbvprintf(out, ctx, fmt, u.ap);
}
#else
/*
* Default implementation shared by many architectures like
* 32-bit ARM and Intel.
*
* We assume va_list is a simple pointer.
*/
BUILD_ASSERT(sizeof(va_list) == sizeof(void *),
"architecture specific support is needed");
static int cbprintf_via_va_list(cbprintf_cb out, void *ctx,
const char *fmt, void *buf)
{
union {
va_list ap;
void *ptr;
} u;
u.ptr = buf;
return cbvprintf(out, ctx, fmt, u.ap);
}
#endif
/*
* Special alignment cases
*/
#if defined(__i386__)
/* there are no gaps on the stack */
#define VA_STACK_ALIGN(type) 1
#endif
#if defined(__riscv)
#define VA_STACK_MIN_ALIGN (__riscv_xlen / 8)
#endif
#if defined(__sparc__)
/* no gaps on the stack even though the CPU can't do unaligned accesses */
#define VA_STACK_ALIGN(type) 1
#define VA_STACK_LL_DBL_MEMCPY true
#endif
/*
* Default alignment values if not specified by architecture config
*/
#ifndef VA_STACK_MIN_ALIGN
#define VA_STACK_MIN_ALIGN 1
#endif
#ifndef VA_STACK_ALIGN
#define VA_STACK_ALIGN(type) MAX(VA_STACK_MIN_ALIGN, __alignof__(type))
#endif
#ifndef VA_STACK_LL_DBL_MEMCPY
#define VA_STACK_LL_DBL_MEMCPY false
#endif
int cbvprintf_package(void *packaged, size_t len,
const char *fmt, va_list ap)
{
char *buf = packaged, *buf0 = buf;
unsigned int align, size, i, s_idx = 0;
uint8_t str_ptr_pos[16];
const char *s;
bool parsing = false;
/*
* Make room to store the arg list size and the number of
* appended strings. They both occupy 1 byte each.
*
* Given the next value to store is the format string pointer
* which is guaranteed to be at least 4 bytes, we just reserve
* a pointer size for the above to preserve alignment.
*/
buf += sizeof(char *);
/*
* When buf0 is NULL we don't store anything.
* Instead we count the needed space to store the data.
*/
if (!buf0) {
len = 0;
}
/*
* Otherwise we must ensure we can store at least
* thepointer to the format string itself.
*/
if (buf0 && buf - buf0 + sizeof(char *) > len) {
return -ENOSPC;
}
/*
* Then process the format string itself.
* Here we branch directly into the code processing strings
* which is in the middle of the following while() loop. That's the
* reason for the post-decrement on fmt as it will be incremented
* prior to the next (actually first) round of that loop.
*/
s = fmt--;
align = VA_STACK_ALIGN(char *);
size = sizeof(char *);
goto process_string;
/* Scan the format string */
while (*++fmt) {
if (!parsing) {
if (*fmt == '%') {
parsing = true;
align = VA_STACK_ALIGN(int);
size = sizeof(int);
}
continue;
}
switch (*fmt) {
case '%':
parsing = false;
continue;
case '#':
case '-':
case '+':
case ' ':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '.':
case 'h':
case 'l':
case 'L':
continue;
case '*':
break;
case 'j':
align = VA_STACK_ALIGN(intmax_t);
size = sizeof(intmax_t);
continue;
case 'z':
align = VA_STACK_ALIGN(size_t);
size = sizeof(size_t);
continue;
case 't':
align = VA_STACK_ALIGN(ptrdiff_t);
size = sizeof(ptrdiff_t);
continue;
case 'c':
case 'd':
case 'i':
case 'o':
case 'u':
case 'x':
case 'X':
if (fmt[-1] == 'l') {
if (fmt[-2] == 'l') {
align = VA_STACK_ALIGN(long long);
size = sizeof(long long);
} else {
align = VA_STACK_ALIGN(long);
size = sizeof(long);
}
}
parsing = false;
break;
case 's':
case 'p':
case 'n':
align = VA_STACK_ALIGN(void *);
size = sizeof(void *);
parsing = false;
break;
case 'a':
case 'A':
case 'e':
case 'E':
case 'f':
case 'F':
case 'g':
case 'G': {
/*
* Handle floats separately as they may be
* held in a different register set.
*/
union { double d; long double ld; } v;
if (fmt[-1] == 'L') {
v.ld = va_arg(ap, long double);
align = VA_STACK_ALIGN(long double);
size = sizeof(long double);
} else {
v.d = va_arg(ap, double);
align = VA_STACK_ALIGN(double);
size = sizeof(double);
}
/* align destination buffer location */
buf = (void *) ROUND_UP(buf, align);
if (buf0) {
/* make sure it fits */
if (buf - buf0 + size > len) {
return -ENOSPC;
}
if (VA_STACK_LL_DBL_MEMCPY) {
memcpy(buf, &v, size);
} else if (fmt[-1] == 'L') {
*(long double *)buf = v.ld;
} else {
*(double *)buf = v.d;
}
}
buf += size;
parsing = false;
continue;
}
default:
parsing = false;
continue;
}
/* align destination buffer location */
buf = (void *) ROUND_UP(buf, align);
/* make sure the data fits */
if (buf0 && buf - buf0 + size > len) {
return -ENOSPC;
}
/* copy va_list data over to our buffer */
if (*fmt == 's') {
s = va_arg(ap, char *);
process_string:
if (buf0) {
*(const char **)buf = s;
}
if (ptr_in_rodata(s)) {
/* do nothing special */
} else if (buf0) {
/*
* Remember string pointer location.
* We will append it later.
*/
if (s_idx >= ARRAY_SIZE(str_ptr_pos)) {
__ASSERT(false, "str_ptr_pos[] too small");
return -EINVAL;
}
/* Use same multiple as the arg list size. */
str_ptr_pos[s_idx++] = (buf - buf0) / sizeof(int);
} else {
/*
* Add the string length, the final '\0'
* and size of the pointer position prefix.
*/
len += strlen(s) + 1 + 1;
}
buf += sizeof(char *);
} else if (size == sizeof(int)) {
int v = va_arg(ap, int);
if (buf0) {
*(int *)buf = v;
}
buf += sizeof(int);
} else if (size == sizeof(long)) {
long v = va_arg(ap, long);
if (buf0) {
*(long *)buf = v;
}
buf += sizeof(long);
} else if (size == sizeof(long long)) {
long long v = va_arg(ap, long long);
if (buf0) {
if (VA_STACK_LL_DBL_MEMCPY) {
memcpy(buf, &v, sizeof(long long));
} else {
*(long long *)buf = v;
}
}
buf += sizeof(long long);
} else {
__ASSERT(false, "unexpected size %u", size);
return -EINVAL;
}
}
/*
* We remember the size of the argument list as a multiple of
* sizeof(int) and limit it to a 8-bit field. That means 1020 bytes
* worth of va_list, or about 127 arguments on a 64-bit system
* (twice that on 32-bit systems). That ought to be good enough.
*/
if ((buf - buf0) / sizeof(int) > 255) {
__ASSERT(false, "too many format args");
return -EINVAL;
}
/*
* If all we wanted was to count required buffer size
* then we have it now.
*/
if (!buf0) {
return len + buf - buf0;
}
/* Clear our buffer header. We made room for it initially. */
*(char **)buf0 = 0;
/* Record end of argument list and number of appended strings. */
buf0[0] = (buf - buf0) / sizeof(int);
buf0[1] = s_idx;
/* Store strings prefixed by their pointer location. */
for (i = 0; i < s_idx; i++) {
/* retrieve the string pointer */
s = *(char **)(buf0 + str_ptr_pos[i] * sizeof(int));
/* clear the in-buffer pointer (less entropy if compressed) */
*(char **)(buf0 + str_ptr_pos[i] * sizeof(int)) = NULL;
/* find the string length including terminating '\0' */
size = strlen(s) + 1;
/* make sure it fits */
if (buf - buf0 + 1 + size > len) {
return -ENOSPC;
}
/* store the pointer position prefix */
*buf++ = str_ptr_pos[i];
/* copy the string with its terminating '\0' */
memcpy(buf, s, size);
buf += size;
}
/*
* TODO: remove pointers for appended strings since they're useless.
* TODO: explore leveraging same mechanism to remove alignment padding
*/
return buf - buf0;
}
int cbprintf_package(void *packaged, size_t len, const char *format, ...)
{
va_list ap;
int ret;
va_start(ap, format);
ret = cbvprintf_package(packaged, len, format, ap);
va_end(ap);
return ret;
}
int cbpprintf(cbprintf_cb out, void *ctx, void *packaged)
{
char *buf = packaged, *fmt, *s, **ps;
unsigned int i, args_size, s_nbr, s_idx;
if (!buf) {
return -EINVAL;
}
/* Retrieve the size of the arg list and number of strings. */
args_size = ((uint8_t *)buf)[0] * sizeof(int);
s_nbr = ((uint8_t *)buf)[1];
/* Locate the string table */
s = buf + args_size;
/*
* Patch in string pointers.
*/
for (i = 0; i < s_nbr; i++) {
/* Locate pointer location for this string */
s_idx = *(uint8_t *)s++;
ps = (char **)(buf + s_idx * sizeof(int));
/* update the pointer with current string location */
*ps = s;
/* move to next string */
s += strlen(s) + 1;
}
/* Retrieve format string */
fmt = ((char **)buf)[1];
/* skip past format string pointer */
buf += sizeof(char *) * 2;
/* Turn this into a va_list and print it */
return cbprintf_via_va_list(out, ctx, fmt, buf);
}