mirror of
https://github.com/esphome/esphome.git
synced 2026-05-23 19:55:33 +08:00
[socket] Cache lwip_sock pointers and inline ready() chain (#14408)
This commit is contained in:
@@ -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};
|
||||
};
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user