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>
Wire Application::wake_loop_any_context() to socket::socket_wake()
on ESP8266. This allows any component calling wake_loop_any_context()
from an ISR or callback to immediately wake the main loop from
esp_delay() sleep, instead of waiting up to 16ms for the timeout.
Changes:
- Add IRAM_ATTR to socket_wake() so it's safe to call from ISR context
- Add ESP8266 wake_loop_any_context() backed by socket::socket_wake()
- Extend enable_loop_soon_any_context() to wake the loop on ESP8266
Each notify_*_update method had an identical loop that only differed
in the vtable offset for the virtual call. Extract the shared loop
into a single noinline notify() function and have each wrapper pass
a small trampoline lambda that dispatches to the correct Controller
virtual method.
Before: 8 copies of the loop at ~56 bytes each = ~448 bytes
After: 1 shared loop (56 B) + 8 wrappers (16 B) + 8 trampolines (8 B) = ~248 bytes
Saves ~200 bytes of flash.
LibreTiny lacks IRAM_ATTR support needed for ISR-safe paths,
and BK72xx doesn't support the mrs ipsr instruction in Thumb mode.
The existing fast select functionality on LibreTiny is unaffected.
API protobuf messages are never deleted through base class pointers —
they are always stack-allocated and passed by reference. The virtual
destructor occupies 2 vtable slots (complete + deleting destructor)
on 32-bit platforms using the Itanium ABI, costing 8 bytes per vtable.
With 122 message classes, removing the virtual destructor saves
~976 bytes of flash. Each vtable shrinks from 24 bytes to 16 bytes.
Wire Application::wake_loop_any_context() to socket::socket_wake()
on ESP8266. This allows any component calling wake_loop_any_context()
from an ISR or callback to immediately wake the main loop from
esp_delay() sleep, instead of waiting up to 16ms for the timeout.
Changes:
- Add IRAM_ATTR to socket_wake() so it's safe to call from ISR context
- Add ESP8266 wake_loop_any_context() backed by socket::socket_wake()
- Extend enable_loop_soon_any_context() to wake the loop on ESP8266