From 7997743b15cfc10a9522ebfd75f31201ef48f0d9 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Sat, 21 Mar 2026 12:01:08 +0100 Subject: [PATCH 01/10] [core] Use placement new allocation for t Pvariables --- esphome/core/helpers.h | 37 +++++++++++++++ esphome/cpp_generator.py | 45 +++++++++++++++++-- .../binary_sensor/test_binary_sensor.py | 2 +- tests/component_tests/button/test_button.py | 3 +- tests/component_tests/conftest.py | 2 +- .../deep_sleep/test_deep_sleep.py | 6 ++- .../gpio/test_gpio_binary_sensor.py | 3 +- tests/component_tests/image/test_init.py | 6 ++- tests/component_tests/logger/test_logger.py | 4 ++ .../mipi_dsi/test_mipi_dsi_config.py | 5 ++- .../status_led/test_status_led.py | 23 ++++++++++ tests/component_tests/text/test_text.py | 3 +- .../text_sensor/test_text_sensor.py | 3 +- 13 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 tests/component_tests/status_led/test_status_led.py diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 43431299dea..233fa481ed8 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -11,9 +11,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -2220,6 +2222,41 @@ template U> T clamp_at_most(T value, return value; } +/** + * @brief Provides properly aligned, uninitialized static storage for a given type T. + * + * This struct is designed to replace dynamic heap allocations (`new T(...)`) for + * global or static singletons within ESPHome, preventing memory fragmentation. + * The underlying object must be explicitly constructed using placement new + * before access. + */ +template struct PlacementStorage { + /// @brief Raw byte storage, strictly aligned for type T. + alignas(T) unsigned char data[sizeof(T)]; + + /** + * @brief Safely retrieves a pointer to the constructed object. + * @return T* Pointer to the underlying object. + */ + constexpr T *get() { return reinterpret_cast(data); } + + /** + * @brief Safely retrieves a const pointer to the constructed object. + * @return const T* Const pointer to the underlying object. + */ + constexpr const T *get() const { return reinterpret_cast(data); } + + /// @brief Member access operator pointing to the underlying object. + constexpr T *operator->() { return get(); } + /// @brief Const member access operator pointing to the underlying object. + constexpr const T *operator->() const { return get(); } + + /// @brief Dereference operator yielding a reference to the underlying object. + constexpr T &operator*() { return *get(); } + /// @brief Const dereference operator yielding a const reference to the underlying object. + constexpr const T &operator*() const { return *get(); } +}; + /// @name Internal functions ///@{ diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 5457485d254..af332243c52 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -579,10 +579,47 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": obj = MockObj(id_, "->") if type_ is not None: id_.type = type_ - decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) - CORE.add_global(decl) - assignment = AssignmentExpression(None, None, id_, rhs) - CORE.add(assignment) + + # Check if the right-hand side expression is an instantiation via the 'new' operator. + # This typically happens when `.new(...)` is used to create a new object. + # We detect this by checking if the base of the CallExpression starts with "new ". + rhs_str = str(rhs) + is_new = rhs_str.startswith("new ") + + if is_new: + # For 'new' allocations, we avoid dynamic heap allocation to prevent fragmentation. + # Instead, we statically allocate raw, aligned storage for the object and use + # raw placement new to initialize it in-place during setup(). + # We must use raw placement new here because C++ cannot deduce types + # for brace-enclosed initializer lists passed to variadic templates. + + call_str = rhs_str[4:] # Strip "new " from "new Type(args)" + the_type = id_.type if id_.type is not None else call_str.split("(")[0].strip() + storage_name = f"{id_.id}_storage_" + + # Declare the static PlacementStorage + decl1 = RawStatement( + f"static esphome::PlacementStorage<{the_type}> {storage_name};" + ) + CORE.add_global(decl1) + + # Declare a constant pointer referencing the storage so it can be used identically to a standard pointer + decl2 = RawStatement( + f"static {the_type} *const {id_.id} = {storage_name}.get();" + ) + CORE.add_global(decl2) + + # Construct the object via raw placement new in setup() + assignment = RawStatement(f"new({id_.id}) {call_str};") + CORE.add(assignment) + else: + # For standard assignments (e.g. passing an existing pointer like '&my_struct' or 'nullptr'), + # we generate a standard static pointer and assign it directly. + decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) + CORE.add_global(decl) + assignment = AssignmentExpression(None, None, id_, rhs) + CORE.add(assignment) + CORE.register_variable(id_, obj) return obj diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index 10d7f808346..4f41f2cc704 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -15,7 +15,7 @@ def test_binary_sensor_is_setup(generate_main): ) # Then - assert "new gpio::GPIOBinarySensor();" in main_cpp + assert "static gpio::GPIOBinarySensor *const" in main_cpp assert "App.register_binary_sensor" in main_cpp diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index a35994a682c..544e748f913 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -13,7 +13,8 @@ def test_button_is_setup(generate_main): main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then - assert "new wake_on_lan::WakeOnLanButton();" in main_cpp + assert "static wake_on_lan::WakeOnLanButton *const" in main_cpp + assert ") wake_on_lan::WakeOnLanButton();" in main_cpp assert "App.register_button" in main_cpp assert "App.register_component" in main_cpp diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py index 0641e698e97..763628f57c9 100644 --- a/tests/component_tests/conftest.py +++ b/tests/component_tests/conftest.py @@ -134,7 +134,7 @@ def generate_main() -> Generator[Callable[[str | Path], str]]: CORE.config_path = Path(path) CORE.config = read_config({}) generate_cpp_contents(CORE.config) - return CORE.cpp_main_section + return CORE.cpp_global_section + CORE.cpp_main_section yield generator diff --git a/tests/component_tests/deep_sleep/test_deep_sleep.py b/tests/component_tests/deep_sleep/test_deep_sleep.py index 41ddd72febd..d78848872b1 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep.py +++ b/tests/component_tests/deep_sleep/test_deep_sleep.py @@ -7,7 +7,11 @@ def test_deep_sleep_setup(generate_main): """ main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml") - assert "deepsleep = new deep_sleep::DeepSleepComponent();" in main_cpp + assert ( + "static deep_sleep::DeepSleepComponent *const deepsleep = deepsleep_storage_.get();" + in main_cpp + ) + assert "new(deepsleep) deep_sleep::DeepSleepComponent();" in main_cpp assert "App.register_component_(deepsleep);" in main_cpp diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor.py b/tests/component_tests/gpio/test_gpio_binary_sensor.py index 73665dc45d2..f336a9105ec 100644 --- a/tests/component_tests/gpio/test_gpio_binary_sensor.py +++ b/tests/component_tests/gpio/test_gpio_binary_sensor.py @@ -16,7 +16,8 @@ def test_gpio_binary_sensor_basic_setup( """ main_cpp = generate_main("tests/component_tests/gpio/test_gpio_binary_sensor.yaml") - assert "new gpio::GPIOBinarySensor();" in main_cpp + assert "static gpio::GPIOBinarySensor *const" in main_cpp + assert ") gpio::GPIOBinarySensor();" in main_cpp assert "App.register_binary_sensor" in main_cpp # set_use_interrupt(true) should NOT be generated (uses C++ default) assert "bs_gpio->set_use_interrupt(true);" not in main_cpp diff --git a/tests/component_tests/image/test_init.py b/tests/component_tests/image/test_init.py index c9481a0e1d7..3e554cff73c 100644 --- a/tests/component_tests/image/test_init.py +++ b/tests/component_tests/image/test_init.py @@ -242,7 +242,11 @@ def test_image_generation( main_cpp = generate_main(component_config_path("image_test.yaml")) assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp assert ( - "cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);" + "static esphome::PlacementStorage cat_img_storage_;" in main_cpp + ) + assert "static image::Image *const cat_img = cat_img_storage_.get();" in main_cpp + assert ( + "new(cat_img) image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);" in main_cpp ) diff --git a/tests/component_tests/logger/test_logger.py b/tests/component_tests/logger/test_logger.py index 98aa7419642..94a6f7ac7bc 100644 --- a/tests/component_tests/logger/test_logger.py +++ b/tests/component_tests/logger/test_logger.py @@ -22,6 +22,10 @@ def test_logger_pre_setup_before_other_components(generate_main): # Find all "new " allocations (component creation) new_allocations = list(re.finditer(r"\bnew [\w:]+", main_cpp)) + # Find all "new(" allocations (component creation) and combine them + new_allocations.extend(re.finditer(r"\bnew\([^)]+\) [\w:]+", main_cpp)) + # Sort allocations by position in the file + new_allocations.sort(key=lambda m: m.start()) assert len(new_allocations) > 0, "No component allocations found" # Separate logger and non-logger allocations diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index 92f56b5451a..519e39a1d9b 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -119,7 +119,10 @@ def test_code_generation( main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml")) assert ( - "p4_nano = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" + "static mipi_dsi::MIPI_DSI *const p4_nano = p4_nano_storage_.get();" in main_cpp + ) + assert ( + "new(p4_nano) mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" in main_cpp ) assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp diff --git a/tests/component_tests/status_led/test_status_led.py b/tests/component_tests/status_led/test_status_led.py new file mode 100644 index 00000000000..b40f9b8bccb --- /dev/null +++ b/tests/component_tests/status_led/test_status_led.py @@ -0,0 +1,23 @@ +"""Tests for status_led.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + + +def test_status_led_generation( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test status_led generation.""" + main_cpp = generate_main(component_config_path("status_led_test.yaml")) + assert ( + "static esphome::PlacementStorage status_led_statusled_id_storage_;" + in main_cpp + ) + assert ( + "static status_led::StatusLED *const status_led_statusled_id = status_led_statusled_id_storage_.get();" + in main_cpp + ) + assert "new(status_led_statusled_id) status_led::StatusLED(" in main_cpp diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index c74dfb8a471..63eb4f19515 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -13,7 +13,8 @@ def test_text_is_setup(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "new template_::TemplateText();" in main_cpp + assert "static template_::TemplateText *const" in main_cpp + assert ") template_::TemplateText();" in main_cpp assert "App.register_text" in main_cpp diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 1ff31ab96bd..ae094fadf87 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -13,7 +13,8 @@ def test_text_sensor_is_setup(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert "new template_::TemplateTextSensor();" in main_cpp + assert "static template_::TemplateTextSensor *const" in main_cpp + assert ") template_::TemplateTextSensor();" in main_cpp assert "App.register_text_sensor" in main_cpp From c93c701e0af7bebd84cbe7d1a0da5d82bc6667f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 11:01:36 -1000 Subject: [PATCH 02/10] fixes, safety --- esphome/core/helpers.h | 27 ++++++--------------------- esphome/cpp_generator.py | 6 +++++- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 233fa481ed8..c1e3c6eaf26 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -2228,33 +2228,18 @@ template U> T clamp_at_most(T value, * This struct is designed to replace dynamic heap allocations (`new T(...)`) for * global or static singletons within ESPHome, preventing memory fragmentation. * The underlying object must be explicitly constructed using placement new - * before access. + * before access. No destructor is called — this is intentional since ESPHome + * singletons live for the entire device lifetime. */ template struct PlacementStorage { /// @brief Raw byte storage, strictly aligned for type T. alignas(T) unsigned char data[sizeof(T)]; - /** - * @brief Safely retrieves a pointer to the constructed object. - * @return T* Pointer to the underlying object. - */ - constexpr T *get() { return reinterpret_cast(data); } + /// @brief Retrieves a pointer to the constructed object. + T *get() { return std::launder(reinterpret_cast(data)); } - /** - * @brief Safely retrieves a const pointer to the constructed object. - * @return const T* Const pointer to the underlying object. - */ - constexpr const T *get() const { return reinterpret_cast(data); } - - /// @brief Member access operator pointing to the underlying object. - constexpr T *operator->() { return get(); } - /// @brief Const member access operator pointing to the underlying object. - constexpr const T *operator->() const { return get(); } - - /// @brief Dereference operator yielding a reference to the underlying object. - constexpr T &operator*() { return *get(); } - /// @brief Const dereference operator yielding a const reference to the underlying object. - constexpr const T &operator*() const { return *get(); } + /// @brief Retrieves a const pointer to the constructed object. + const T *get() const { return std::launder(reinterpret_cast(data)); } }; /// @name Internal functions diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index af332243c52..3d34ff0eb3a 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -594,7 +594,11 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": # for brace-enclosed initializer lists passed to variadic templates. call_str = rhs_str[4:] # Strip "new " from "new Type(args)" - the_type = id_.type if id_.type is not None else call_str.split("(")[0].strip() + the_type = ( + id_.type + if id_.type is not None + else call_str.split("(", maxsplit=1)[0].strip() + ) storage_name = f"{id_.id}_storage_" # Declare the static PlacementStorage From c3ef2faa4fc0fadbedb9b105a93ded6777765a61 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 11:09:26 -1000 Subject: [PATCH 03/10] fixes --- tests/component_tests/status_led/__init__.py | 0 .../status_led/config/status_led_test.yaml | 8 ++++++++ 2 files changed, 8 insertions(+) create mode 100644 tests/component_tests/status_led/__init__.py create mode 100644 tests/component_tests/status_led/config/status_led_test.yaml diff --git a/tests/component_tests/status_led/__init__.py b/tests/component_tests/status_led/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/status_led/config/status_led_test.yaml b/tests/component_tests/status_led/config/status_led_test.yaml new file mode 100644 index 00000000000..c86197d2256 --- /dev/null +++ b/tests/component_tests/status_led/config/status_led_test.yaml @@ -0,0 +1,8 @@ +esphome: + name: test + +esp32: + board: esp32dev + +status_led: + pin: GPIO2 From bcc3ec80541809872f0aeef45ceaac5f187b77c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 11:14:49 -1000 Subject: [PATCH 04/10] simplify --- esphome/cpp_generator.py | 57 ++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 3d34ff0eb3a..5a6452f35a2 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -580,49 +580,29 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": if type_ is not None: id_.type = type_ - # Check if the right-hand side expression is an instantiation via the 'new' operator. - # This typically happens when `.new(...)` is used to create a new object. - # We detect this by checking if the base of the CallExpression starts with "new ". - rhs_str = str(rhs) - is_new = rhs_str.startswith("new ") + is_new = isinstance(rhs, MockObj) and rhs._is_new_expr if is_new: - # For 'new' allocations, we avoid dynamic heap allocation to prevent fragmentation. - # Instead, we statically allocate raw, aligned storage for the object and use - # raw placement new to initialize it in-place during setup(). - # We must use raw placement new here because C++ cannot deduce types - # for brace-enclosed initializer lists passed to variadic templates. - - call_str = rhs_str[4:] # Strip "new " from "new Type(args)" - the_type = ( - id_.type - if id_.type is not None - else call_str.split("(", maxsplit=1)[0].strip() - ) + # For 'new' allocations, use placement new into static storage + # to avoid heap fragmentation on embedded devices. storage_name = f"{id_.id}_storage_" + the_type = id_.type - # Declare the static PlacementStorage - decl1 = RawStatement( - f"static esphome::PlacementStorage<{the_type}> {storage_name};" + CORE.add_global( + RawStatement( + f"static esphome::PlacementStorage<{the_type}> {storage_name};" + ) ) - CORE.add_global(decl1) - - # Declare a constant pointer referencing the storage so it can be used identically to a standard pointer - decl2 = RawStatement( - f"static {the_type} *const {id_.id} = {storage_name}.get();" + CORE.add_global( + RawStatement(f"static {the_type} *const {id_.id} = {storage_name}.get();") ) - CORE.add_global(decl2) - - # Construct the object via raw placement new in setup() - assignment = RawStatement(f"new({id_.id}) {call_str};") - CORE.add(assignment) + # Strip "new " prefix to get "Type(args)" for placement new + rhs_str = str(rhs) + CORE.add(RawStatement(f"new({id_.id}) {rhs_str[4:]};")) else: - # For standard assignments (e.g. passing an existing pointer like '&my_struct' or 'nullptr'), - # we generate a standard static pointer and assign it directly. decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) CORE.add_global(decl) - assignment = AssignmentExpression(None, None, id_, rhs) - CORE.add(assignment) + CORE.add(AssignmentExpression(None, None, id_, rhs)) CORE.register_variable(id_, obj) return obj @@ -840,11 +820,12 @@ class MockObj(Expression): Mostly consists of magic methods that allow ESPHome's codegen syntax. """ - __slots__ = ("base", "op") + __slots__ = ("base", "op", "_is_new_expr") - def __init__(self, base, op="."): + def __init__(self, base, op=".", is_new_expr=False): self.base = base self.op = op + self._is_new_expr = is_new_expr def __getattr__(self, attr: str) -> "MockObj": # prevent python dunder methods being replaced by mock objects @@ -859,7 +840,7 @@ class MockObj(Expression): def __call__(self, *args: SafeExpType) -> "MockObj": call = CallExpression(self.base, *args) - return MockObj(call, self.op) + return MockObj(call, self.op, is_new_expr=self._is_new_expr) def __str__(self): return str(self.base) @@ -873,7 +854,7 @@ class MockObj(Expression): @property def new(self) -> "MockObj": - return MockObj(f"new {self.base}", "->") + return MockObj(f"new {self.base}", "->", is_new_expr=True) def template(self, *args: SafeExpType) -> "MockObj": """Apply template parameters to this object.""" From 9a310d307fd915b144c2351541ddc91771cda2de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 11:17:30 -1000 Subject: [PATCH 05/10] touch ups --- esphome/cpp_generator.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 5a6452f35a2..a1e4c2ac59d 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -585,20 +585,25 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": if is_new: # For 'new' allocations, use placement new into static storage # to avoid heap fragmentation on embedded devices. - storage_name = f"{id_.id}_storage_" the_type = id_.type + storage_type = MockObj(f"esphome::PlacementStorage<{the_type}>") + storage_id = ID(f"{id_.id}_storage_", type=storage_type) CORE.add_global( - RawStatement( - f"static esphome::PlacementStorage<{the_type}> {storage_name};" - ) + VariableDeclarationExpression(storage_type, "", storage_id, static=True) ) CORE.add_global( - RawStatement(f"static {the_type} *const {id_.id} = {storage_name}.get();") + AssignmentExpression( + f"static {the_type}", + "*const ", + id_, + MockObj(f"{storage_id.id}.get()"), + ) ) - # Strip "new " prefix to get "Type(args)" for placement new - rhs_str = str(rhs) - CORE.add(RawStatement(f"new({id_.id}) {rhs_str[4:]};")) + # Extract args from the CallExpression and rebuild as placement new + call_expr = rhs.base # CallExpression("new Type", args...) + placement_new = CallExpression(f"new({id_.id}) {the_type}", *call_expr.args) + CORE.add(ExpressionStatement(placement_new)) else: decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) CORE.add_global(decl) From 6b59d01dd2ea3d56cc51bd563421e15be75bbfc2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 11:20:22 -1000 Subject: [PATCH 06/10] touch ups --- esphome/cpp_generator.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index a1e4c2ac59d..da10567f5c5 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -580,9 +580,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": if type_ is not None: id_.type = type_ - is_new = isinstance(rhs, MockObj) and rhs._is_new_expr - - if is_new: + if isinstance(rhs, MockObj) and rhs._is_new_expr: # For 'new' allocations, use placement new into static storage # to avoid heap fragmentation on embedded devices. the_type = id_.type From 85c970e1ddb46e8d6817de65664afe56c84ef7a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 11:23:46 -1000 Subject: [PATCH 07/10] globals is special --- esphome/cpp_generator.py | 6 ++++- tests/component_tests/globals/__init__.py | 0 .../globals/config/globals_test.yaml | 16 +++++++++++ tests/component_tests/globals/test_globals.py | 27 +++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/component_tests/globals/__init__.py create mode 100644 tests/component_tests/globals/config/globals_test.yaml create mode 100644 tests/component_tests/globals/test_globals.py diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index da10567f5c5..a32cd945d4c 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -600,7 +600,11 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": ) # Extract args from the CallExpression and rebuild as placement new call_expr = rhs.base # CallExpression("new Type", args...) - placement_new = CallExpression(f"new({id_.id}) {the_type}", *call_expr.args) + placement_args: list[Expression] = [] + if call_expr.template_args is not None: + placement_args.append(call_expr.template_args) + placement_args.extend(call_expr.args) + placement_new = CallExpression(f"new({id_.id}) {the_type}", *placement_args) CORE.add(ExpressionStatement(placement_new)) else: decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) diff --git a/tests/component_tests/globals/__init__.py b/tests/component_tests/globals/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/globals/config/globals_test.yaml b/tests/component_tests/globals/config/globals_test.yaml new file mode 100644 index 00000000000..1d1a9edaa62 --- /dev/null +++ b/tests/component_tests/globals/config/globals_test.yaml @@ -0,0 +1,16 @@ +esphome: + name: test + +esp32: + board: esp32dev + +globals: + - id: my_global_int + type: int + initial_value: "42" + - id: my_global_float + type: float + initial_value: "1.5" + - id: my_global_bool + type: bool + initial_value: "true" diff --git a/tests/component_tests/globals/test_globals.py b/tests/component_tests/globals/test_globals.py new file mode 100644 index 00000000000..3dec3b16152 --- /dev/null +++ b/tests/component_tests/globals/test_globals.py @@ -0,0 +1,27 @@ +"""Tests for the globals component.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + + +def test_globals_placement_new_with_template_args( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test that globals uses placement new with template arguments preserved.""" + main_cpp = generate_main(component_config_path("globals_test.yaml")) + + # Globals uses Pvariable with Type.new(template_args, initial_value) + # which exercises the template_args preservation in placement new. + assert "static globals::GlobalsComponent *const my_global_int" in main_cpp + assert "PlacementStorage>" in main_cpp + assert "new(my_global_int) globals::GlobalsComponent" in main_cpp + + # Verify initial value is passed as constructor arg + assert "42" in main_cpp + + # Check other globals are also generated + assert "PlacementStorage>" in main_cpp + assert "PlacementStorage>" in main_cpp From 2e8b009167a2af7aeaecffbd1e2a811c6f8a1280 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 11:31:03 -1000 Subject: [PATCH 08/10] fix duplication --- esphome/cpp_generator.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index a32cd945d4c..2937a437547 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -598,13 +598,11 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": MockObj(f"{storage_id.id}.get()"), ) ) - # Extract args from the CallExpression and rebuild as placement new + # Extract args from the CallExpression and rebuild as placement new. + # Template args are already encoded in the_type (e.g. GlobalsComponent), + # so we only pass the constructor args, not template_args. call_expr = rhs.base # CallExpression("new Type", args...) - placement_args: list[Expression] = [] - if call_expr.template_args is not None: - placement_args.append(call_expr.template_args) - placement_args.extend(call_expr.args) - placement_new = CallExpression(f"new({id_.id}) {the_type}", *placement_args) + placement_new = CallExpression(f"new({id_.id}) {the_type}", *call_expr.args) CORE.add(ExpressionStatement(placement_new)) else: decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) From 478ac9d9f2f4a4f48ee25aaeb46ae8a3d48c7e14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 11:32:36 -1000 Subject: [PATCH 09/10] lint --- esphome/cpp_generator.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 2937a437547..5c7da6e1c1d 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -580,7 +580,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": if type_ is not None: id_.type = type_ - if isinstance(rhs, MockObj) and rhs._is_new_expr: + if isinstance(rhs, MockObj) and rhs.is_new_expr: # For 'new' allocations, use placement new into static storage # to avoid heap fragmentation on embedded devices. the_type = id_.type @@ -825,12 +825,12 @@ class MockObj(Expression): Mostly consists of magic methods that allow ESPHome's codegen syntax. """ - __slots__ = ("base", "op", "_is_new_expr") + __slots__ = ("base", "op", "is_new_expr") - def __init__(self, base, op=".", is_new_expr=False): + def __init__(self, base, op=".", is_new_expr=False) -> None: self.base = base self.op = op - self._is_new_expr = is_new_expr + self.is_new_expr = is_new_expr def __getattr__(self, attr: str) -> "MockObj": # prevent python dunder methods being replaced by mock objects @@ -845,7 +845,7 @@ class MockObj(Expression): def __call__(self, *args: SafeExpType) -> "MockObj": call = CallExpression(self.base, *args) - return MockObj(call, self.op, is_new_expr=self._is_new_expr) + return MockObj(call, self.op, is_new_expr=self.is_new_expr) def __str__(self): return str(self.base) From bc688b973a060c1f4e4a309007b70c860d89697e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 11:34:27 -1000 Subject: [PATCH 10/10] address bot comments --- esphome/core/helpers.h | 9 +++++---- tests/component_tests/mipi_dsi/test_mipi_dsi_config.py | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index c1e3c6eaf26..6d6a31749d0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -2235,11 +2235,12 @@ template struct PlacementStorage { /// @brief Raw byte storage, strictly aligned for type T. alignas(T) unsigned char data[sizeof(T)]; - /// @brief Retrieves a pointer to the constructed object. - T *get() { return std::launder(reinterpret_cast(data)); } + /// @brief Retrieves a pointer to the storage as the target type. + /// The caller must ensure the object has been constructed via placement new before dereferencing. + T *get() { return reinterpret_cast(data); } - /// @brief Retrieves a const pointer to the constructed object. - const T *get() const { return std::launder(reinterpret_cast(data)); } + /// @brief Retrieves a const pointer to the storage as the target type. + const T *get() const { return reinterpret_cast(data); } }; /// @name Internal functions diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index 519e39a1d9b..eba6305a48c 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -118,6 +118,10 @@ def test_code_generation( """Test code generation for display.""" main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml")) + assert ( + "static esphome::PlacementStorage p4_nano_storage_;" + in main_cpp + ) assert ( "static mipi_dsi::MIPI_DSI *const p4_nano = p4_nano_storage_.get();" in main_cpp )