Merge remote-tracking branch 'upstream/eliminate-trigger-trampolines' into integration

This commit is contained in:
J. Nick Koston
2026-03-25 15:38:03 -10:00
6 changed files with 37 additions and 64 deletions
+10 -22
View File
@@ -671,8 +671,7 @@ async def build_callback_automation(
callback_method: str,
args: TemplateArgsType,
config: ConfigType,
forwarder: MockObjClass | None = None,
forwarder_extra_args: list | None = None,
forwarder: MockObj | MockObjClass | None = None,
) -> None:
"""Build an Automation and register it as a callback on the parent.
@@ -680,7 +679,9 @@ async def build_callback_automation(
automation's trigger() directly as a callback on the parent component.
Uses template forwarder structs so the compiler deduplicates the operator()
body across all call sites with the same signature.
body across all call sites with the same signature. The forwarder must be
pointer-sized (single Automation* field) to fit inline in Callback::ctx_
and avoid heap allocation.
:param parent: The component object (e.g., button, sensor).
:param callback_method: Name of the callback method (e.g., "add_on_press_callback").
@@ -688,11 +689,7 @@ async def build_callback_automation(
:param config: The automation config dict.
:param forwarder: Optional forwarder type to use instead of the default
TriggerForwarder<Ts...>. Pass any struct type whose aggregate init takes
an Automation pointer as the first field (e.g., TriggerOnTrueForwarder,
or a custom component-defined forwarder).
:param forwarder_extra_args: Optional list of extra MockObj args to pass to the
forwarder after the automation pointer in aggregate init. For example,
a lock forwarder needs the lock entity pointer: [lock_var].
a single Automation pointer (e.g., TriggerOnTrueForwarder).
"""
arg_types = [arg[0] for arg in args]
templ = cg.TemplateArguments(*arg_types)
@@ -703,17 +700,8 @@ async def build_callback_automation(
# one operator() per forwarder type; different automation pointers are just
# data in the struct.
if forwarder is None:
forwarder = (
TriggerForwarder.template(templ)
if arg_types
else TriggerForwarder.template()
)
init_args = str(obj)
if forwarder_extra_args:
extra = ", ".join(str(a) for a in forwarder_extra_args)
init_args = f"{init_args}, {extra}"
cg.add(
getattr(parent, callback_method)(
cg.RawExpression(f"{forwarder}{{{init_args}}}")
)
)
forwarder = TriggerForwarder.template(templ)
# RawExpression for aggregate init — both forwarder and obj are codegen
# MockObjs (not user input), and there's no Expression type for positional
# aggregate initialization (StructInitializer uses named fields).
cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}")))
+3 -2
View File
@@ -92,8 +92,9 @@ def event_schema(
@setup_entity("event")
async def setup_event_core_(var, config, *, event_types: list[str]):
for conf in config.get(CONF_ON_EVENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf)
await automation.build_callback_automation(
var, "add_on_event_callback", [(cg.StringRef, "event_type")], conf
)
cg.add(var.set_event_types(event_types))
+4 -17
View File
@@ -35,7 +35,6 @@ LockLockTrigger = lock_ns.class_("LockLockTrigger", automation.Trigger.template(
LockUnlockTrigger = lock_ns.class_("LockUnlockTrigger", automation.Trigger.template())
LockState = lock_ns.enum("LockState")
LockStateForwarder = lock_ns.class_("LockStateForwarder")
LOCK_STATES = {
"LOCKED": LockState.LOCK_STATE_LOCKED,
@@ -95,23 +94,11 @@ def lock_schema(
@setup_entity("lock")
async def _setup_lock_core(var, config):
for conf in config.get(CONF_ON_LOCK, []):
await automation.build_callback_automation(
var,
"add_on_state_callback",
[],
conf,
forwarder=LockStateForwarder.template(LockState.LOCK_STATE_LOCKED),
forwarder_extra_args=[var],
)
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_UNLOCK, []):
await automation.build_callback_automation(
var,
"add_on_state_callback",
[],
conf,
forwarder=LockStateForwarder.template(LockState.LOCK_STATE_UNLOCKED),
forwarder_extra_args=[var],
)
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
-10
View File
@@ -66,14 +66,4 @@ template<LockState State> class LockStateTrigger : public Trigger<> {
using LockLockTrigger = LockStateTrigger<LockState::LOCK_STATE_LOCKED>;
using LockUnlockTrigger = LockStateTrigger<LockState::LOCK_STATE_UNLOCKED>;
/// Forwarder that triggers an Automation<> when a Lock reaches a specific state.
template<LockState State> struct LockStateForwarder {
Automation<> *automation;
Lock *lock;
void operator()() const {
if (this->lock->state == State)
this->automation->trigger();
}
};
} // namespace esphome::lock
+14 -1
View File
@@ -470,6 +470,7 @@ template<typename... Ts> class ActionList {
template<typename... Ts> class Automation {
public:
/// Default constructor for use with TriggerForwarder (no Trigger object needed).
Automation() = default;
explicit Automation(Trigger<Ts...> *trigger) { trigger->set_automation_parent(this); }
@@ -493,12 +494,14 @@ template<typename... Ts> class Automation {
/// Callback forwarder that triggers an Automation directly.
/// One operator() instantiation per Automation<Ts...> signature, shared across all call sites.
/// Must stay pointer-sized to fit inline in Callback::ctx_ without heap allocation.
template<typename... Ts> struct TriggerForwarder {
Automation<Ts...> *automation;
void operator()(Ts... args) const { this->automation->trigger(args...); }
void operator()(const Ts &...args) const { this->automation->trigger(args...); }
};
/// Callback forwarder that triggers an Automation<> only when the bool arg is true.
/// Must stay pointer-sized to fit inline in Callback::ctx_ without heap allocation.
struct TriggerOnTrueForwarder {
Automation<> *automation;
void operator()(bool state) const {
@@ -508,6 +511,7 @@ struct TriggerOnTrueForwarder {
};
/// Callback forwarder that triggers an Automation<> only when the bool arg is false.
/// Must stay pointer-sized to fit inline in Callback::ctx_ without heap allocation.
struct TriggerOnFalseForwarder {
Automation<> *automation;
void operator()(bool state) const {
@@ -516,4 +520,13 @@ struct TriggerOnFalseForwarder {
}
};
// Ensure forwarders fit in Callback::ctx_ (pointer-sized inline storage).
// If these fail, the forwarder would heap-allocate in Callback::create().
static_assert(sizeof(TriggerForwarder<>) <= sizeof(void *));
static_assert(sizeof(TriggerOnTrueForwarder) <= sizeof(void *));
static_assert(sizeof(TriggerOnFalseForwarder) <= sizeof(void *));
static_assert(std::is_trivially_copyable_v<TriggerForwarder<>>);
static_assert(std::is_trivially_copyable_v<TriggerOnTrueForwarder>);
static_assert(std::is_trivially_copyable_v<TriggerOnFalseForwarder>);
} // namespace esphome
+6 -12
View File
@@ -187,7 +187,6 @@ def _build_forwarder(
automation_name: str,
args: list[tuple[str, str]],
forwarder: MockObj | None = None,
extra_args: list[str] | None = None,
) -> str:
"""Build a trigger forwarder expression the same way build_callback_automation does.
@@ -202,10 +201,7 @@ def _build_forwarder(
cg.TemplateArguments(*arg_types) if arg_types else cg.TemplateArguments()
)
forwarder = TriggerForwarder.template(templ)
init_args = str(obj)
if extra_args:
init_args += ", " + ", ".join(extra_args)
return f"{forwarder}{{{init_args}}}"
return f"{forwarder}{{{obj}}}"
def test_trigger_forwarder_no_args() -> None:
@@ -253,10 +249,8 @@ def test_trigger_forwarder_string_arg() -> None:
assert result == "TriggerForwarder<std::string>{auto_1}"
def test_trigger_forwarder_custom_with_extra_args() -> None:
"""Lock on_lock: custom forwarder with extra args for entity pointer."""
lock_forwarder = MockObj("LockStateForwarder<LOCK_STATE_LOCKED>", "")
result = _build_forwarder(
"auto_1", [], forwarder=lock_forwarder, extra_args=["lock_var"]
)
assert result == "LockStateForwarder<LOCK_STATE_LOCKED>{auto_1, lock_var}"
def test_trigger_forwarder_custom_type() -> None:
"""Custom forwarder type passed directly."""
custom = MockObj("MyForwarder", "")
result = _build_forwarder("auto_1", [], forwarder=custom)
assert result == "MyForwarder{auto_1}"