1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-30 14:43:51 +00:00

Optimize stateless lambdas to use function pointers (#11551)

This commit is contained in:
J. Nick Koston
2025-10-27 14:06:22 -05:00
committed by GitHub
parent 7394cbf773
commit 3c18558003
12 changed files with 190 additions and 11 deletions

View File

@@ -16,7 +16,12 @@ from esphome.const import (
CONF_UPDATE_INTERVAL,
)
from esphome.core import ID
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
from esphome.cpp_generator import (
LambdaExpression,
MockObj,
MockObjClass,
TemplateArgsType,
)
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType
from esphome.util import Registry
@@ -87,6 +92,7 @@ def validate_potentially_or_condition(value):
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
IfAction = cg.esphome_ns.class_("IfAction", Action)
WhileAction = cg.esphome_ns.class_("WhileAction", Action)
RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
@@ -97,9 +103,40 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
Automation = cg.esphome_ns.class_("Automation")
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition)
ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component)
def new_lambda_pvariable(
id_obj: ID,
lambda_expr: LambdaExpression,
stateless_class: MockObjClass,
template_arg: cg.TemplateArguments | None = None,
) -> MockObj:
"""Create Pvariable for lambda, using stateless class if applicable.
Combines ID selection and Pvariable creation in one call. For stateless
lambdas (empty capture), uses function pointer instead of std::function.
Args:
id_obj: The ID object (action_id, condition_id, or filter_id)
lambda_expr: The lambda expression object
stateless_class: The stateless class to use for stateless lambdas
template_arg: Optional template arguments (for actions/conditions)
Returns:
The created Pvariable
"""
# For stateless lambdas, use function pointer instead of std::function
if lambda_expr.capture == "":
id_obj = id_obj.copy()
id_obj.type = stateless_class
if template_arg is not None:
return cg.new_Pvariable(id_obj, template_arg, lambda_expr)
return cg.new_Pvariable(id_obj, lambda_expr)
def validate_automation(extra_schema=None, extra_validators=None, single=False):
if extra_schema is None:
extra_schema = {}
@@ -240,7 +277,9 @@ async def lambda_condition_to_code(
args: TemplateArgsType,
) -> MockObj:
lambda_ = await cg.process_lambda(config, args, return_type=bool)
return cg.new_Pvariable(condition_id, template_arg, lambda_)
return new_lambda_pvariable(
condition_id, lambda_, StatelessLambdaCondition, template_arg
)
@register_condition(
@@ -406,7 +445,7 @@ async def lambda_action_to_code(
args: TemplateArgsType,
) -> MockObj:
lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
return cg.new_Pvariable(action_id, template_arg, lambda_)
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
@register_action(

View File

@@ -155,6 +155,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter)
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
_LOGGER = getLogger(__name__)
@@ -299,7 +300,7 @@ async def lambda_filter_to_code(config, filter_id):
lambda_ = await cg.process_lambda(
config, [(bool, "x")], return_type=cg.optional.template(bool)
)
return cg.new_Pvariable(filter_id, lambda_)
return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter)
@register_filter(

View File

@@ -111,6 +111,21 @@ class LambdaFilter : public Filter {
std::function<optional<bool>(bool)> f_;
};
/** Optimized lambda filter for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
*/
class StatelessLambdaFilter : public Filter {
public:
explicit StatelessLambdaFilter(optional<bool> (*f)(bool)) : f_(f) {}
optional<bool> new_value(bool value) override { return this->f_(value); }
protected:
optional<bool> (*f_)(bool);
};
class SettleFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value) override;

View File

@@ -1,7 +1,7 @@
import re
from esphome import automation
from esphome.automation import LambdaAction
from esphome.automation import LambdaAction, StatelessLambdaAction
import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
from esphome.components.esp32.const import (
@@ -430,7 +430,9 @@ async def logger_log_action_to_code(config, action_id, template_arg, args):
text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_)))
lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
return cg.new_Pvariable(action_id, template_arg, lambda_)
return automation.new_lambda_pvariable(
action_id, lambda_, StatelessLambdaAction, template_arg
)
@automation.register_action(
@@ -455,7 +457,9 @@ async def logger_set_level_to_code(config, action_id, template_arg, args):
text = str(cg.statement(logger.set_log_level(level)))
lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
return cg.new_Pvariable(action_id, template_arg, lambda_)
return automation.new_lambda_pvariable(
action_id, lambda_, StatelessLambdaAction, template_arg
)
FILTER_SOURCE_FILES = filter_source_files_from_platform(

View File

@@ -261,6 +261,7 @@ ExponentialMovingAverageFilter = sensor_ns.class_(
)
ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component)
LambdaFilter = sensor_ns.class_("LambdaFilter", Filter)
StatelessLambdaFilter = sensor_ns.class_("StatelessLambdaFilter", Filter)
OffsetFilter = sensor_ns.class_("OffsetFilter", Filter)
MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter)
ValueListFilter = sensor_ns.class_("ValueListFilter", Filter)
@@ -573,7 +574,7 @@ async def lambda_filter_to_code(config, filter_id):
lambda_ = await cg.process_lambda(
config, [(float, "x")], return_type=cg.optional.template(float)
)
return cg.new_Pvariable(filter_id, lambda_)
return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter)
DELTA_SCHEMA = cv.Schema(

View File

@@ -296,6 +296,21 @@ class LambdaFilter : public Filter {
lambda_filter_t lambda_filter_;
};
/** Optimized lambda filter for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
*/
class StatelessLambdaFilter : public Filter {
public:
explicit StatelessLambdaFilter(optional<float> (*lambda_filter)(float)) : lambda_filter_(lambda_filter) {}
optional<float> new_value(float value) override { return this->lambda_filter_(value); }
protected:
optional<float> (*lambda_filter_)(float);
};
/// A simple filter that adds `offset` to each value it receives.
class OffsetFilter : public Filter {
public:

View File

@@ -57,6 +57,7 @@ validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
# Filters
Filter = text_sensor_ns.class_("Filter")
LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter)
StatelessLambdaFilter = text_sensor_ns.class_("StatelessLambdaFilter", Filter)
ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter)
ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter)
AppendFilter = text_sensor_ns.class_("AppendFilter", Filter)
@@ -70,7 +71,7 @@ async def lambda_filter_to_code(config, filter_id):
lambda_ = await cg.process_lambda(
config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string)
)
return cg.new_Pvariable(filter_id, lambda_)
return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter)
@FILTER_REGISTRY.register("to_upper", ToUpperFilter, {})

View File

@@ -62,6 +62,21 @@ class LambdaFilter : public Filter {
lambda_filter_t lambda_filter_;
};
/** Optimized lambda filter for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
*/
class StatelessLambdaFilter : public Filter {
public:
explicit StatelessLambdaFilter(optional<std::string> (*lambda_filter)(std::string)) : lambda_filter_(lambda_filter) {}
optional<std::string> new_value(std::string value) override { return this->lambda_filter_(value); }
protected:
optional<std::string> (*lambda_filter_)(std::string);
};
/// A simple filter that converts all text to uppercase
class ToUpperFilter : public Filter {
public:

View File

@@ -79,6 +79,18 @@ template<typename... Ts> class LambdaCondition : public Condition<Ts...> {
std::function<bool(Ts...)> f_;
};
/// Optimized lambda condition for stateless lambdas (no capture).
/// Uses function pointer instead of std::function to reduce memory overhead.
/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
template<typename... Ts> class StatelessLambdaCondition : public Condition<Ts...> {
public:
explicit StatelessLambdaCondition(bool (*f)(Ts...)) : f_(f) {}
bool check(Ts... x) override { return this->f_(x...); }
protected:
bool (*f_)(Ts...);
};
template<typename... Ts> class ForCondition : public Condition<Ts...>, public Component {
public:
explicit ForCondition(Condition<> *condition) : condition_(condition) {}
@@ -190,6 +202,19 @@ template<typename... Ts> class LambdaAction : public Action<Ts...> {
std::function<void(Ts...)> f_;
};
/// Optimized lambda action for stateless lambdas (no capture).
/// Uses function pointer instead of std::function to reduce memory overhead.
/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
template<typename... Ts> class StatelessLambdaAction : public Action<Ts...> {
public:
explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {}
void play(Ts... x) override { this->f_(x...); }
protected:
void (*f_)(Ts...);
};
template<typename... Ts> class IfAction : public Action<Ts...> {
public:
explicit IfAction(Condition<Ts...> *condition) : condition_(condition) {}

View File

@@ -198,6 +198,8 @@ class LambdaExpression(Expression):
self.return_type = safe_exp(return_type) if return_type is not None else None
def __str__(self):
# Stateless lambdas (empty capture) implicitly convert to function pointers
# when assigned to function pointer types - no unary + needed
cpp = f"[{self.capture}]({self.parameters})"
if self.return_type is not None:
cpp += f" -> {self.return_type}"
@@ -700,6 +702,12 @@ async def process_lambda(
parts[i * 3 + 1] = var
parts[i * 3 + 2] = ""
# All id() references are global variables in generated C++ code.
# Global variables should not be captured - they're accessible everywhere.
# Use empty capture instead of capture-by-value.
if capture == "=":
capture = ""
if isinstance(value, ESPHomeDataBase) and value.esp_range is not None:
location = value.esp_range.start_mark
location.line += value.content_offset

View File

@@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main):
def test_text_config_lamda_is_set(generate_main):
"""
Test if lambda is set for lambda mode
Test if lambda is set for lambda mode (optimized with stateless lambda)
"""
# Given
@@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main):
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
# Then
assert "it_4->set_template([=]() -> esphome::optional<std::string> {" in main_cpp
assert "it_4->set_template([]() -> esphome::optional<std::string> {" in main_cpp
assert 'return std::string{"Hello"};' in main_cpp

View File

@@ -173,6 +173,61 @@ class TestLambdaExpression:
"}"
)
def test_str__stateless_no_return(self):
"""Test stateless lambda (empty capture) generates correctly"""
target = cg.LambdaExpression(
('ESP_LOGD("main", "Test message");',),
(), # No parameters
"", # Empty capture (stateless)
)
actual = str(target)
assert actual == ('[]() {\n ESP_LOGD("main", "Test message");\n}')
def test_str__stateless_with_return(self):
"""Test stateless lambda with return type generates correctly"""
target = cg.LambdaExpression(
("return global_value > 0;",),
(), # No parameters
"", # Empty capture (stateless)
bool, # Return type
)
actual = str(target)
assert actual == ("[]() -> bool {\n return global_value > 0;\n}")
def test_str__stateless_with_params(self):
"""Test stateless lambda with parameters generates correctly"""
target = cg.LambdaExpression(
("return foo + bar;",),
((int, "foo"), (float, "bar")),
"", # Empty capture (stateless)
float,
)
actual = str(target)
assert actual == (
"[](int32_t foo, float bar) -> float {\n return foo + bar;\n}"
)
def test_str__with_capture(self):
"""Test lambda with capture generates correctly"""
target = cg.LambdaExpression(
("return captured_var + x;",),
((int, "x"),),
"captured_var", # Has capture (not stateless)
int,
)
actual = str(target)
assert actual == (
"[captured_var](int32_t x) -> int32_t {\n return captured_var + x;\n}"
)
class TestLiterals:
@pytest.mark.parametrize(