Merge branch 'kamilcuk/use-placement-new' into integration

This commit is contained in:
J. Nick Koston
2026-03-22 13:14:10 -10:00
18 changed files with 164 additions and 17 deletions
+23
View File
@@ -11,9 +11,11 @@
#include <iterator>
#include <limits>
#include <memory>
#include <new>
#include <span>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include <concepts>
#include <strings.h>
@@ -2230,6 +2232,27 @@ template<std::totally_ordered T, comparable_with<T> 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<class T> 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<T *>(data); }
/// @brief Retrieves a const pointer to the storage as the target type.
const T *get() const { return reinterpret_cast<const T *>(data); }
};
/// @name Internal functions
///@{
+35 -8
View File
@@ -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<int>),
# 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."""
@@ -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
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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
@@ -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
@@ -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"
@@ -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<int> *const my_global_int" in main_cpp
assert "PlacementStorage<globals::GlobalsComponent<int>>" in main_cpp
assert "new(my_global_int) globals::GlobalsComponent<int>" in main_cpp
# Verify initial value is passed as constructor arg
assert "42" in main_cpp
# Check other globals are also generated
assert "PlacementStorage<globals::GlobalsComponent<float>>" in main_cpp
assert "PlacementStorage<globals::GlobalsComponent<bool>>" in main_cpp
@@ -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
+5 -1
View File
@@ -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<image::Image> 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
)
@@ -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
@@ -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<mipi_dsi::MIPI_DSI> 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
@@ -0,0 +1,8 @@
esphome:
name: test
esp32:
board: esp32dev
status_led:
pin: GPIO2
@@ -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> 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
+2 -1
View File
@@ -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
@@ -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