diff --git a/esphome/components/api/api_buffer.cpp b/esphome/components/api/api_buffer.cpp new file mode 100644 index 00000000000..6db18b0365e --- /dev/null +++ b/esphome/components/api/api_buffer.cpp @@ -0,0 +1,13 @@ +#include "api_buffer.h" + +namespace esphome::api { + +void APIBuffer::grow_(size_t n) { + auto new_data = make_buffer(n); + if (this->size_) + std::memcpy(new_data.get(), this->data_.get(), this->size_); + this->data_ = std::move(new_data); + this->capacity_ = n; +} + +} // namespace esphome::api diff --git a/esphome/components/api/api_buffer.h b/esphome/components/api/api_buffer.h new file mode 100644 index 00000000000..00801e3ee58 --- /dev/null +++ b/esphome/components/api/api_buffer.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +namespace esphome::api { + +/// Helper to use make_unique_for_overwrite where available (skips zero-fill), +/// falling back to make_unique on older GCC (ESP8266, LibreTiny). +inline std::unique_ptr make_buffer(size_t n) { +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) + return std::make_unique(n); +#else + return std::make_unique_for_overwrite(n); +#endif +} + +/// Byte buffer that skips zero-initialization on resize(). +/// +/// std::vector::resize() zero-fills new bytes via memset. For the +/// shared protobuf write buffer, every byte is overwritten by the encoder, +/// making the zero-fill pure waste. For the receive buffer, bytes are +/// overwritten by socket reads. +/// +/// Designed for bulk clear/resize/overwrite patterns. grow_() allocates +/// exactly the requested size (no growth factor) since callers resize to +/// known sizes rather than appending incrementally. +/// +/// Safe because: callers always write exactly the number of bytes they +/// resize for. In the protobuf write path, debug_check_bounds_ validates +/// writes in debug builds. +class APIBuffer { + public: + void clear() { this->size_ = 0; } + inline void reserve(size_t n) ESPHOME_ALWAYS_INLINE { + if (n > this->capacity_) + this->grow_(n); + } + inline void resize(size_t n) ESPHOME_ALWAYS_INLINE { + this->reserve(n); + this->size_ = n; // no zero-fill + } + uint8_t *data() { return this->data_.get(); } + const uint8_t *data() const { return this->data_.get(); } + size_t size() const { return this->size_; } + bool empty() const { return this->size_ == 0; } + uint8_t &operator[](size_t i) { return this->data_[i]; } + const uint8_t &operator[](size_t i) const { return this->data_[i]; } + /// Release all memory (equivalent to std::vector swap trick). + void release() { + this->data_.reset(); + this->size_ = 0; + this->capacity_ = 0; + } + + protected: + void grow_(size_t n); + std::unique_ptr data_; + size_t size_{0}; + size_t capacity_{0}; +}; + +} // namespace esphome::api diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7bd5d5120b8..dea3ba5460b 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -2016,7 +2016,7 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode if (total_calculated_size > remaining_size) return 0; // Doesn't fit - std::vector &shared_buf = conn->parent_->get_shared_buffer_ref(); + auto &shared_buf = conn->parent_->get_shared_buffer_ref(); if (conn->flags_.batch_first_message) { // First message - buffer already prepared by caller, just clear flag @@ -2184,7 +2184,7 @@ void APIConnection::process_batch_() { // Separated from process_batch_() so the single-message fast path gets a minimal // stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array. -void APIConnection::process_batch_multi_(std::vector &shared_buf, size_t num_items, uint8_t header_padding, +void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size) { // Ensure MessageInfo remains trivially destructible for our placement new approach static_assert(std::is_trivially_destructible::value, diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index ccb51186d62..3356511684f 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -288,7 +288,7 @@ class APIConnection final : public APIServerConnectionBase { } } - void prepare_first_message_buffer(std::vector &shared_buf, size_t header_padding, size_t total_size) { + void prepare_first_message_buffer(APIBuffer &shared_buf, size_t header_padding, size_t total_size) { shared_buf.clear(); // Reserve space for header padding + message + footer // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) @@ -299,7 +299,7 @@ class APIConnection final : public APIServerConnectionBase { } // Convenience overload - computes frame overhead internally - void prepare_first_message_buffer(std::vector &shared_buf, size_t payload_size) { + void prepare_first_message_buffer(APIBuffer &shared_buf, size_t payload_size) { const uint8_t header_padding = this->helper_->frame_header_padding(); const uint8_t footer_size = this->helper_->frame_footer_size(); this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size); @@ -687,8 +687,8 @@ class APIConnection final : public APIServerConnectionBase { bool schedule_batch_(); void process_batch_(); - void process_batch_multi_(std::vector &shared_buf, size_t num_items, uint8_t header_padding, - uint8_t footer_size) __attribute__((noinline)); + void process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size) + __attribute__((noinline)); void clear_batch_() { this->deferred_batch_.clear(); this->flags_.batch_scheduled = false; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 151314658ea..98de24501ea 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -5,10 +5,10 @@ #include #include #include -#include #include "esphome/core/defines.h" #ifdef USE_API +#include "esphome/components/api/api_buffer.h" #include "esphome/components/socket/socket.h" #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -178,8 +178,7 @@ class APIFrameHelper { // rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame // and clearing would lose partially received data. if (this->rx_buf_len_ == 0) { - // Use swap trick since shrink_to_fit() is non-binding and may be ignored - std::vector().swap(this->rx_buf_); + this->rx_buf_.release(); } } @@ -206,9 +205,6 @@ class APIFrameHelper { // Common socket write error handling APIError handle_socket_write_error_(); - template - APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, - const std::string &info, StateEnum &state, StateEnum failed_state); // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) std::unique_ptr socket_; @@ -245,7 +241,7 @@ class APIFrameHelper { // Containers (size varies, but typically 12+ bytes on 32-bit) std::array, API_MAX_SEND_QUEUE> tx_buf_; - std::vector rx_buf_; + APIBuffer rx_buf_; // Client name buffer - stores name from Hello message or initial peername char client_name_[CLIENT_INFO_NAME_MAX_LEN]{}; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 256357ce6a7..3e6ecf9dc30 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -207,9 +207,7 @@ APIError APINoiseFrameHelper::try_read_frame_() { // During handshake, rx_buf_.size() is used in prologue construction, so // the buffer must be exactly msg_size to avoid prologue mismatch.) uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0); - if (this->rx_buf_.size() != alloc_size) { - this->rx_buf_.resize(alloc_size); - } + this->rx_buf_.resize(alloc_size); if (rx_buf_len_ < msg_size) { // more data to read @@ -571,8 +569,7 @@ APIError APINoiseFrameHelper::init_handshake_() { if (aerr != APIError::OK) return aerr; // set_prologue copies it into handshakestate, so we can get rid of it now - // Use swap idiom to actually release memory (= {} only clears size, not capacity) - std::vector().swap(prologue_); + prologue_.release(); err = noise_handshakestate_start(handshake_); aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED); diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 183b8c8a51d..83410febb26 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -43,8 +43,8 @@ class APINoiseFrameHelper final : public APIFrameHelper { // Reference to noise context (4 bytes on 32-bit) APINoiseContext &ctx_; - // Vector (12 bytes on 32-bit) - std::vector prologue_; + // Buffer for noise handshake prologue (released after handshake) + APIBuffer prologue_; // NoiseProtocolId (size depends on implementation) NoiseProtocolId nid_; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 793cece3b82..007da7ef2b1 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -165,9 +165,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { // Reserve space for body (+ null terminator so protobuf StringRef fields // can be safely null-terminated in-place after decode) - if (this->rx_buf_.size() != this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR) { - this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR); - } + this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR); if (rx_buf_len_ < rx_header_parsed_len_) { // more data to read diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index e5f371d8a13..69fc26cc00c 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #ifdef USE_API +#include "api_buffer.h" #include "api_noise_context.h" #include "api_pb2.h" #include "api_pb2_service.h" @@ -65,7 +66,7 @@ class APIServer : public Component, void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; } // Get reference to shared buffer for API connections - std::vector &get_shared_buffer_ref() { return shared_write_buffer_; } + APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; } #ifdef USE_API_NOISE bool save_noise_psk(psk_t psk, bool make_active = true); @@ -276,7 +277,7 @@ class APIServer : public Component, // Not pre-allocated: all send paths call prepare_first_message_buffer() which // reserves the exact needed size. Pre-allocating here would cause heap fragmentation // since the buffer would almost always reallocate on first use. - std::vector shared_write_buffer_; + APIBuffer shared_write_buffer_; #ifdef USE_API_HOMEASSISTANT_STATES std::vector state_subs_; #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 7050efb4460..d1c955b1fb9 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -1,6 +1,7 @@ #pragma once #include "api_pb2_defines.h" +#include "api_buffer.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -203,9 +204,8 @@ class Proto32Bit { class ProtoWriteBuffer { public: - ProtoWriteBuffer(std::vector *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} - ProtoWriteBuffer(std::vector *buffer, size_t write_pos) - : buffer_(buffer), pos_(buffer->data() + write_pos) {} + ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} + ProtoWriteBuffer(APIBuffer *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {} inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) { if (value < 128) [[likely]] { this->debug_check_bounds_(1); @@ -340,7 +340,7 @@ class ProtoWriteBuffer { // Non-template core for encode_optional_sub_message. void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &)); - std::vector *get_buffer() const { return buffer_; } + APIBuffer *get_buffer() const { return buffer_; } protected: // Slow path for encode_varint_raw values >= 128, outlined to keep fast path small @@ -353,7 +353,7 @@ class ProtoWriteBuffer { void debug_check_bounds_([[maybe_unused]] size_t bytes) {} #endif - std::vector *buffer_; + APIBuffer *buffer_; uint8_t *pos_; }; diff --git a/tests/components/api/test.ln882x-ard.yaml b/tests/components/api/test.ln882x-ard.yaml new file mode 100644 index 00000000000..46c01d926f2 --- /dev/null +++ b/tests/components/api/test.ln882x-ard.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +wifi: + ssid: MySSID + password: password1