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