From 69d3f563d5e3176bea6015ea53ac332656422ceb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 15:43:36 -1000 Subject: [PATCH 01/12] [core] Replace std::vector in CallbackManager with trivial-copy container Replace std::vector> with a minimal container that exploits Callback's trivially-copyable nature. Since Callback is just {fn_ptr, ctx_ptr}, reallocation is a plain __builtin_memcpy instead of std::vector's exception-safe move/copy machinery. Uses uint16_t for size/capacity (8 bytes on 32-bit vs 12 for std::vector), and grows to exact size since callbacks are registered during setup() and most instances have 0-1 callbacks. Eliminates _M_realloc_insert/_M_default_append template instantiations per callback signature type. --- esphome/core/helpers.h | 72 +++++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 82c6b3833c..1557ca0790 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1785,30 +1785,78 @@ template struct Callback { template class CallbackManager; /** Helper class to allow having multiple subscribers to a callback. + * + * Uses a trivial-copyable-specialized container instead of std::vector to avoid + * template bloat (_M_realloc_insert, exception-safe copies). Since Callback is + * trivially copyable (just {fn_ptr, ctx_ptr}), reallocation is a plain memcpy. + * Uses uint16_t for size/capacity (8 bytes on 32-bit vs 12 for std::vector). + * Grows to exact size on each add — callbacks are registered during setup() + * and most instances have only 1-2 callbacks, so slack capacity is wasteful. * * @tparam Ts The arguments for the callbacks, wrapped in void(). */ template class CallbackManager { + using CbType = Callback; + static_assert(std::is_trivially_copyable_v, "Callback must be trivially copyable"); + public: + CallbackManager() = default; + ~CallbackManager() { delete[] this->data_; } + + // Non-copyable (would alias data_), movable (for std::map support) + CallbackManager(const CallbackManager &) = delete; + CallbackManager &operator=(const CallbackManager &) = delete; + CallbackManager(CallbackManager &&other) noexcept + : data_(other.data_), size_(other.size_), capacity_(other.capacity_) { + other.data_ = nullptr; + other.size_ = 0; + other.capacity_ = 0; + } + CallbackManager &operator=(CallbackManager &&other) noexcept { + if (this != &other) { + delete[] this->data_; + this->data_ = other.data_; + this->size_ = other.size_; + this->capacity_ = other.capacity_; + other.data_ = nullptr; + other.size_ = 0; + other.capacity_ = 0; + } + return *this; + } + /// Add any callable. Small trivially-copyable callables (like [this] lambdas) /// are stored inline without heap allocation or std::function. - template void add(F &&callback) { this->add_(Callback::create(std::forward(callback))); } - - /// Call all callbacks in this manager. No null check on invoke. - void call(Ts... args) { - for (auto &cb : this->callbacks_) - cb.call(args...); - } - size_t size() const { return this->callbacks_.size(); } + template void add(F &&callback) { this->add_(CbType::create(std::forward(callback))); } /// Call all callbacks in this manager. - void operator()(Ts... args) { call(args...); } + void call(Ts... args) { + for (uint16_t i = 0; i < this->size_; i++) + this->data_[i].call(args...); + } + uint16_t size() const { return this->size_; } + + /// Call all callbacks in this manager. + void operator()(Ts... args) { this->call(args...); } protected: template friend class LazyCallbackManager; /// Non-template core to avoid code duplication per lambda type. - void add_(Callback cb) { this->callbacks_.push_back(cb); } - std::vector> callbacks_; + void add_(CbType cb) { + if (this->size_ == this->capacity_) { + auto *new_data = new CbType[this->size_ + 1]; + if (this->data_) { + __builtin_memcpy(new_data, this->data_, this->size_ * sizeof(CbType)); + delete[] this->data_; + } + this->data_ = new_data; + this->capacity_ = this->size_ + 1; + } + this->data_[this->size_++] = cb; + } + CbType *data_{nullptr}; + uint16_t size_{0}; + uint16_t capacity_{0}; }; template class LazyCallbackManager; @@ -1820,7 +1868,7 @@ template class LazyCallbackManager; * from API and web_server components). * * Memory overhead comparison (32-bit systems): - * - CallbackManager: 12 bytes (empty std::vector) + * - CallbackManager: 8 bytes (pointer + uint16 size + uint16 capacity) * - LazyCallbackManager: 4 bytes (nullptr pointer) * * Uses plain pointer instead of unique_ptr to avoid template instantiation overhead. From 29b5b3eba1837820251564057e0d2e727db1f5ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 15:48:52 -1000 Subject: [PATCH 02/12] Move CallbackManager growth path out-of-line Split add_() into inline fast path (size check + store) and noinline cold grow_() for the allocation/memcpy/delete path. Prevents the cold growth code from bloating every call site. --- esphome/core/helpers.h | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 1557ca0790..99eabb704f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1842,23 +1842,30 @@ template class CallbackManager { protected: template friend class LazyCallbackManager; /// Non-template core to avoid code duplication per lambda type. + /// Inline fast path; cold growth path is out-of-line in grow_(). void add_(CbType cb) { - if (this->size_ == this->capacity_) { - auto *new_data = new CbType[this->size_ + 1]; - if (this->data_) { - __builtin_memcpy(new_data, this->data_, this->size_ * sizeof(CbType)); - delete[] this->data_; - } - this->data_ = new_data; - this->capacity_ = this->size_ + 1; - } + if (this->size_ == this->capacity_) + this->grow_(); this->data_[this->size_++] = cb; } + /// Out-of-line cold path: allocate exact size needed. + void grow_(); CbType *data_{nullptr}; uint16_t size_{0}; uint16_t capacity_{0}; }; +/// Out-of-line cold growth path for CallbackManager::add_(). +template __attribute__((noinline, cold)) void CallbackManager::grow_() { + auto *new_data = new CbType[this->size_ + 1]; + if (this->data_) { + __builtin_memcpy(new_data, this->data_, this->size_ * sizeof(CbType)); + delete[] this->data_; + } + this->data_ = new_data; + this->capacity_ = this->size_ + 1; +} + template class LazyCallbackManager; /** Lazy-allocating callback manager that only allocates memory when callbacks are registered. From 1bdf6402a2f83029553d4df112a3a134534c61a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 15:52:33 -1000 Subject: [PATCH 03/12] Move grow path to helpers.cpp as non-template function Since Callback is trivially copyable regardless of signature, the grow logic is type-independent. Move it to a concrete function in helpers.cpp using void*/memcpy, eliminating the template definition from the header entirely. --- esphome/core/helpers.cpp | 12 ++++++++++++ esphome/core/helpers.h | 21 ++++++--------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 1732fc72e8..8c97a03b12 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -22,6 +22,18 @@ namespace esphome { static const char *const TAG = "helpers"; +__attribute__((noinline, cold)) void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity, + size_t elem_size) { + uint16_t new_cap = size + 1; + auto *new_data = ::operator new(new_cap *elem_size); + if (data) { + __builtin_memcpy(new_data, data, size * elem_size); + ::operator delete(data); + } + capacity = new_cap; + return new_data; +} + static const uint16_t CRC16_A001_LE_LUT_L[] = {0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440}; static const uint16_t CRC16_A001_LE_LUT_H[] = {0x0000, 0xcc01, 0xd801, 0x1400, 0xf001, 0x3c00, 0x2800, 0xe401, diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 99eabb704f..a2b33dd7b1 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1782,6 +1782,9 @@ template struct Callback { } }; +/// Grow a CallbackManager's backing array to exactly size+1. Defined in helpers.cpp. +void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity, size_t elem_size); + template class CallbackManager; /** Helper class to allow having multiple subscribers to a callback. @@ -1842,30 +1845,18 @@ template class CallbackManager { protected: template friend class LazyCallbackManager; /// Non-template core to avoid code duplication per lambda type. - /// Inline fast path; cold growth path is out-of-line in grow_(). + /// Inline fast path; cold growth path is in helpers.cpp via callback_manager_grow(). void add_(CbType cb) { if (this->size_ == this->capacity_) - this->grow_(); + this->data_ = + static_cast(callback_manager_grow(this->data_, this->size_, this->capacity_, sizeof(CbType))); this->data_[this->size_++] = cb; } - /// Out-of-line cold path: allocate exact size needed. - void grow_(); CbType *data_{nullptr}; uint16_t size_{0}; uint16_t capacity_{0}; }; -/// Out-of-line cold growth path for CallbackManager::add_(). -template __attribute__((noinline, cold)) void CallbackManager::grow_() { - auto *new_data = new CbType[this->size_ + 1]; - if (this->data_) { - __builtin_memcpy(new_data, this->data_, this->size_ * sizeof(CbType)); - delete[] this->data_; - } - this->data_ = new_data; - this->capacity_ = this->size_ + 1; -} - template class LazyCallbackManager; /** Lazy-allocating callback manager that only allocates memory when callbacks are registered. From fe8e7c134cdb180fd762b34aa51358151310f01c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 15:53:47 -1000 Subject: [PATCH 04/12] Add debug assert for uint16_t overflow in callback_manager_grow --- esphome/core/helpers.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 8c97a03b12..5940f6ec98 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -24,6 +24,7 @@ static const char *const TAG = "helpers"; __attribute__((noinline, cold)) void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity, size_t elem_size) { + ESPHOME_DEBUG_ASSERT(size < UINT16_MAX); uint16_t new_cap = size + 1; auto *new_data = ::operator new(new_cap *elem_size); if (data) { From f48eff29c3ae5017915523b4068bcbe585e5d968 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 15:54:40 -1000 Subject: [PATCH 05/12] =?UTF-8?q?Fix=20::operator=20new=20/=20delete[]=20m?= =?UTF-8?q?ismatch=20=E2=80=94=20use=20::operator=20delete=20to=20match?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- esphome/core/helpers.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index a2b33dd7b1..f68ad5a9f6 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -420,7 +420,7 @@ template::max()> if constexpr (std::is_trivially_copyable::value && std::is_trivially_default_constructible::value) { ::operator delete(this->data_); } else { - delete[] this->data_; + ::operator delete(this->data_); } } @@ -1804,7 +1804,7 @@ template class CallbackManager { public: CallbackManager() = default; - ~CallbackManager() { delete[] this->data_; } + ~CallbackManager() { ::operator delete(this->data_); } // Non-copyable (would alias data_), movable (for std::map support) CallbackManager(const CallbackManager &) = delete; @@ -1817,7 +1817,7 @@ template class CallbackManager { } CallbackManager &operator=(CallbackManager &&other) noexcept { if (this != &other) { - delete[] this->data_; + ::operator delete(this->data_); this->data_ = other.data_; this->size_ = other.size_; this->capacity_ = other.capacity_; From acef5ad5bbb8c2b96126ef5ecba0d12efd15d833 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 15:59:01 -1000 Subject: [PATCH 06/12] Fix off-by-one in debug assert: size + 1 must also fit in uint16_t --- esphome/core/helpers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 5940f6ec98..4768c4eb95 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -24,7 +24,7 @@ static const char *const TAG = "helpers"; __attribute__((noinline, cold)) void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity, size_t elem_size) { - ESPHOME_DEBUG_ASSERT(size < UINT16_MAX); + ESPHOME_DEBUG_ASSERT(size < UINT16_MAX - 1); uint16_t new_cap = size + 1; auto *new_data = ::operator new(new_cap *elem_size); if (data) { From 3a3345ace4382f80307b53ae50c42a602d2adeb6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:03:06 -1000 Subject: [PATCH 07/12] =?UTF-8?q?Revert=20accidental=20FixedRingBuffer=20c?= =?UTF-8?q?hange=20=E2=80=94=20only=20CallbackManager=20should=20be=20affe?= =?UTF-8?q?cted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- esphome/core/helpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index f68ad5a9f6..924a36dd1e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -420,7 +420,7 @@ template::max()> if constexpr (std::is_trivially_copyable::value && std::is_trivially_default_constructible::value) { ::operator delete(this->data_); } else { - ::operator delete(this->data_); + delete[] this->data_; } } From 41f9ed7fda4a5f9509d059c338cae80b5e2a496d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:21:07 -1000 Subject: [PATCH 08/12] Add braces to satisfy clang-tidy readability-braces-around-statements --- esphome/core/helpers.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 924a36dd1e..47775dd43c 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1847,9 +1847,10 @@ template class CallbackManager { /// Non-template core to avoid code duplication per lambda type. /// Inline fast path; cold growth path is in helpers.cpp via callback_manager_grow(). void add_(CbType cb) { - if (this->size_ == this->capacity_) + if (this->size_ == this->capacity_) { this->data_ = static_cast(callback_manager_grow(this->data_, this->size_, this->capacity_, sizeof(CbType))); + } this->data_[this->size_++] = cb; } CbType *data_{nullptr}; From b6abfec82e4e51bac2f0a927ca3180b174d70c02 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:22:24 -0400 Subject: [PATCH 09/12] [core] Fix area/device hash collision validation not running (#15259) --- esphome/config.py | 18 ++++++++++++++++++ esphome/core/config.py | 15 ++++++--------- script/ci-custom.py | 12 ++++++++++++ tests/unit_tests/core/test_config.py | 18 ++++++++++++++++++ .../config/area_singular_hash_collision.yaml | 10 ++++++++++ 5 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml diff --git a/esphome/config.py b/esphome/config.py index 7a6feea3d3..641b6ec1b4 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -958,6 +958,23 @@ class FinalValidateValidationStep(ConfigValidationStep): fv.full_config.reset(token) +class CoreFinalValidateStep(ConfigValidationStep): + """Run final validation on core esphome config (area/device hash collisions).""" + + # Same priority as component final validate steps + priority = -20.0 + + def run(self, result: Config) -> None: + if result.errors: + return + + token = fv.full_config.set(result) + with result.catch_error([CONF_ESPHOME]): + if CONF_ESPHOME in result: + core_config.validate_ids_and_references(result[CONF_ESPHOME]) + fv.full_config.reset(token) + + class PinUseValidationCheck(ConfigValidationStep): """Check for pin reuse""" @@ -1085,6 +1102,7 @@ def validate_config( for domain, conf in config.items(): result.add_validation_step(LoadValidationStep(domain, conf)) result.add_validation_step(IDPassValidationStep()) + result.add_validation_step(CoreFinalValidateStep()) result.add_validation_step(PinUseValidationCheck()) result.add_validation_step(RemoveReferenceValidationStep()) diff --git a/esphome/core/config.py b/esphome/core/config.py index e02c6ec75f..c47693c783 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -156,22 +156,22 @@ def validate_ids_and_references(config: ConfigType) -> ConfigType: hash_dict[hash_val] = id_obj.id # Collect all areas - all_areas: list[dict[str, str | core.ID]] = [] + all_areas: list[tuple[dict[str, str | core.ID], str]] = [] if CONF_AREA in config: - all_areas.append(config[CONF_AREA]) - all_areas.extend(config[CONF_AREAS]) + all_areas.append((config[CONF_AREA], CONF_AREA)) + all_areas.extend((area, CONF_AREAS) for area in config.get(CONF_AREAS, [])) # Validate area hash collisions and collect IDs area_hashes: dict[int, str] = {} area_ids: set[str] = set() - for area in all_areas: + for area, key in all_areas: area_id: core.ID = area[CONF_ID] - check_hash_collision(area_id, area_hashes, "Area", [CONF_AREAS, area_id.id]) + check_hash_collision(area_id, area_hashes, "Area", [key, area_id.id]) area_ids.add(area_id.id) # Validate device hash collisions and area references device_hashes: dict[int, str] = {} - for device in config[CONF_DEVICES]: + for device in config.get(CONF_DEVICES, []): device_id: core.ID = device[CONF_ID] check_hash_collision( device_id, device_hashes, "Device", [CONF_DEVICES, device_id.id] @@ -329,9 +329,6 @@ CONFIG_SCHEMA = cv.All( ) -FINAL_VALIDATE_SCHEMA = cv.All(validate_ids_and_references) - - PRELOAD_CONFIG_SCHEMA = cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, diff --git a/script/ci-custom.py b/script/ci-custom.py index 7d0680a491..ad39f92005 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1006,6 +1006,18 @@ def lint_log_in_header(fname, line, col, content): ) +@lint_content_find_check( + "FINAL_VALIDATE_SCHEMA", + include=["esphome/core/*.py"], + exclude=["esphome/core/entity_helpers.py"], +) +def lint_final_validate_in_core(fname, line, col, content): + return ( + "FINAL_VALIDATE_SCHEMA in esphome/core/ is not picked up by the component loader. " + "Use CoreFinalValidateStep in esphome/config.py instead." + ) + + def main(): colorama.init() diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 474d31a90a..6fa8f7ed43 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -248,6 +248,24 @@ def test_area_id_hash_collision( ) +def test_area_singular_hash_collision( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that area hash collisions between singular area: and areas: list are detected.""" + result = load_config_from_fixture( + yaml_file, "area_singular_hash_collision.yaml", FIXTURES_DIR + ) + assert result is None + + captured = capsys.readouterr() + assert ( + "Area ID 'd6ka' with hash 3082558663 collides with existing area ID 'test_2258'" + in captured.out + ) + # Error path should point to 'areas' (where the colliding entry is), not 'area' + assert "areas" in captured.out + + def test_device_duplicate_id( yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] ) -> None: diff --git a/tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml b/tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml new file mode 100644 index 0000000000..6e137f5f6e --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + area: + id: test_2258 + name: "Area 1" + areas: + - id: d6ka + name: "Area 2" + +host: From 42fe39cfd9bd163ac7e2c26799164d6eef01ae2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:24:23 -1000 Subject: [PATCH 10/12] Use pointer iteration in call() to avoid uint16_t movzx overhead on x86-64 --- esphome/core/helpers.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 47775dd43c..169028be1a 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1834,8 +1834,9 @@ template class CallbackManager { /// Call all callbacks in this manager. void call(Ts... args) { - for (uint16_t i = 0; i < this->size_; i++) - this->data_[i].call(args...); + for (auto *it = this->data_, *end = it + this->size_; it != end; ++it) { + it->call(args...); + } } uint16_t size() const { return this->size_; } From 967ded5477680983fe7ac08ab2992374fb3c1490 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:40:59 -1000 Subject: [PATCH 11/12] Add ESPHOME_ALWAYS_INLINE to call() to stabilize inlining across struct layout changes --- esphome/core/helpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 169028be1a..4937dbdd4e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1833,7 +1833,7 @@ template class CallbackManager { template void add(F &&callback) { this->add_(CbType::create(std::forward(callback))); } /// Call all callbacks in this manager. - void call(Ts... args) { + inline void ESPHOME_ALWAYS_INLINE call(Ts... args) { for (auto *it = this->data_, *end = it + this->size_; it != end; ++it) { it->call(args...); } From fa2b14ef412fceba6a62d6d11181490605e55453 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:47:56 -1000 Subject: [PATCH 12/12] Guard against nullptr+0 UB in call() and fix assert bound - Add early return when size_==0 to avoid nullptr+0 pointer arithmetic (UB per C++ standard, even though all ESPHome targets treat it as no-op) - Fix debug assert: size < UINT16_MAX allows capacity up to 65535 --- esphome/core/helpers.cpp | 2 +- esphome/core/helpers.h | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 4768c4eb95..5940f6ec98 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -24,7 +24,7 @@ static const char *const TAG = "helpers"; __attribute__((noinline, cold)) void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity, size_t elem_size) { - ESPHOME_DEBUG_ASSERT(size < UINT16_MAX - 1); + ESPHOME_DEBUG_ASSERT(size < UINT16_MAX); uint16_t new_cap = size + 1; auto *new_data = ::operator new(new_cap *elem_size); if (data) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 4937dbdd4e..f65409bc5f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1834,6 +1834,9 @@ template class CallbackManager { /// Call all callbacks in this manager. inline void ESPHOME_ALWAYS_INLINE call(Ts... args) { + if (this->size_ == 0) { + return; + } for (auto *it = this->data_, *end = it + this->size_; it != end; ++it) { it->call(args...); }