[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.
This commit is contained in:
J. Nick Koston
2026-02-27 12:35:41 -10:00
parent b9d70dcda2
commit af1a00ccf9
2 changed files with 89 additions and 0 deletions

View File

@@ -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")

View File

@@ -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 <cstdarg>
#include <cstdio>
#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<size_t>(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<size_t>(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