From 4a6eb0b16d74bc48ab62b2e6d3a1511ca453243d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Feb 2026 12:30:20 -0600 Subject: [PATCH] [socket] Devirtualize Socket::ready() and get_fd() for hot loop path Move fd_, closed_, and loop_monitored_ fields from BSD/LWIP socket implementations to the base Socket class. Since only one socket implementation is active per build, these can be non-virtual. Make Socket::ready() and get_fd() non-virtual, eliminating vtable dispatch on every main loop iteration. Inline is_socket_ready via friendship for the fast path while keeping the public API with bounds checking for external callers. Saves ~316 bytes of flash on ESP32-IDF builds. --- .../components/socket/bsd_sockets_impl.cpp | 29 ++----------------- .../components/socket/lwip_sockets_impl.cpp | 29 ++----------------- esphome/components/socket/socket.cpp | 8 +++++ esphome/components/socket/socket.h | 22 ++++++++++---- esphome/core/application.cpp | 9 ------ esphome/core/application.h | 16 +++++++++- 6 files changed, 44 insertions(+), 69 deletions(-) diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index b670b9c068..c96713f376 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -16,19 +16,13 @@ namespace esphome::socket { class BSDSocketImpl final : public Socket { public: - BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { -#ifdef USE_SOCKET_SELECT_SUPPORT + BSDSocketImpl(int fd, bool monitor_loop = false) { + 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_); - } else { - this->loop_monitored_ = false; } -#else - // Without select support, ignore monitor_loop parameter - (void) monitor_loop; -#endif } ~BSDSocketImpl() override { if (!this->closed_) { @@ -52,12 +46,10 @@ class BSDSocketImpl final : public Socket { int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); } int close() override { if (!this->closed_) { -#ifdef USE_SOCKET_SELECT_SUPPORT // Unregister from select() before closing if monitored if (this->loop_monitored_) { App.unregister_socket_fd(this->fd_); } -#endif int ret = ::close(this->fd_); this->closed_ = true; return ret; @@ -130,23 +122,6 @@ class BSDSocketImpl final : public Socket { ::fcntl(this->fd_, F_SETFL, fl); return 0; } - - int get_fd() const override { return this->fd_; } - -#ifdef USE_SOCKET_SELECT_SUPPORT - bool ready() const override { - if (!this->loop_monitored_) - return true; - return App.is_socket_ready(this->fd_); - } -#endif - - protected: - int fd_; - bool closed_{false}; -#ifdef USE_SOCKET_SELECT_SUPPORT - bool loop_monitored_{false}; -#endif }; // Helper to create a socket with optional monitoring diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp index a885f243f3..79d68e085a 100644 --- a/esphome/components/socket/lwip_sockets_impl.cpp +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -11,19 +11,13 @@ namespace esphome::socket { class LwIPSocketImpl final : public Socket { public: - LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { -#ifdef USE_SOCKET_SELECT_SUPPORT + LwIPSocketImpl(int fd, bool monitor_loop = false) { + 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_); - } else { - this->loop_monitored_ = false; } -#else - // Without select support, ignore monitor_loop parameter - (void) monitor_loop; -#endif } ~LwIPSocketImpl() override { if (!this->closed_) { @@ -49,12 +43,10 @@ class LwIPSocketImpl final : public Socket { int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); } int close() override { if (!this->closed_) { -#ifdef USE_SOCKET_SELECT_SUPPORT // Unregister from select() before closing if monitored if (this->loop_monitored_) { App.unregister_socket_fd(this->fd_); } -#endif int ret = lwip_close(this->fd_); this->closed_ = true; return ret; @@ -97,23 +89,6 @@ class LwIPSocketImpl final : public Socket { lwip_fcntl(this->fd_, F_SETFL, fl); return 0; } - - int get_fd() const override { return this->fd_; } - -#ifdef USE_SOCKET_SELECT_SUPPORT - bool ready() const override { - if (!this->loop_monitored_) - return true; - return App.is_socket_ready(this->fd_); - } -#endif - - protected: - int fd_; - bool closed_{false}; -#ifdef USE_SOCKET_SELECT_SUPPORT - bool loop_monitored_{false}; -#endif }; // Helper to create a socket with optional monitoring diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index fd8725b363..e4dfe91cc5 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -10,6 +10,14 @@ namespace esphome::socket { Socket::~Socket() {} +bool Socket::ready() const { +#ifdef USE_SOCKET_SELECT_SUPPORT + return !this->loop_monitored_ || App.is_socket_ready_(this->fd_); +#else + return true; +#endif +} + // Platform-specific inet_ntop wrappers #if defined(USE_SOCKET_IMPL_LWIP_TCP) // LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index e8b0948acd..abb2ddb589 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -63,13 +63,25 @@ class Socket { virtual int setblocking(bool blocking) = 0; virtual int loop() { return 0; }; - /// Get the underlying file descriptor (returns -1 if not supported) - virtual int get_fd() const { return -1; } + /// Get the underlying file descriptor (returns -1 if not supported) + /// Non-virtual: only one socket implementation is active per build. +#ifdef USE_SOCKET_SELECT_SUPPORT + int get_fd() const { return this->fd_; } +#else + int get_fd() const { return -1; } +#endif - /// Check if socket has data ready to read - /// For loop-monitored sockets, checks with the Application's select() results + /// Check if socket has data ready to read (non-virtual for direct call) + /// For loop-monitored sockets, checks the Application's select() results /// For non-monitored sockets, always returns true (assumes data may be available) - virtual bool ready() const { return true; } + bool ready() const; + + protected: +#ifdef USE_SOCKET_SELECT_SUPPORT + int fd_{-1}; + bool closed_{false}; + bool loop_monitored_{false}; +#endif }; /// Create a socket of the given domain, type and protocol. diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 0daabc0282..4dbd789c19 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -605,15 +605,6 @@ void Application::unregister_socket_fd(int fd) { } } -bool Application::is_socket_ready(int fd) const { - // This function is thread-safe for reading the result of select() - // However, it should only be called after select() has been executed in the main loop - // The read_fds_ is only modified by select() in the main loop - if (fd < 0 || fd >= FD_SETSIZE) - return false; - - return FD_ISSET(fd, &this->read_fds_); -} #endif void Application::yield_with_select_(uint32_t delay_ms) { diff --git a/esphome/core/application.h b/esphome/core/application.h index 592bf809f1..aca2620281 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -101,6 +101,10 @@ #include "esphome/components/update/update_entity.h" #endif +namespace esphome::socket { +class Socket; +} // namespace esphome::socket + namespace esphome { // Teardown timeout constant (in milliseconds) @@ -491,7 +495,8 @@ class Application { void unregister_socket_fd(int fd); /// Check if there's data available on a socket without blocking /// This function is thread-safe for reading, but should be called after select() has run - bool is_socket_ready(int fd) const; + /// The read_fds_ is only modified by select() in the main loop + bool is_socket_ready(int fd) const { return fd >= 0 && this->is_socket_ready_(fd); } #ifdef USE_WAKE_LOOP_THREADSAFE /// Wake the main event loop from a FreeRTOS task @@ -503,6 +508,15 @@ class Application { protected: friend Component; + friend class socket::Socket; + +#ifdef USE_SOCKET_SELECT_SUPPORT + /// Fast path for Socket::ready() via friendship - skips negative fd check. + /// Safe because: fd was validated in register_socket_fd() at registration time, + /// and Socket::ready() only calls this when loop_monitored_ is true (registration succeeded). + /// FD_ISSET may include its own upper bounds check depending on platform. + bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } +#endif void register_component_(Component *comp);