diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index 2e191fff78..4afdaea7e1 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -150,6 +150,10 @@ _Static_assert(offsetof(struct netconn, callback) % sizeof(netconn_callback) == _Static_assert(offsetof(struct lwip_sock, rcvevent) % sizeof(((struct lwip_sock *) 0)->rcvevent) == 0, "lwip_sock.rcvevent must be naturally aligned for atomic access"); +// Verify the hardcoded offset used in the header's inline esphome_lwip_socket_has_data(). +_Static_assert(offsetof(struct lwip_sock, rcvevent) == ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET, + "lwip_sock.rcvevent offset changed — update ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET in lwip_fast_select.h"); + // Task handle for the main loop — written once in init(), read from TCP/IP and background tasks. static TaskHandle_t s_main_loop_task = NULL; @@ -198,17 +202,6 @@ struct lwip_sock *esphome_lwip_get_sock(int fd) { return get_sock(fd); } -// Hot path: caller has already cached the lwip_sock pointer at registration time. -// No fd lookup, no null checks — just a volatile 16-bit load of rcvevent. -bool esphome_lwip_socket_has_data(struct lwip_sock *sock) { - // volatile prevents the compiler from caching/reordering this cross-thread read. - // The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a - // FreeRTOS mutex (ESP32) or resumes the scheduler (LibreTiny), ensuring the value - // is visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH/LDRH) on - // Xtensa/RISC-V/ARM and cannot produce torn values. - return *(volatile s16_t *) &sock->rcvevent > 0; -} - void esphome_lwip_hook_socket(struct lwip_sock *sock) { // Save original callback once — all LwIP sockets share the same static event_callback // (DEFAULT_SOCKET_EVENTCB in sockets.c, used for SOCK_RAW, SOCK_DGRAM, and SOCK_STREAM). diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index ac9f94b962..3c7afb2e37 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -4,6 +4,16 @@ // Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications. #include +#include + +// Forward declare lwip_sock for C++ callers that store cached pointers. +// The full definition is only available in the .c file (lwip/priv/sockets_priv.h +// conflicts with C++ compilation units). +struct lwip_sock; + +// Byte offset of rcvevent (s16_t) within struct lwip_sock. +// Verified at compile time in lwip_fast_select.c via _Static_assert. +#define ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET 8 // Forward declare lwip_sock for C++ callers that store cached pointers. // The full definition is only available in the .c file (lwip/priv/sockets_priv.h @@ -26,8 +36,17 @@ struct lwip_sock *esphome_lwip_get_sock(int fd); /// Check if a cached LwIP socket has data ready via direct rcvevent read. /// The sock pointer must have been obtained from esphome_lwip_get_sock() and must /// remain valid (caller owns socket lifetime — no concurrent close). -/// Hot path: no fd lookup, no null checks — just a volatile 16-bit load. -bool esphome_lwip_socket_has_data(struct lwip_sock *sock); +/// Hot path: inlined volatile 16-bit load — no function call overhead. +/// Uses offset-based access because lwip/priv/sockets_priv.h conflicts with C++. +/// The offset is verified at compile time in lwip_fast_select.c. +static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) { + // volatile prevents the compiler from caching/reordering this cross-thread read. + // The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a + // FreeRTOS mutex (ESP32) or resumes the scheduler (LibreTiny), ensuring the value + // is visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH/LDRH) on + // Xtensa/RISC-V/ARM and cannot produce torn values. + return *(volatile int16_t *) ((char *) sock + ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET) > 0; +} /// Hook a socket's netconn callback to notify the main loop task on receive events. /// Wraps the original event_callback with one that also calls xTaskNotifyGive().