Repository: RIOT-OS/RIOT
Pull request #20664: pkg/mpaland-printf: Add alternative stdio as package
Author: login: maribu
Labels: name: Area: doc
description: Area: Documentation
name: Area: tests
description: Area: tests and testing framework
name: Area: build system
description: Area: Build system
name: Type: new feature
description: The issue requests / The PR implemements a new feature for RIOT
name: Area: pkg
description: Area: External package ports
name: CI: ready for build
description: If set, CI server will compile all applications for all available boards for the labeled PR
Created: 2024-05-11T17:18:18Z | Merged: 2025-04-25T05:02:28Z

### Contribution description

This packs the stdio implementation from https://github.com/mpaland/printf as alternative to what the used standard C lib provides with the intent to provide a thread-safe, smaller, and more feature-complete alternative on newlib targets.

Compared to `newlib_nano` this reduces `.text` by a bit more than 200 B while adding support for standard format specifiers such as `RPIu8`, `PRIu16`, `PRIu64`, `%z`, and `%t`.

Note that `newlib_nano`'s stdio can be thread-safe in reentrant mode at the cost of RAM (per thread) and latency. Especially the increase in latency can be prohibitive when real time requirements need to be met.

[1]: https://github.com/mpaland/printf

### Testing procedure

A test application has been added in `tests/sys/snprintf`. This will use the package if `newlib` is used. It should pass, but removing `Makefile.board.dep` (which pulls in the `mpaland-printf` package) should a failing test.

### Issues/PRs references

https://github.com/RIOT-OS/RIOT/issues/20361


Diff:
diff --git a/pkg/mpaland-printf/Makefile b/pkg/mpaland-printf/Makefile
new file mode 100644
index 000000000000..9a7f1f490f9f
--- /dev/null
+++ b/pkg/mpaland-printf/Makefile
@@ -0,0 +1,10 @@
+PKG_NAME=mpaland-printf
+PKG_URL=https://github.com/mpaland/printf
+# 4.0.0
+PKG_VERSION=0dd4b64bc778bf55229428cefccba4c0a81f384b
+PKG_LICENSE=MIT
+
+include $(RIOTBASE)/pkg/pkg.mk
+
+all:
+	$(QQ)"$(MAKE)" -C $(PKG_SOURCE_DIR) -f $(RIOTPKG)/$(PKG_NAME)/$(PKG_NAME).mk
diff --git a/pkg/mpaland-printf/Makefile.dep b/pkg/mpaland-printf/Makefile.dep
new file mode 100644
index 000000000000..d509d368da60
--- /dev/null
+++ b/pkg/mpaland-printf/Makefile.dep
@@ -0,0 +1,4 @@
+# mpaland's printf does not implement non-standard format specifiers. For
+# AVR MCUs we do need the ability to print from PROGMEM, though. Hence, this
+# is not compatible with AVR MCUs.
+FEATURES_BLACKLIST += arch_avr8
diff --git a/pkg/mpaland-printf/Makefile.include b/pkg/mpaland-printf/Makefile.include
new file mode 100644
index 000000000000..15ade60f0af6
--- /dev/null
+++ b/pkg/mpaland-printf/Makefile.include
@@ -0,0 +1,17 @@
+# wrap stdio functions to use mpaland's printf instead of the one from the
+# standard C lib
+LINKFLAGS += -Wl,-wrap=printf
+LINKFLAGS += -Wl,-wrap=sprintf
+LINKFLAGS += -Wl,-wrap=snprintf
+LINKFLAGS += -Wl,-wrap=vprintf
+LINKFLAGS += -Wl,-wrap=vsnprintf
+LINKFLAGS += -Wl,-wrap=putchar
+LINKFLAGS += -Wl,-wrap=puts
+
+# Workaround for bug in the newlib headers shipped with e.g. Ubuntu 24.04 LTS
+# not defining PRId64 / PRIu64 / PRIx64 / PRIo64 in <inttypes.h> due to an issue
+# in <machine/_default_types.h>. Tested to cause no regression on fixed newlib
+# headers.
+ifneq (,$(filter newlib,$(FEATURES_USED)))
+  CFLAGS += -D__int64_t_defined=1
+endif
diff --git a/pkg/mpaland-printf/doc.md b/pkg/mpaland-printf/doc.md
new file mode 100644
index 000000000000..177d067209d7
--- /dev/null
+++ b/pkg/mpaland-printf/doc.md
@@ -0,0 +1,27 @@
+@defgroup   pkg_mpaland-printf   mpaland's printf
+@ingroup    pkg
+@brief      mpaland's printf implementation is a lean, thread-safe and
+            feature-complete printf implementation
+
+# License
+
+Licensed under the MIT license.
+
+# Usage
+
+Add `USEPKG += mpaland-printf` to the application's `Makefile` or compile
+using `USEPKG=mpaland-printf make BOARD=<BOARD> -C <APP_DIR>`.
+
+# Features
+
+The package implements all standard format specifiers. However, support
+for floating point is disabled by default due to the immense ROM overhead
+on MCUs without FPU.
+
+@note   Support for floating point formatting can be enabled
+        via the `printf_float` module, i.e., by adding
+        `USEMODULE += printf_float` to the application's `Makefile`.
+@note   Support for non-standard format specifiers such as needed for
+        printing from flash on AVR MCUs are not implemented.
+
+@see    https://github.com/mpaland/printf
diff --git a/pkg/mpaland-printf/mpaland-printf.mk b/pkg/mpaland-printf/mpaland-printf.mk
new file mode 100644
index 000000000000..2d559d90ba11
--- /dev/null
+++ b/pkg/mpaland-printf/mpaland-printf.mk
@@ -0,0 +1,7 @@
+MODULE := mpaland-printf
+
+SRC := \
+  printf.c \
+  #
+
+include $(RIOTBASE)/Makefile.base
diff --git a/pkg/mpaland-printf/patches/0001-RIOT-integration.patch b/pkg/mpaland-printf/patches/0001-RIOT-integration.patch
new file mode 100644
index 000000000000..632a8abd61db
--- /dev/null
+++ b/pkg/mpaland-printf/patches/0001-RIOT-integration.patch
@@ -0,0 +1,50 @@
+From 04f0f9616f74bbb93a2994d627d4ab476bd81d31 Mon Sep 17 00:00:00 2001
+From: Marian Buschsieweke <marian.buschsieweke@posteo.net>
+Date: Sat, 11 May 2024 17:51:01 +0200
+Subject: [PATCH 1/4] RIOT integration
+
+Use stdio_write() from stdio_base.h for output
+---
+ printf.c | 8 ++++++++
+ printf.h | 2 +-
+ 2 files changed, 9 insertions(+), 1 deletion(-)
+
+diff --git a/printf.c b/printf.c
+index 8a700ad..4a17672 100644
+--- a/printf.c
++++ b/printf.c
+@@ -34,6 +34,7 @@
+ #include <stdint.h>
+ 
+ #include "printf.h"
++#include "stdio_base.h"
+ 
+ 
+ // define this globally (e.g. gcc -DPRINTF_INCLUDE_CONFIG_H ...) to include the
+@@ -912,3 +913,10 @@ int fctprintf(void (*out)(char character, void* arg), void* arg, const char* for
+   va_end(va);
+   return ret;
+ }
++
++
++/* RIOT integration: Use stdio_write for output */
++static void _putchar(char character)
++{
++    stdio_write(&character, sizeof(character));
++}
+diff --git a/printf.h b/printf.h
+index 6075bc2..154930a 100644
+--- a/printf.h
++++ b/printf.h
+@@ -46,7 +46,7 @@ extern "C" {
+  * This function is declared here only. You have to write your custom implementation somewhere
+  * \param character Character to output
+  */
+-void _putchar(char character);
++static void _putchar(char character);
+ 
+ 
+ /**
+-- 
+2.43.0
+
diff --git a/pkg/mpaland-printf/patches/0002-Wrapper-targets-Add-endpoints-for-Wl-wrap.patch b/pkg/mpaland-printf/patches/0002-Wrapper-targets-Add-endpoints-for-Wl-wrap.patch
new file mode 100644
index 000000000000..8c59b45d039d
--- /dev/null
+++ b/pkg/mpaland-printf/patches/0002-Wrapper-targets-Add-endpoints-for-Wl-wrap.patch
@@ -0,0 +1,66 @@
+From 2070026e3325ad7640c5cddc9cb1254aa63d61bb Mon Sep 17 00:00:00 2001
+From: Marian Buschsieweke <marian.buschsieweke@posteo.net>
+Date: Sat, 11 May 2024 17:51:38 +0200
+Subject: [PATCH 2/4] Wrapper targets: Add endpoints for -Wl,wrap=...
+
+This adds aliases needed to wrap printf() and friends.
+---
+ printf.c | 38 ++++++++++++++++++++++++++++++++++++++
+ 1 file changed, 38 insertions(+)
+
+diff --git a/printf.c b/printf.c
+index 4a17672..e06c9aa 100644
+--- a/printf.c
++++ b/printf.c
+@@ -32,6 +32,7 @@
+ 
+ #include <stdbool.h>
+ #include <stdint.h>
++#include <string.h>
+ 
+ #include "printf.h"
+ #include "stdio_base.h"
+@@ -920,3 +921,40 @@ static void _putchar(char character)
+ {
+     stdio_write(&character, sizeof(character));
+ }
++
++
++/* provide entry points for linker to redirect stdio */
++__attribute__((alias("printf_")))
++int __wrap_printf(const char* format, ...);
++
++
++__attribute__((alias("sprintf_")))
++int __wrap_sprintf(char* buffer, const char* format, ...);
++
++
++__attribute__((alias("snprintf_")))
++int __wrap_snprintf(char* buffer, size_t count, const char* format, ...);
++
++
++__attribute__((alias("vprintf_")))
++int __wrap_vprintf(const char* format, va_list va);
++
++
++__attribute__((alias("vsnprintf_")))
++int __wrap_vsnprintf(char* buffer, size_t count, const char* format, va_list va);
++
++
++int __wrap_putchar(int c)
++{
++    _putchar((char)c);
++    return 1;
++}
++
++
++int __wrap_puts(const char *s)
++{
++    size_t len = strlen(s);
++    stdio_write(s, len);
++    stdio_write("\n", 1);
++    return len + 1;
++}
+-- 
+2.43.0
+
diff --git a/pkg/mpaland-printf/patches/0003-RIOT-integration-Enable-floating-support-based-on-mo.patch b/pkg/mpaland-printf/patches/0003-RIOT-integration-Enable-floating-support-based-on-mo.patch
new file mode 100644
index 000000000000..dbc720c2f27d
--- /dev/null
+++ b/pkg/mpaland-printf/patches/0003-RIOT-integration-Enable-floating-support-based-on-mo.patch
@@ -0,0 +1,26 @@
+From 33f690a4882dc567df328a900c5d12e113feb15c Mon Sep 17 00:00:00 2001
+From: Marian Buschsieweke <marian.buschsieweke@posteo.net>
+Date: Sat, 11 May 2024 18:38:51 +0200
+Subject: [PATCH 3/4] RIOT integration: Enable floating support based on module
+ selection
+
+---
+ printf.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/printf.c b/printf.c
+index e06c9aa..5266cf7 100644
+--- a/printf.c
++++ b/printf.c
+@@ -62,7 +62,7 @@
+ 
+ // support for the floating point type (%f)
+ // default: activated
+-#ifndef PRINTF_DISABLE_SUPPORT_FLOAT
++#ifdef MODULE_PRINTF_FLOAT
+ #define PRINTF_SUPPORT_FLOAT
+ #endif
+ 
+-- 
+2.43.0
+
diff --git a/pkg/mpaland-printf/patches/0004-Fix-parsing-of-int8_t.patch b/pkg/mpaland-printf/patches/0004-Fix-parsing-of-int8_t.patch
new file mode 100644
index 000000000000..19262fb85317
--- /dev/null
+++ b/pkg/mpaland-printf/patches/0004-Fix-parsing-of-int8_t.patch
@@ -0,0 +1,28 @@
+From 81ef21a29faafbdb95800ce40b2e20b533b88ebd Mon Sep 17 00:00:00 2001
+From: Marian Buschsieweke <marian.buschsieweke@posteo.net>
+Date: Sat, 11 May 2024 22:42:21 +0200
+Subject: [PATCH 4/4] Fix parsing of `int8_t`
+
+The code assumes that `char` is signed, but the C standard allows
+`char` to be either signed or unsigned. Instead, `singed char` and
+`unsigned char` need to be used for portable code.
+---
+ printf.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/printf.c b/printf.c
+index 5266cf7..3f5f66a 100644
+--- a/printf.c
++++ b/printf.c
+@@ -734,7 +734,7 @@ static int _vsnprintf(out_fct_type out, char* buffer, const size_t maxlen, const
+             idx = _ntoa_long(out, buffer, idx, maxlen, (unsigned long)(value > 0 ? value : 0 - value), value < 0, base, precision, width, flags);
+           }
+           else {
+-            const int value = (flags & FLAGS_CHAR) ? (char)va_arg(va, int) : (flags & FLAGS_SHORT) ? (short int)va_arg(va, int) : va_arg(va, int);
++            const int value = (flags & FLAGS_CHAR) ? (signed char)va_arg(va, int) : (flags & FLAGS_SHORT) ? (short int)va_arg(va, int) : va_arg(va, int);
+             idx = _ntoa_long(out, buffer, idx, maxlen, (unsigned int)(value > 0 ? value : 0 - value), value < 0, base, precision, width, flags);
+           }
+         }
+-- 
+2.43.0
+
diff --git a/tests/sys/snprintf/Makefile b/tests/sys/snprintf/Makefile
new file mode 100644
index 000000000000..d5e587b491b3
--- /dev/null
+++ b/tests/sys/snprintf/Makefile
@@ -0,0 +1,7 @@
+include ../Makefile.sys_common
+
+# avrlibc's snprintf doesn't support uint64_t / int64_t and even fails
+# to compile due to PRI*64 macros not being defined
+FEATURES_BLACKLIST := arch_avr8
+
+include $(RIOTBASE)/Makefile.include
diff --git a/tests/sys/snprintf/Makefile.board.dep b/tests/sys/snprintf/Makefile.board.dep
new file mode 100644
index 000000000000..d2f05e988ee0
--- /dev/null
+++ b/tests/sys/snprintf/Makefile.board.dep
@@ -0,0 +1,4 @@
+# newlib's printf is known to be incomplete, despite being bloated
+ifneq (,$(filter newlib,$(USEMODULE)))
+  USEPKG += mpaland-printf
+endif
diff --git a/tests/sys/snprintf/README.md b/tests/sys/snprintf/README.md
new file mode 100644
index 000000000000..8a034210d016
--- /dev/null
+++ b/tests/sys/snprintf/README.md
@@ -0,0 +1,6 @@
+# snprintf
+
+This test aims to test if the stdio implementations correctly implements
+standard format specifiers. Instead of relying on the transport of stdout to
+be fast and reliable, it will use snprintf to format in-memory and compare
+in the app with correctness.
diff --git a/tests/sys/snprintf/main.c b/tests/sys/snprintf/main.c
new file mode 100644
index 000000000000..bf9b33886779
--- /dev/null
+++ b/tests/sys/snprintf/main.c
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2024 Marian Buschsieweke
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @ingroup tests
+ * @{
+ *
+ * @file
+ * @brief       snprintf test (correctness of standard format specifier
+ *              implementation)
+ *
+ * @author      Marian Buschsieweke <marian.buschsieweke@posteo.net>
+ *
+ * @}
+ */
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+static bool failed = false;
+
+static void check(const char *expected, const char *got, int retval)
+{
+    if (retval != (int)strlen(expected)) {
+        failed = true;
+        printf("snprintf() returned %d, but expected %u\n",
+               retval, (unsigned)strlen(expected));
+    }
+
+    if (strcmp(expected, got) != 0) {
+        printf("Expected: \"%s\"\n"
+               "Got:      \"%s\"\n",
+               expected, got);
+        failed = true;
+    }
+}
+
+static void test_int8(void)
+{
+    static uint8_t u8 = 10;
+    static int8_t s8 = -3;
+
+    /* memory barrier to prevent the compiler from constant folding on the
+     * snprintf calls */
+    __asm__ volatile ("" ::: "memory");
+
+    char buf[32];
+    size_t len;
+
+    len = snprintf(buf, sizeof(buf), "%" PRIu8, u8);
+    check("10", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIx8, u8);
+    check("a", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%#" PRIx8, u8);
+    check("0xa", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIX8, u8);
+    check("A", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%#" PRIX8, u8);
+    check("0XA", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIo8, u8);
+    check("12", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%#" PRIo8, u8);
+    check("012", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRId8, s8);
+    check("-3", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIi8, s8);
+    check("-3", buf, len);
+}
+
+static void test_int16(void)
+{
+    static uint16_t u16 = 45054;
+    static int16_t s16 = -1337;
+
+    /* memory barrier to prevent the compiler from constant folding on the
+     * snprintf calls */
+    __asm__ volatile ("" ::: "memory");
+
+    char buf[32];
+    size_t len;
+
+    len = snprintf(buf, sizeof(buf), "%" PRIu16, u16);
+    check("45054", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIx16, u16);
+    check("affe", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%#" PRIx16, u16);
+    check("0xaffe", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIX16, u16);
+    check("AFFE", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%#" PRIX16, u16);
+    check("0XAFFE", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIo16, u16);
+    check("127776", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%#" PRIo16, u16);
+    check("0127776", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRId16, s16);
+    check("-1337", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIi16, s16);
+    check("-1337", buf, len);
+}
+
+static void test_int32(void)
+{
+    static uint32_t u32 = 2952663863;
+    static int32_t s32 = -2147483648;
+
+    /* memory barrier to prevent the compiler from constant folding on the
+     * snprintf calls */
+    __asm__ volatile ("" ::: "memory");
+
+    char buf[32];
+    size_t len;
+
+    len = snprintf(buf, sizeof(buf), "%" PRIu32, u32);
+    check("2952663863", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIx32, u32);
+    check("affe1337", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%#" PRIx32, u32);
+    check("0xaffe1337", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIX32, u32);
+    check("AFFE1337", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%#" PRIX32, u32);
+    check("0XAFFE1337", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIo32, u32);
+    check("25777411467", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%#" PRIo32, u32);
+    check("025777411467", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRId32, s32);
+    check("-2147483648", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIi32, s32);
+    check("-2147483648", buf, len);
+}
+
+static void test_int64(void)
+{
+    static uint64_t u64 = 16045690984050070327ULL;
+    static int64_t s64 = -9223372036854775807LL;
+
+    /* memory barrier to prevent the compiler from constant folding on the
+     * snprintf calls */
+    __asm__ volatile ("" ::: "memory");
+
+    char buf[32];
+    size_t len;
+
+    len = snprintf(buf, sizeof(buf), "%" PRIu64, u64);
+    check("16045690984050070327", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIx64, u64);
+    check("deadbeefaffe1337", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%#" PRIx64, u64);
+    check("0xdeadbeefaffe1337", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIX64, u64);
+    check("DEADBEEFAFFE1337", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%#" PRIX64, u64);
+    check("0XDEADBEEFAFFE1337", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIo64, u64);
+    check("1572555756765777411467", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%#" PRIo64, u64);
+    check("01572555756765777411467", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRId64, s64);
+    check("-9223372036854775807", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%" PRIi64, s64);
+    check("-9223372036854775807", buf, len);
+}
+
+static void test_size(void)
+{
+    static size_t s = 42;
+    static ssize_t ss = -1;
+    static ptrdiff_t p = 1337;
+
+    /* memory barrier to prevent the compiler from constant folding on the
+     * snprintf calls */
+    __asm__ volatile ("" ::: "memory");
+
+    char buf[32];
+    size_t len;
+
+    len = snprintf(buf, sizeof(buf), "%zu", s);
+    check("42", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%zd", ss);
+    check("-1", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%td", p);
+    check("1337", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%tu", p);
+    check("1337", buf, len);
+}
+
+static void test_flags_widths(void)
+{
+    static uint16_t u16 = 42;
+    static int16_t s16 = -42;
+
+    /* memory barrier to prevent the compiler from constant folding on the
+     * snprintf calls */
+    __asm__ volatile ("" ::: "memory");
+
+    char buf[32];
+    size_t len;
+
+    len = snprintf(buf, sizeof(buf), "%03" PRIu16, u16);
+    check("042", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%3" PRIu16, u16);
+    check(" 42", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%-3" PRIu16, u16);
+    check("42 ", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%*" PRIu16, 8, u16);
+    check("      42", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%-*" PRIu16, 8, u16);
+    check("42      ", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%+" PRId16, (int16_t)u16);
+    check("+42", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%04" PRId16, s16);
+    check("-042", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%4" PRId16, s16);
+    check(" -42", buf, len);
+
+    len = snprintf(buf, sizeof(buf), "%-4" PRId16, s16);
+    check("-42 ", buf, len);
+}
+
+int main(void)
+{
+    puts("Testing snprintf() implementation...");
+    test_int8();
+    test_int16();
+    test_int32();
+    test_int64();
+    test_size();
+    test_flags_widths();
+
+    if (failed) {
+        puts("TEST FAILED!");
+    }
+    else {
+        puts("Test succeeded");
+    }
+
+    return 0;
+}
diff --git a/tests/sys/snprintf/tests/01-run.py b/tests/sys/snprintf/tests/01-run.py
new file mode 100755
index 000000000000..4f3fcac1cc9a
--- /dev/null
+++ b/tests/sys/snprintf/tests/01-run.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+
+#  Copyright (C) 2024 Marian Buschsieweke
+#
+# This file is subject to the terms and conditions of the GNU Lesser
+# General Public License v2.1. See the file LICENSE in the top level
+# directory for more details.
+
+# @author      Marian Buschsieweke <marian.buschsieweke@posteo.net>
+
+import sys
+from testrunner import run
+
+
+def testfunc(child):
+    child.expect_exact("Testing snprintf() implementation...")
+    child.expect_exact("Test succeeded")
+
+
+if __name__ == "__main__":
+    sys.exit(run(testfunc))


Reviews:
user: login: Enoch247
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T01:50:06Z
user: login: maribu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T05:57:15Z
user: login: maribu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T06:00:18Z
user: login: maribu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T06:04:52Z
user: login: maribu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T06:05:56Z
user: login: maribu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T07:57:50Z
user: login: Enoch247
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T00:21:35Z
user: login: Enoch247
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T01:29:31Z
user: login: mguetschow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T09:41:19Z
user: login: maribu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T09:44:19Z
user: login: maribu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T09:46:31Z
user: login: mcr
body: Having patches in git is giving me Quilt-PTSD, but I get why. I just hate it.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T13:31:01Z

Review comments:
diff_hunk: @@ -0,0 +1,4 @@
+# newlib's printf is known to be incomplete, despite being bloated
path: tests/sys/snprintf/Makefile.board.dep
user: login: Enoch247
body: Should this be the default for all builds rather than just for this one test?
created_at: 2025-04-12T00:31:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,14 @@
+# wrap stdio functions to use mpaland's printf instead of the one from the
+# standard C lib
+LINKFLAGS += -Wl,-wrap=printf
+LINKFLAGS += -Wl,-wrap=sprintf
+LINKFLAGS += -Wl,-wrap=snprintf
+LINKFLAGS += -Wl,-wrap=vprintf
+LINKFLAGS += -Wl,-wrap=vsnprintf
+LINKFLAGS += -Wl,-wrap=putchar
+LINKFLAGS += -Wl,-wrap=puts
+
+# Fix bug with newlib not providing PRId64 / PRIu64
path: pkg/mpaland-printf/Makefile.include
user: login: Enoch247
body: What was the bug? I traced this a little deeper tonight. It looks like `__int64_t_defined` (2 underscores) is defined in `_stdint.h` if `___int64_t_defined` (3 underscores) is defined.  `___int64_t_defined` is defined in `_default_types.h` based on some auto detection logic. Is this logic broken?
created_at: 2025-04-17T00:42:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2024 Marian Buschsieweke
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @ingroup tests
+ * @{
+ *
+ * @file
+ * @brief       snprintf test (correctness of standard format specifier
+ *              implementation)
+ *
+ * @author      Marian Buschsieweke <marian.buschsieweke@posteo.net>
+ *
+ * @}
+ */
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static bool failed = false;
+
+static void check(const char *expected, const char *got, int retval)
+{
+    if (retval != (int)strlen(expected)) {
+        failed = true;
+        printf("snprintf() returned %d, but expected %u\n",
+               retval, (unsigned)strlen(expected));
+    }
+
+    if (strcmp(expected, got) != 0) {
+        printf("Expected: \"%s\"\n"
+               "Got:      \"%s\"\n",
+               expected, got);
+        failed = true;
+    }
+}
+
+static void test_int8(void)
+{
+    static uint8_t u8 = 10;
+    static int8_t s8 = -3;
+
+    /* memory barrier to prevent the compiler from constant folding on the
path: tests/sys/snprintf/main.c
user: login: Enoch247
body: Would marking `u8` and `s8` as `volatile` accomplish the same thing, but be more portable? I honestly don't know the answer.
created_at: 2025-04-17T01:06:48Z
author_association: CONTRIBUTOR
path: pkg/mpaland-printf/patches/0003-RIOT-integration-Enable-floating-support-based-on-mo.patch
user: login: Enoch247
body: What's up with this patch? Line ending changes?
created_at: 2025-04-17T01:49:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,4 @@
+# newlib's printf is known to be incomplete, despite being bloated
path: tests/sys/snprintf/Makefile.board.dep
user: login: maribu
body: That is what I am aiming for. But I'd like to do this as a follow up and focus here on the technical details of the package integration, and have the strategic discussion later on.
created_at: 2025-04-17T05:57:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,14 @@
+# wrap stdio functions to use mpaland's printf instead of the one from the
+# standard C lib
+LINKFLAGS += -Wl,-wrap=printf
+LINKFLAGS += -Wl,-wrap=sprintf
+LINKFLAGS += -Wl,-wrap=snprintf
+LINKFLAGS += -Wl,-wrap=vprintf
+LINKFLAGS += -Wl,-wrap=vsnprintf
+LINKFLAGS += -Wl,-wrap=putchar
+LINKFLAGS += -Wl,-wrap=puts
+
+# Fix bug with newlib not providing PRId64 / PRIu64
path: pkg/mpaland-printf/Makefile.include
user: login: maribu
body: It did just work on Alpine where a newer versions of newlib is packaged, so I probably could just biscet the issue in newlib and see how exactly it was fixes.
created_at: 2025-04-17T06:00:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2024 Marian Buschsieweke
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @ingroup tests
+ * @{
+ *
+ * @file
+ * @brief       snprintf test (correctness of standard format specifier
+ *              implementation)
+ *
+ * @author      Marian Buschsieweke <marian.buschsieweke@posteo.net>
+ *
+ * @}
+ */
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static bool failed = false;
+
+static void check(const char *expected, const char *got, int retval)
+{
+    if (retval != (int)strlen(expected)) {
+        failed = true;
+        printf("snprintf() returned %d, but expected %u\n",
+               retval, (unsigned)strlen(expected));
+    }
+
+    if (strcmp(expected, got) != 0) {
+        printf("Expected: \"%s\"\n"
+               "Got:      \"%s\"\n",
+               expected, got);
+        failed = true;
+    }
+}
+
+static void test_int8(void)
+{
+    static uint8_t u8 = 10;
+    static int8_t s8 = -3;
+
+    /* memory barrier to prevent the compiler from constant folding on the
path: tests/sys/snprintf/main.c
user: login: maribu
body: Using `volatile` on the variable declaration forces every single read and write to be done unoptimised. With few exceptions, this should only be done with memory mapped I/O.

There is this excellent article demystifying `volatile` that I can highly recommend: https://blog.regehr.org/archives/28
created_at: 2025-04-17T06:04:52Z
author_association: MEMBER
path: pkg/mpaland-printf/patches/0003-RIOT-integration-Enable-floating-support-based-on-mo.patch
user: login: maribu
body: Whoopsie :-)
created_at: 2025-04-17T06:05:56Z
author_association: MEMBER
path: pkg/mpaland-printf/patches/0003-RIOT-integration-Enable-floating-support-based-on-mo.patch
user: login: maribu
body: Fixed
created_at: 2025-04-17T07:57:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,14 @@
+# wrap stdio functions to use mpaland's printf instead of the one from the
+# standard C lib
+LINKFLAGS += -Wl,-wrap=printf
+LINKFLAGS += -Wl,-wrap=sprintf
+LINKFLAGS += -Wl,-wrap=snprintf
+LINKFLAGS += -Wl,-wrap=vprintf
+LINKFLAGS += -Wl,-wrap=vsnprintf
+LINKFLAGS += -Wl,-wrap=putchar
+LINKFLAGS += -Wl,-wrap=puts
+
+# Fix bug with newlib not providing PRId64 / PRIu64
path: pkg/mpaland-printf/Makefile.include
user: login: Enoch247
body: The current workaround of setting a macro that (I presume) is meant to be private, feels a bit wrong. If it is the only way, I'll live with it, but I believe its worth a few extra minutes to see if there is a more robust fix or workaround.
created_at: 2025-04-18T00:21:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+/**
path: pkg/mpaland-printf/doc.txt
user: login: mguetschow
body: How about moving this to a `doc.md`?
created_at: 2025-04-24T09:37:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+/**
+ * @defgroup    pkg_mpaland-printf   mpaland's printf
+ * @ingroup     pkg
+ * @brief       mpaland's printf implementation is a lean, thread-safe and
+ *              feature-complete printf implementation
+ *
+ * # License
+ *
+ * Licensed under the MIT license.
+ *
+ * # Usage
+ *
+ * Add `USEPKG += mpaland-printf` to the application's `Makefile` or compile
+ * using `USEPKG=mpaland-printf make BOARD=<BOARD> -C <APP_DIR>`.
+ *
+ * # Features
+ *
+ * The package implements all standard format specifiers. However, support
+ * for floating point is disabled by default due to the immense ROM overhead
+ * on MCUs without FPU.
+ *
+ * @note    Support for floating point formatting can be enabled
+ *          `printf_float` module, e.g. by adding
+ *          `USEMODULE += printf_float` to the application's `Makefile`.
path: pkg/mpaland-printf/doc.txt
user: login: mguetschow
body: ```suggestion
 * @note    Support for floating point formatting can be enabled
 *          via the `printf_float` module, i.e., by adding
 *          `USEMODULE += printf_float` to the application's `Makefile`.
```
created_at: 2025-04-24T09:37:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+/**
+ * @defgroup    pkg_mpaland-printf   mpaland's printf
+ * @ingroup     pkg
+ * @brief       mpaland's printf implementation is a lean, thread-safe and
+ *              feature-complete printf implementation
+ *
+ * # License
+ *
+ * Licensed under the MIT license.
+ *
+ * # Usage
+ *
+ * Add `USEPKG += mpaland-printf` to the application's `Makefile` or compile
+ * using `USEPKG=mpaland-printf make BOARD=<BOARD> -C <APP_DIR>`.
+ *
+ * # Features
+ *
+ * The package implements all standard format specifiers. However, support
+ * for floating point is disabled by default due to the immense ROM overhead
+ * on MCUs without FPU.
+ *
+ * @note    Support for floating point formatting can be enabled
+ *          `printf_float` module, e.g. by adding
+ *          `USEMODULE += printf_float` to the application's `Makefile`.
+ * @note    Support for non-standard format specifiers such as needed for
+ *          printing from flash on AVR MCUs are not implemented.
+ *
+ * @see         https://github.com/mpaland/printf
path: pkg/mpaland-printf/doc.txt
user: login: mguetschow
body: ```suggestion
 * @see     https://github.com/mpaland/printf
```
created_at: 2025-04-24T09:38:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+/**
path: pkg/mpaland-printf/doc.txt
user: login: maribu
body: Will do. I'll rebase on master, because support for `doc.md` was not available back then.
created_at: 2025-04-24T09:44:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,29 @@
+/**
path: pkg/mpaland-printf/doc.txt
user: login: maribu
body: Done
created_at: 2025-04-24T09:46:31Z
author_association: MEMBER

PR comments:
user: login: riot-ci
created_at: 2024-05-11T17:33:45Z
body: ### Murdock results

:heavy_check_mark: [PASSED](https://ci.riot-os.org/details/ab0a9bdec66b4378bad0ddb516db905a)

42dacd7c9d74c2cdb4fc59dd5c0ea27a71180d07 tests/sys/snprintf: Test format specifiers

|  Success  |  Failures  |  Total  |  Runtime  |
| :-------: | :--------: | :-----: | :-------: |
| 10320 | 0 | 10320 | 13m:26s |


#### Artifacts
- [Documentation preview](https://ci.riot-os.org/results/ab0a9bdec66b4378bad0ddb516db905a/doc-preview/)

author_association: NONE
user: login: Enoch247
created_at: 2025-04-10T13:20:54Z
body: I'll try and take a look at this later since I am familiar with the pkg already.
author_association: CONTRIBUTOR
user: login: Enoch247
created_at: 2025-04-11T02:19:19Z
body: Started looking this over, but ran out of time. I'll pick it up another night.
author_association: CONTRIBUTOR
user: login: Enoch247
created_at: 2025-04-12T01:11:39Z
body: For me, the new test would not build when doing `make BOARD=stm32f429i-disc1` until I added the following hack:

``` patch
diff --git a/tests/sys/snprintf/main.c b/tests/sys/snprintf/main.c
index 151966c0d5..0d27dca3cb 100644
--- a/tests/sys/snprintf/main.c
+++ b/tests/sys/snprintf/main.c
@@ -19,6 +19,7 @@
  * @}
  */
 
+#define __int64_t_defined 1
 #include <inttypes.h>
 #include <stdbool.h>
 #include <stddef.h>
```
author_association: CONTRIBUTOR
user: login: maribu
created_at: 2025-04-12T18:28:14Z
body: Thx for reporting. I can reproduce the issue on my Ubuntu machine.

Can you give it another try with the fixup commit?
author_association: MEMBER
user: login: Enoch247
created_at: 2025-04-18T00:56:26Z
body: Indeed `make -C tests/sys/snprintf BOARD=stm32f429i-disc1 test` passes if I use this branch unmodified, and fails if I disable `mpaland-printf`. Is newlib's snprintf implementation really broken? A quick internet search did not turn up anything about this. The test also passes on `native` where neither newlib nor `mpaland-printf` are enabled.
author_association: CONTRIBUTOR
user: login: maribu
created_at: 2025-04-18T05:59:14Z
body: The newlib's stdio comes in two flavors: "super bloated" (stdio-nano), and "supermassive black hole of bloat" (regular stdio). The latter would pass the stido unit tests (try disabled newlib_nano), if it would even fit your MCU's flash and RAM. The former is a stripped down version of the latter, trading features (in other words compatibility with format specifiers) for size.

Both are way too large to be a sane option on MCUs. That is why the picolibs fork of newlib just trashed the stdio and replaces it with something that does make sense on MCUs.
author_association: MEMBER
user: login: maribu
created_at: 2025-04-25T05:09:13Z
body: Thx :-)
author_association: MEMBER