[esp8266] Wrap printf/vprintf/fprintf to eliminate _vfiprintf_r (~1.6 KB flash) (#14621)

This commit is contained in:
J. Nick Koston
2026-03-08 14:32:47 -10:00
committed by GitHub
parent c11ad7f0e6
commit e1c849d5d2
4 changed files with 85 additions and 0 deletions
+10
View File
@@ -23,6 +23,7 @@ from esphome.helpers import copy_file_if_changed
from .boards import BOARDS, ESP8266_LD_SCRIPTS
from .const import (
CONF_EARLY_PIN_INIT,
CONF_ENABLE_FULL_PRINTF,
CONF_ENABLE_SERIAL,
CONF_ENABLE_SERIAL1,
CONF_RESTORE_FROM_FLASH,
@@ -179,6 +180,7 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_ENABLE_SERIAL): cv.boolean,
cv.Optional(CONF_ENABLE_SERIAL1): cv.boolean,
cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean,
}
),
set_core_data,
@@ -260,6 +262,14 @@ 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 _vfiprintf_r
# (~1.6 KB). See printf_stubs.cpp for implementation.
if config.get(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}")
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
+1
View File
@@ -6,6 +6,7 @@ KEY_BOARD = "board"
KEY_PIN_INITIAL_STATES = "pin_initial_states"
CONF_RESTORE_FROM_FLASH = "restore_from_flash"
CONF_EARLY_PIN_INIT = "early_pin_init"
CONF_ENABLE_FULL_PRINTF = "enable_full_printf"
CONF_ENABLE_SERIAL = "enable_serial"
CONF_ENABLE_SERIAL1 = "enable_serial1"
KEY_FLASH_SIZE = "flash_size"
@@ -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 writes directly to the UART via
* Arduino's Serial, 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 ~1.6 KB of flash.
*/
#if defined(USE_ESP8266) && !defined(USE_FULL_PRINTF)
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
namespace esphome::esp8266 {}
static constexpr size_t PRINTF_BUFFER_SIZE = 512;
// These stubs are essentially dead code at runtime — ESPHome writes directly
// to the UART via Arduino's Serial, and 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
@@ -1,3 +1,6 @@
esp8266:
enable_full_printf: false
logger:
level: VERBOSE