diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 93b187d850a..eb009e770f0 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 @@ -2230,6 +2232,27 @@ 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. 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 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 storage as the target type. + const T *get() const { return reinterpret_cast(data); } +}; + /// @name Internal functions ///@{ diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 5457485d254..5c7da6e1c1d 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -579,10 +579,36 @@ 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) + + 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 + storage_type = MockObj(f"esphome::PlacementStorage<{the_type}>") + storage_id = ID(f"{id_.id}_storage_", type=storage_type) + + CORE.add_global( + VariableDeclarationExpression(storage_type, "", storage_id, static=True) + ) + CORE.add_global( + AssignmentExpression( + f"static {the_type}", + "*const ", + id_, + MockObj(f"{storage_id.id}.get()"), + ) + ) + # 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_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) + CORE.add(AssignmentExpression(None, None, id_, rhs)) + CORE.register_variable(id_, obj) return obj @@ -799,11 +825,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) -> None: 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 @@ -818,7 +845,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) @@ -832,7 +859,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.""" 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/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 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..eba6305a48c 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,14 @@ 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 esphome::PlacementStorage p4_nano_storage_;" + in main_cpp + ) + assert ( + "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/__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 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