Cache struct lwip_sock* pointers at socket registration time instead of
looking them up by fd on every iteration. The previous code stored file
descriptors and called lwip_socket_dbg_get_socket(fd) on every loop
iteration to convert each fd back to a pointer — only to read a single
field from it.
- register_socket(lwip_sock*) returns bool and hooks the callback
internally — replaces the separate register_socket_fd + hook dance
- Hot loop iterates struct lwip_sock* directly instead of converting
fds each iteration
- Socket::ready() uses the cached pointer instead of fd lookup
- esphome_lwip_hook_socket() takes struct lwip_sock* directly
- esphome_lwip_socket_has_data() moved to header as static inline
for inlining — uses offset-based access verified by _Static_assert
- Null cached_sock_ on close to prevent post-close dereference
- socket_ready() tolerant of null pointer for safety
Cache struct lwip_sock* pointers at socket registration time instead of
looking them up by fd on every iteration. The previous code stored file
descriptors and called lwip_socket_dbg_get_socket(fd) on every loop
iteration to convert each fd back to a pointer — only to read a single
field from it.
- register_socket(lwip_sock*) returns bool and hooks the callback
internally — replaces the separate register_socket_fd + hook dance
- Hot loop iterates struct lwip_sock* directly instead of converting
fds each iteration
- Socket::ready() uses the cached pointer instead of fd lookup
- esphome_lwip_hook_socket() takes struct lwip_sock* directly
- esphome_lwip_socket_has_data() moved to header as static inline
for inlining — uses offset-based access verified by _Static_assert
- Null cached_sock_ on close to prevent post-close dereference
- socket_ready() tolerant of null pointer for safety
Cache struct lwip_sock* pointers at socket registration time instead of
looking them up by fd on every iteration. The previous code stored file
descriptors and called lwip_socket_dbg_get_socket(fd) on every loop
iteration to convert each fd back to a pointer — only to read a single
field from it.
- register_socket(lwip_sock*) returns bool and hooks the callback
internally — replaces the separate register_socket_fd + hook dance
- Hot loop iterates struct lwip_sock* directly instead of converting
fds each iteration
- Socket::ready() uses the cached pointer instead of fd lookup
- esphome_lwip_hook_socket() takes struct lwip_sock* directly
- esphome_lwip_socket_has_data() moved to header as static inline
for inlining — uses offset-based access verified by _Static_assert
- Null cached_sock_ on close to prevent post-close dereference
- socket_ready() tolerant of null pointer for safety
Cache struct lwip_sock* pointers at socket registration time instead of
looking them up by fd on every iteration. The previous code stored file
descriptors and called lwip_socket_dbg_get_socket(fd) on every loop
iteration to convert each fd back to a pointer — only to read a single
field from it.
- register_socket(lwip_sock*) returns bool and hooks the callback
internally — replaces the separate register_socket_fd + hook dance
- Hot loop iterates struct lwip_sock* directly instead of converting
fds each iteration
- Socket::ready() uses the cached pointer instead of fd lookup
- esphome_lwip_hook_socket() takes struct lwip_sock* directly
- Null cached_sock_ on close to prevent post-close dereference
- socket_ready() tolerant of null pointer for safety
In yield_with_select_(), the fast select path iterated all monitored
sockets and called esphome_lwip_socket_has_data(fd) for each one.
That C function called lwip_socket_dbg_get_socket(fd) — a cross-
compilation-unit call that can't be inlined because sockets[] is
static in lwip's sockets.c. The actual rcvevent check is ~4
instructions, but the lookup adds entry/retw window overhead on
Xtensa (~8-12 cycles per socket per loop iteration).
Now lwip_sock* pointers are cached at socket registration time and
stored directly in monitored_sockets_. The hot path iterates pointers
and reads rcvevent with no fd lookup. esphome_lwip_hook_socket() also
takes lwip_sock* directly, avoiding a redundant get_sock() call.
API split for fast select vs fallback select:
- register_socket(lwip_sock*) / unregister_socket(lwip_sock*)
- register_socket_fd(int fd) / unregister_socket_fd(int fd)
Chained on top of socket-devirtualize (#14398).
Emit a static constexpr in main.cpp that uses std::is_same_v to count
components with overridden loop() at C++ compile time. Pre-init the
FixedVector with the exact capacity so calculate_looping_components_()
can skip its counting pass entirely.
Replace the runtime GCC-specific pointer-to-member-function comparison
in has_overridden_loop() with a compile-time std::is_same_v check in a
templated register_component_().
The template resolves &T::loop vs &Component::loop at compile time and
passes the result to register_component_impl_(), which stores it as
bit 5 in the existing component_state_ byte (zero additional RAM).
This eliminates:
- The non-standard -Wpmf-conversions GCC extension
- The USE_HOST/CLANG_TIDY special case that incorrectly returned true
for all components (putting every component in the loop list)
- Runtime vtable probing during calculate_looping_components_()
tcp_abort()/tcp_abandon() asserts pcb->state != LISTEN and accesses
fields beyond tcp_pcb_listen. Use tcp_close() instead and null pcb_
so the base destructor skips tcp_abort.
tcp_listen_with_backlog() converts the full tcp_pcb to a smaller
tcp_pcb_listen struct that lacks the errf field. Calling tcp_err()
on a listen PCB writes past the struct boundary, causing memory
corruption and crashes (confirmed on RP2040).
LWIP itself asserts pcb->state != LISTEN in tcp_err().
The destructor relies solely on tcp_abort() for cleanup, but LWIP
considers ownership of pbufs transferred once the recv callback
accepts them. If rx_buf_ is non-null when the socket is destroyed,
those pbufs are leaked. Free them explicitly before the base class
destructor calls tcp_abort().
Fix two pre-existing bugs found during review of #14398:
1. bind() returns 0 on nullptr name instead of -1, inconsistent with
POSIX bind() semantics and can mask invalid calls.
2. listen() doesn't re-register tcp_err after tcp_listen_with_backlog
reallocates the PCB. The error callback was set during init() on
the original PCB which is now freed, leaving the new listen PCB
without an error handler.
clang-tidy flags the forward declaration in application.h as redundant
since socket.h (included earlier via esphome.h) already declares it.
The forward declaration is needed here for the friend declaration.
- Rename internal_write/internal_output to internal_write_/internal_output_
(protected methods require trailing underscore)
- Fix getpeername/getsockname parameter name mismatch between .h and .cpp
The CI lint rule disallows ESP_LOG* references in header files.
Move LWIP_LOG macro definition and all methods that use it
(destructors, init(), static callbacks) to the .cpp file.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Replace the virtual Socket base class with compile-time type aliases.
Since only one socket implementation is active per build (selected via
#ifdef), virtual dispatch was unnecessary overhead.
Each implementation (BSDSocketImpl, LwIPSocketImpl, LWIPRawImpl) is now
a concrete class in its own header with no virtual methods. socket::Socket
and socket::ListenSocket are type aliases to the active implementation.
For LWIP_TCP (ESP8266/RP2040), the listen socket is a separate type
(LWIPRawListenImpl) sharing a non-virtual LWIPRawCommon base with the
connected socket type (LWIPRawImpl). On BSD/LWIP_SOCKETS, both aliases
resolve to the same type.
This eliminates all vtable overhead, virtual destructors, and indirect
call overhead across all platforms.
Co-Authored-By: J. Nick Koston <nick@koston.org>