From af1a00ccf9b8f055a9a51fd9fe019b9c8d9eb638 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 12:35:41 -1000 Subject: [PATCH] [esp32] Wrap printf/vprintf/fprintf to eliminate _vfprintf_r (~11 KB flash) ESP-IDF SDK components 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. 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() and printf() calls in SDK components are only in debug/assert paths that are either GC'd or never called. These linker --wrap stubs redirect through vsnprintf() + fwrite(), allowing the linker to dead-code eliminate _vfprintf_r. An escape hatch is provided via enable_full_printf: true in the esp32 advanced config section for external components that need full FILE*-based fprintf. --- esphome/components/esp32/__init__.py | 10 +++ esphome/components/esp32/printf_stubs.cpp | 79 +++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 esphome/components/esp32/printf_stubs.cpp diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 998913ecec..e3a58e904e 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -952,6 +952,7 @@ CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle" CONF_DISABLE_DEBUG_STUBS = "disable_debug_stubs" +CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_DISABLE_OCD_AWARE = "disable_ocd_aware" CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary" CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs" @@ -1126,6 +1127,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional( CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, default=[] ): cv.ensure_list(cv.string_strict), + cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean, cv.Optional(CONF_DISABLE_DEBUG_STUBS, default=True): cv.boolean, cv.Optional(CONF_DISABLE_OCD_AWARE, default=True): cv.boolean, cv.Optional( @@ -1469,6 +1471,14 @@ async def to_code(config): "_ZSt25__throw_bad_function_callv", ]: cg.add_build_flag(f"-Wl,--wrap={mangled}") + + # Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r + # (~11 KB). See printf_stubs.cpp for implementation. + if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF]: + cg.add_define("USE_FULL_PRINTF") + else: + for symbol in ["vprintf", "printf", "fprintf"]: + cg.add_build_flag(f"-Wl,--wrap={symbol}") else: cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") diff --git a/esphome/components/esp32/printf_stubs.cpp b/esphome/components/esp32/printf_stubs.cpp new file mode 100644 index 0000000000..5ee4650d50 --- /dev/null +++ b/esphome/components/esp32/printf_stubs.cpp @@ -0,0 +1,79 @@ +/* + * 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. + * + * 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() + * and printf() calls in SDK components are only in debug/assert paths + * (gpio_dump_io_configuration, ringbuf diagnostics) that are either + * GC'd or never called. + * + * These stubs redirect through vsnprintf() (which uses _svfprintf_r + * already in the binary) and fwrite(), allowing the linker to + * dead-code eliminate _vfprintf_r. + * + * Saves ~11 KB of flash. + * + * To disable these wraps, set enable_full_printf: true in the esp32 + * advanced config section. + */ + +#if defined(USE_ESP_IDF) && !defined(USE_FULL_PRINTF) +#include +#include + +#include "esp_system.h" + +static constexpr size_t PRINTF_BUFFER_SIZE = 512; + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +int __wrap_vprintf(const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + int len = vsnprintf(buf, sizeof(buf), fmt, ap); + if (len < 0) { + return len; + } + if (static_cast(len) >= sizeof(buf)) { + // Output was truncated — this should not happen in normal operation. + // Abort to make the issue visible rather than silently losing output. + esp_system_abort("printf buffer overflow; set enable_full_printf: true in esp32 advanced config"); + } + fwrite(buf, 1, len, stdout); + return len; +} + +int __wrap_printf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int len = __wrap_vprintf(fmt, ap); + va_end(ap); + return len; +} + +int __wrap_fprintf(FILE *stream, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + char buf[PRINTF_BUFFER_SIZE]; + int len = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + if (len < 0) { + return len; + } + if (static_cast(len) >= sizeof(buf)) { + esp_system_abort("fprintf buffer overflow; set enable_full_printf: true in esp32 advanced config"); + } + fwrite(buf, 1, len, stream); + return len; +} + +} // extern "C" +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_ESP_IDF && !USE_FULL_PRINTF