[socket] Cache lwip_sock pointers and inline ready() chain (#14408)

This commit is contained in:
J. Nick Koston
2026-03-03 07:03:02 -10:00
committed by GitHub
parent b6f0bb9b6b
commit d53ff7892a
10 changed files with 190 additions and 89 deletions
+17 -16
View File
@@ -11,11 +11,15 @@ namespace esphome::socket {
BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) {
this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
}
if (!monitor_loop || this->fd_ < 0)
return;
#ifdef USE_LWIP_FAST_SELECT
// Cache lwip_sock pointer and register for monitoring (hooks callback internally)
this->cached_sock_ = esphome_lwip_get_sock(this->fd_);
this->loop_monitored_ = App.register_socket(this->cached_sock_);
#else
this->loop_monitored_ = App.register_socket_fd(this->fd_);
#endif
}
BSDSocketImpl::~BSDSocketImpl() {
@@ -26,10 +30,17 @@ BSDSocketImpl::~BSDSocketImpl() {
int BSDSocketImpl::close() {
if (!this->closed_) {
// Unregister from select() before closing if monitored
// Unregister before closing to avoid dangling pointer in monitored set
#ifdef USE_LWIP_FAST_SELECT
if (this->loop_monitored_) {
App.unregister_socket(this->cached_sock_);
this->cached_sock_ = nullptr;
}
#else
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
#endif
int ret = ::close(this->fd_);
this->closed_ = true;
return ret;
@@ -48,8 +59,6 @@ int BSDSocketImpl::setblocking(bool blocking) {
return 0;
}
bool BSDSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); }
size_t BSDSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
@@ -86,14 +95,6 @@ std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol
return create_socket(domain, type, protocol, true);
}
std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, false);
}
std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, true);
}
} // namespace esphome::socket
#endif // USE_SOCKET_IMPL_BSD_SOCKETS
@@ -13,6 +13,10 @@
#include <lwip/sockets.h>
#endif
#ifdef USE_LWIP_FAST_SELECT
struct lwip_sock;
#endif
namespace esphome::socket {
class BSDSocketImpl {
@@ -105,6 +109,9 @@ class BSDSocketImpl {
protected:
int fd_{-1};
#ifdef USE_LWIP_FAST_SELECT
struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready()
#endif
bool closed_{false};
bool loop_monitored_{false};
};
+17 -16
View File
@@ -11,11 +11,15 @@ namespace esphome::socket {
LwIPSocketImpl::LwIPSocketImpl(int fd, bool monitor_loop) {
this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
}
if (!monitor_loop || this->fd_ < 0)
return;
#ifdef USE_LWIP_FAST_SELECT
// Cache lwip_sock pointer and register for monitoring (hooks callback internally)
this->cached_sock_ = esphome_lwip_get_sock(this->fd_);
this->loop_monitored_ = App.register_socket(this->cached_sock_);
#else
this->loop_monitored_ = App.register_socket_fd(this->fd_);
#endif
}
LwIPSocketImpl::~LwIPSocketImpl() {
@@ -26,10 +30,17 @@ LwIPSocketImpl::~LwIPSocketImpl() {
int LwIPSocketImpl::close() {
if (!this->closed_) {
// Unregister from select() before closing if monitored
// Unregister before closing to avoid dangling pointer in monitored set
#ifdef USE_LWIP_FAST_SELECT
if (this->loop_monitored_) {
App.unregister_socket(this->cached_sock_);
this->cached_sock_ = nullptr;
}
#else
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
#endif
int ret = lwip_close(this->fd_);
this->closed_ = true;
return ret;
@@ -48,8 +59,6 @@ int LwIPSocketImpl::setblocking(bool blocking) {
return 0;
}
bool LwIPSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); }
size_t LwIPSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
@@ -86,14 +95,6 @@ std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol
return create_socket(domain, type, protocol, true);
}
std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, false);
}
std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, true);
}
} // namespace esphome::socket
#endif // USE_SOCKET_IMPL_LWIP_SOCKETS
@@ -9,6 +9,10 @@
#include "esphome/core/helpers.h"
#include "headers.h"
#ifdef USE_LWIP_FAST_SELECT
struct lwip_sock;
#endif
namespace esphome::socket {
class LwIPSocketImpl {
@@ -71,6 +75,9 @@ class LwIPSocketImpl {
protected:
int fd_{-1};
#ifdef USE_LWIP_FAST_SELECT
struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready()
#endif
bool closed_{false};
bool loop_monitored_{false};
};
+5 -1
View File
@@ -8,7 +8,7 @@
namespace esphome::socket {
#ifdef USE_SOCKET_SELECT_SUPPORT
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT)
// Shared ready() implementation for fd-based socket implementations (BSD and LWIP sockets).
// Checks if the Application's select() loop has marked this fd as ready.
bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || App.is_socket_ready_(fd); }
@@ -89,6 +89,9 @@ std::unique_ptr<Socket> socket_ip(int type, int protocol) {
#endif /* USE_NETWORK_IPV6 */
}
#ifdef USE_SOCKET_IMPL_LWIP_TCP
// LWIP_TCP has separate Socket/ListenSocket types — needs out-of-line factory.
// BSD and LWIP_SOCKETS define this inline in socket.h.
std::unique_ptr<ListenSocket> socket_ip_loop_monitored(int type, int protocol) {
#if USE_NETWORK_IPV6
return socket_listen_loop_monitored(AF_INET6, type, protocol);
@@ -96,6 +99,7 @@ std::unique_ptr<ListenSocket> socket_ip_loop_monitored(int type, int protocol) {
return socket_listen_loop_monitored(AF_INET, type, protocol);
#endif /* USE_NETWORK_IPV6 */
}
#endif
socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_address, uint16_t port) {
#if USE_NETWORK_IPV6
+42 -3
View File
@@ -6,6 +6,10 @@
#include "esphome/core/optional.h"
#include "headers.h"
#ifdef USE_LWIP_FAST_SELECT
#include "esphome/core/lwip_fast_select.h"
#endif
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
// Include only the active implementation's header.
@@ -36,12 +40,29 @@ using Socket = LWIPRawImpl;
using ListenSocket = LWIPRawListenImpl;
#endif
#ifdef USE_SOCKET_SELECT_SUPPORT
#ifdef USE_LWIP_FAST_SELECT
/// Shared ready() helper using cached lwip_sock pointer for direct rcvevent read.
inline bool socket_ready(struct lwip_sock *cached_sock, bool loop_monitored) {
return !loop_monitored || (cached_sock != nullptr && esphome_lwip_socket_has_data(cached_sock));
}
#elif defined(USE_SOCKET_SELECT_SUPPORT)
/// Shared ready() helper for fd-based socket implementations.
/// Checks if the Application's select() loop has marked this fd as ready.
bool socket_ready_fd(int fd, bool loop_monitored);
#endif
// Inline ready() — defined here because it depends on socket_ready/socket_ready_fd
// declared above, while the impl headers are included before those declarations.
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
inline bool Socket::ready() const {
#ifdef USE_LWIP_FAST_SELECT
return socket_ready(this->cached_sock_, this->loop_monitored_);
#else
return socket_ready_fd(this->fd_, this->loop_monitored_);
#endif
}
#endif
/// Create a socket of the given domain, type and protocol.
std::unique_ptr<Socket> socket(int domain, int type, int protocol);
/// Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol.
@@ -56,11 +77,29 @@ std::unique_ptr<Socket> socket_ip(int type, int protocol);
std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol);
/// Create a listening socket of the given domain, type and protocol.
std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol);
/// Create a listening socket and monitor it for data in the main loop.
std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol);
/// Create a listening socket in the newest available IP domain and monitor it.
#ifdef USE_SOCKET_IMPL_LWIP_TCP
// LWIP_TCP has separate Socket/ListenSocket types — needs distinct factory functions.
std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol);
std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol);
std::unique_ptr<ListenSocket> socket_ip_loop_monitored(int type, int protocol);
#else
// BSD and LWIP_SOCKETS: Socket == ListenSocket, so listen variants just delegate.
inline std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol) {
return socket(domain, type, protocol);
}
inline std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol) {
return socket_loop_monitored(domain, type, protocol);
}
inline std::unique_ptr<ListenSocket> socket_ip_loop_monitored(int type, int protocol) {
#if USE_NETWORK_IPV6
return socket_loop_monitored(AF_INET6, type, protocol);
#else
return socket_loop_monitored(AF_INET, type, protocol);
#endif
}
#endif
/// Set a sockaddr to the specified address and port for the IP version used by socket_ip().
/// @param addr Destination sockaddr structure
+29 -14
View File
@@ -552,7 +552,32 @@ void Application::after_loop_tasks_() {
this->in_loop_ = false;
}
#ifdef USE_SOCKET_SELECT_SUPPORT
#ifdef USE_LWIP_FAST_SELECT
bool Application::register_socket(struct lwip_sock *sock) {
// It modifies monitored_sockets_ without locking — must only be called from the main loop.
if (sock == nullptr)
return false;
esphome_lwip_hook_socket(sock);
this->monitored_sockets_.push_back(sock);
return true;
}
void Application::unregister_socket(struct lwip_sock *sock) {
// It modifies monitored_sockets_ without locking — must only be called from the main loop.
for (size_t i = 0; i < this->monitored_sockets_.size(); i++) {
if (this->monitored_sockets_[i] != sock)
continue;
// Swap with last element and pop - O(1) removal since order doesn't matter.
// No need to unhook the netconn callback — all LwIP sockets share the same
// static event_callback, and the socket will be closed by the caller.
if (i < this->monitored_sockets_.size() - 1)
this->monitored_sockets_[i] = this->monitored_sockets_.back();
this->monitored_sockets_.pop_back();
return;
}
}
#elif defined(USE_SOCKET_SELECT_SUPPORT)
bool Application::register_socket_fd(int fd) {
// WARNING: This function is NOT thread-safe and must only be called from the main loop
// It modifies socket_fds_ and related variables without locking
@@ -571,15 +596,10 @@ bool Application::register_socket_fd(int fd) {
#endif
this->socket_fds_.push_back(fd);
#ifdef USE_LWIP_FAST_SELECT
// Hook the socket's netconn callback for instant wake on receive events
esphome_lwip_hook_socket(fd);
#else
this->socket_fds_changed_ = true;
if (fd > this->max_fd_) {
this->max_fd_ = fd;
}
#endif
return true;
}
@@ -595,13 +615,9 @@ void Application::unregister_socket_fd(int fd) {
continue;
// Swap with last element and pop - O(1) removal since order doesn't matter.
// No need to unhook the netconn callback on fast select platforms — all LwIP
// sockets share the same static event_callback, and the socket will be closed
// by the caller.
if (i < this->socket_fds_.size() - 1)
this->socket_fds_[i] = this->socket_fds_.back();
this->socket_fds_.pop_back();
#ifndef USE_LWIP_FAST_SELECT
this->socket_fds_changed_ = true;
// Only recalculate max_fd if we removed the current max
if (fd == this->max_fd_) {
@@ -611,7 +627,6 @@ void Application::unregister_socket_fd(int fd) {
this->max_fd_ = sock_fd;
}
}
#endif
return;
}
}
@@ -621,7 +636,7 @@ void Application::unregister_socket_fd(int fd) {
void Application::yield_with_select_(uint32_t delay_ms) {
// Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run.
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT)
// Fast path (ESP32/LibreTiny): reads rcvevent directly via lwip_socket_dbg_get_socket().
// Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers.
// Safe because this runs on the main loop which owns socket lifetime (create, read, close).
if (delay_ms == 0) [[unlikely]] {
yield();
@@ -632,8 +647,8 @@ void Application::yield_with_select_(uint32_t delay_ms) {
// If a socket still has unread data (rcvevent > 0) but the task notification was already
// consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency.
// This scan preserves select() semantics: return immediately when any fd is ready.
for (int fd : this->socket_fds_) {
if (esphome_lwip_socket_has_data(fd)) {
for (struct lwip_sock *sock : this->monitored_sockets_) {
if (esphome_lwip_socket_has_data(sock)) {
yield();
return;
}
+16 -16
View File
@@ -500,14 +500,20 @@ class Application {
Scheduler scheduler;
/// Register/unregister a socket file descriptor to be monitored for read events.
#ifdef USE_SOCKET_SELECT_SUPPORT
/// These functions update the fd_set used by select() in the main loop.
/// Register/unregister a socket to be monitored for read events.
/// WARNING: These functions are NOT thread-safe. They must only be called from the main loop.
#ifdef USE_LWIP_FAST_SELECT
/// Fast select path: hooks netconn callback and registers for monitoring.
/// @return true if registration was successful, false if sock is null
bool register_socket(struct lwip_sock *sock);
void unregister_socket(struct lwip_sock *sock);
#elif defined(USE_SOCKET_SELECT_SUPPORT)
/// Fallback select() path: monitors file descriptors.
/// NOTE: File descriptors >= FD_SETSIZE (typically 10 on ESP) will be rejected with an error.
/// @return true if registration was successful, false if fd exceeds limits
bool register_socket_fd(int fd);
void unregister_socket_fd(int fd);
#endif
#ifdef USE_WAKE_LOOP_THREADSAFE
/// Wake the main event loop from another FreeRTOS task.
@@ -532,7 +538,6 @@ class Application {
static void IRAM_ATTR wake_loop_any_context() { esphome_lwip_wake_main_loop_any_context(); }
#endif
#endif
#endif
#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)
/// Wake the main event loop from any context (ISR, thread, or main loop).
@@ -542,23 +547,14 @@ class Application {
protected:
friend Component;
#ifdef USE_SOCKET_SELECT_SUPPORT
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT)
friend bool socket::socket_ready_fd(int fd, bool loop_monitored);
#endif
friend void ::setup();
friend void ::original_setup();
#ifdef USE_SOCKET_SELECT_SUPPORT
/// Fast path for Socket::ready() via friendship - skips negative fd check.
/// Main loop only — with USE_LWIP_FAST_SELECT, reads rcvevent via
/// lwip_socket_dbg_get_socket(), which has no refcount; safe only because
/// the main loop owns socket lifetime (creates, reads, and closes sockets
/// on the same thread).
#ifdef USE_LWIP_FAST_SELECT
bool is_socket_ready_(int fd) const { return esphome_lwip_socket_has_data(fd); }
#else
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT)
bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); }
#endif
#endif
/// Register a component, detecting loop() override at compile time.
@@ -620,8 +616,12 @@ class Application {
// and active_end_ is incremented
// - This eliminates branch mispredictions from flag checking in the hot loop
FixedVector<Component *> looping_components_{};
#ifdef USE_SOCKET_SELECT_SUPPORT
#ifdef USE_LWIP_FAST_SELECT
std::vector<struct lwip_sock *> monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read
#elif defined(USE_SOCKET_SELECT_SUPPORT)
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
#endif
#ifdef USE_SOCKET_SELECT_SUPPORT
#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT)
int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks
#endif
+11 -17
View File
@@ -140,8 +140,10 @@
_Static_assert(sizeof(TaskHandle_t) <= 4, "TaskHandle_t must be <= 4 bytes for atomic access");
_Static_assert(sizeof(netconn_callback) <= 4, "netconn_callback must be <= 4 bytes for atomic access");
// rcvevent must fit in a single atomic read
_Static_assert(sizeof(((struct lwip_sock *) 0)->rcvevent) <= 4, "rcvevent must be <= 4 bytes for atomic access");
// rcvevent must be exactly 2 bytes (s16_t) — the inline in lwip_fast_select.h reads it as int16_t.
// If lwIP changes this to int or similar, the offset assert would still pass but the load width would be wrong.
_Static_assert(sizeof(((struct lwip_sock *) 0)->rcvevent) == 2,
"rcvevent size changed — update int16_t cast in esphome_lwip_socket_has_data() in lwip_fast_select.h");
// Struct member alignment — natural alignment guarantees atomicity on Xtensa/RISC-V/ARM.
// Misaligned access would not be atomic even if the size is <= 4 bytes.
@@ -150,6 +152,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;
@@ -194,23 +200,11 @@ static inline struct lwip_sock *get_sock(int fd) {
return sock;
}
bool esphome_lwip_socket_has_data(int fd) {
struct lwip_sock *sock = get_sock(fd);
if (sock == NULL)
return false;
// 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;
struct lwip_sock *esphome_lwip_get_sock(int fd) {
return get_sock(fd);
}
void esphome_lwip_hook_socket(int fd) {
struct lwip_sock *sock = get_sock(fd);
if (sock == NULL)
return;
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).
if (s_original_callback == NULL) {
+39 -6
View File
@@ -4,6 +4,17 @@
// Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications.
#include <stdbool.h>
#include <stdint.h>
// 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.
// Anonymous enum for a compile-time constant that works in both C and C++.
enum { ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET = 8 };
#ifdef __cplusplus
extern "C" {
@@ -13,16 +24,38 @@ extern "C" {
/// Saves the current task handle for xTaskNotifyGive() wake notifications.
void esphome_lwip_fast_select_init(void);
/// Check if a LwIP socket has data ready via direct rcvevent read (~215 ns per socket).
/// Uses lwip_socket_dbg_get_socket() — a direct array lookup without the refcount that
/// get_socket()/done_socket() uses. Safe because the caller owns the socket lifetime:
/// both has_data reads and socket close/unregister happen on the main loop thread.
bool esphome_lwip_socket_has_data(int fd);
/// Look up a LwIP socket struct from a file descriptor.
/// Returns NULL if fd is invalid or the socket/netconn is not initialized.
/// Use this at registration time to cache the pointer for esphome_lwip_socket_has_data().
struct lwip_sock *esphome_lwip_get_sock(int fd);
/// Check if a cached LwIP socket has data ready via unlocked hint read of rcvevent.
/// This avoids lwIP core lock contention between the main loop (CPU0) and
/// streaming/networking work (CPU1). Correctness is preserved because callers
/// already handle EWOULDBLOCK on nonblocking sockets — a stale hint simply causes
/// a harmless retry on the next loop iteration. In practice, stale reads have not
/// been observed across multi-day testing, but the design does not depend on that.
///
/// 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: inlined volatile 16-bit load — no function call overhead.
/// Uses offset-based access because lwip/priv/sockets_priv.h conflicts with C++.
/// The offset and size are verified at compile time in lwip_fast_select.c.
static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) {
// Unlocked hint read — no lwIP core lock needed.
// 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 + (int) 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().
/// Must be called from the main loop after socket creation.
void esphome_lwip_hook_socket(int fd);
/// The sock pointer must have been obtained from esphome_lwip_get_sock().
void esphome_lwip_hook_socket(struct lwip_sock *sock);
/// Wake the main loop task from another FreeRTOS task — costs <1 us.
/// NOT ISR-safe — must only be called from task context.