From 6feb0108f0d6e1de2d6017d8decef113a4d20e13 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 01:01:28 -1000 Subject: [PATCH] [esp8266] Wrap printf/vprintf/fprintf to eliminate _vfprintf_r (~900 bytes flash) Apply the same linker wrap technique used on ESP32 (PR #14362) to ESP8266. ESPHome logging uses ets_printf, not libc printf, so the FILE*-based printf path is dead code. The stubs redirect through vsnprintf + fwrite, allowing the linker to GC _vfprintf_r. Savings are smaller than ESP32 (~900 bytes vs ~11 KB) because ESP8266's newlib printf is more modular, but ESP8266 has much less flash headroom so every byte counts. --- esphome/components/esp8266/__init__.py | 5 ++ esphome/components/esp8266/printf_stubs.cpp | 71 +++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 esphome/components/esp8266/printf_stubs.cpp diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 927a59fd61..a3219f1910 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -260,6 +260,11 @@ async def to_code(config): if CORE.testing_mode: cg.add_build_flag("-DESPHOME_TESTING_MODE") + # Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r + # (~900 bytes). See printf_stubs.cpp for implementation. + for symbol in ("vprintf", "printf", "fprintf"): + cg.add_build_flag(f"-Wl,--wrap={symbol}") + cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE]) ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] diff --git a/esphome/components/esp8266/printf_stubs.cpp b/esphome/components/esp8266/printf_stubs.cpp new file mode 100644 index 0000000000..b0bbc3d697 --- /dev/null +++ b/esphome/components/esp8266/printf_stubs.cpp @@ -0,0 +1,71 @@ +/* + * Linker wrap stubs for FILE*-based printf functions. + * + * The ESP8266 Arduino framework and libraries may reference printf(), + * vprintf(), and fprintf() which pull in newlib's _vfprintf_r (~900 bytes). + * ESPHome never uses these — all logging goes through ets_printf/ets_vsnprintf + * directly, so the libc FILE*-based printf path is dead code. + * + * These stubs redirect through vsnprintf() (which is already in the binary + * for ESPHome's logging) and fwrite(), allowing the linker to dead-code + * eliminate _vfprintf_r. + * + * Saves ~900 bytes of flash. + */ + +#if defined(USE_ESP8266) && !defined(USE_FULL_PRINTF) +#include +#include +#include + +namespace esphome::esp8266 {} + +static constexpr size_t PRINTF_BUFFER_SIZE = 128; + +// These stubs are essentially dead code at runtime — ESPHome uses ets_printf +// for logging, and the Arduino core's Serial.printf() has its own implementation. +// The buffer overflow check is purely defensive and should never trigger. +static int write_printf_buffer(FILE *stream, char *buf, int len) { + if (len < 0) { + return len; + } + size_t write_len = len; + if (write_len >= PRINTF_BUFFER_SIZE) { + fwrite(buf, 1, PRINTF_BUFFER_SIZE - 1, stream); + abort(); + } + if (fwrite(buf, 1, write_len, stream) < write_len || ferror(stream)) { + return -1; + } + return len; +} + +// 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]; + return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + +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 = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); + va_end(ap); + return len; +} + +} // extern "C" +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_ESP8266 && !USE_FULL_PRINTF