From 4a842f8e834e30f4f1ad14c8731c605e36b58e9c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Mar 2026 12:44:27 -1000 Subject: [PATCH 1/2] [esp32] Wrap vfprintf to fix printf stub on picolibc (IDF 6) IDF 6 uses picolibc instead of newlib. In newlib, vfprintf was a thin 36-byte trampoline to _vfprintf_r, so wrapping printf/vprintf/fprintf was sufficient to dead-code eliminate _vfprintf_r. In picolibc, vfprintf IS the heavy implementation (~2.8 KB) and is referenced directly by SDK components. Add vfprintf to the wrap list to intercept those references. --- esphome/components/esp32/__init__.py | 2 +- esphome/components/esp32/printf_stubs.cpp | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0e216485ac..91eb913e3d 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1587,7 +1587,7 @@ async def to_code(config): if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF]: cg.add_define("USE_FULL_PRINTF") else: - for symbol in ("vprintf", "printf", "fprintf"): + for symbol in ("vprintf", "printf", "fprintf", "vfprintf"): cg.add_build_flag(f"-Wl,--wrap={symbol}") else: cg.add_build_flag("-DUSE_ARDUINO") diff --git a/esphome/components/esp32/printf_stubs.cpp b/esphome/components/esp32/printf_stubs.cpp index c6f03bc363..3279b6f07e 100644 --- a/esphome/components/esp32/printf_stubs.cpp +++ b/esphome/components/esp32/printf_stubs.cpp @@ -2,10 +2,11 @@ * Linker wrap stubs for FILE*-based printf functions. * * ESP-IDF SDK components (gpio driver, ringbuf, log_write) reference - * fprintf(), printf(), and vprintf() which pull in newlib's _vfprintf_r - * (~11 KB). This is a separate implementation from _svfprintf_r (used by - * snprintf/vsnprintf) that handles FILE* stream I/O with buffering and - * locking. + * fprintf(), printf(), vprintf(), and vfprintf() which pull in the full + * printf implementation (~11 KB on newlib's _vfprintf_r, ~2.8 KB on + * picolibc's vfprintf). This is a separate implementation from the one + * used by snprintf/vsnprintf that handles FILE* stream I/O with buffering + * and locking. * * ESPHome replaces the ESP-IDF log handler via esp_log_set_vprintf_(), * so the SDK's vprintf() path is dead code at runtime. The fprintf() @@ -70,6 +71,11 @@ int __wrap_printf(const char *fmt, ...) { return len; } +int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + int __wrap_fprintf(FILE *stream, const char *fmt, ...) { va_list ap; va_start(ap, fmt); From 802dcdabca5fd6fcb838789d2b592ae5a6d12865 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Mar 2026 12:44:59 -1000 Subject: [PATCH 2/2] Refactor __wrap_fprintf to delegate to __wrap_vfprintf --- esphome/components/esp32/printf_stubs.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/esp32/printf_stubs.cpp b/esphome/components/esp32/printf_stubs.cpp index 3279b6f07e..386fbbd79d 100644 --- a/esphome/components/esp32/printf_stubs.cpp +++ b/esphome/components/esp32/printf_stubs.cpp @@ -79,8 +79,7 @@ int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { int __wrap_fprintf(FILE *stream, const char *fmt, ...) { va_list ap; va_start(ap, fmt); - char buf[PRINTF_BUFFER_SIZE]; - int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); + int len = __wrap_vfprintf(stream, fmt, ap); va_end(ap); return len; }