Commit Graph
31 Commits
Author SHA1 Message Date
J. Nick Koston 2d86ffec7e [api] Drop the unused base decode(), group the vtable asserts, and test the decode emitters
A generated decodable message that lost its decode() would now fail to compile instead of
silently keeping its defaults. The asserts cover exactly the classes that derive from
ProtoDecodableMessage, under one ifdef per run instead of one per line, and the generator
tests pin the inline decode() wrapper, its absence on fixed vector messages, and the
this-> free static body.
2026-09-08 04:43:02 +02:00
J. Nick Koston 0e3554f6f8 [api] Decode without a vtable
decode_field() becomes a static per message function and the generated decode() hands it
to the shared loop as a function pointer, so decodable messages carry no vtable and no
vptr store at every construction site. The loop loses the two vtable loads per field. The
protected destructor on ProtoDecodableMessage goes with the virtuals; ProtoMessage keeps
its own guard for the dump builds that still have them.
2026-09-08 04:43:02 +02:00
J. Nick Koston d09f642eb2 [api] Name the null default invariant once and thread needs_decode through the pointer buffer base
The dead size parameter goes, the string type reads the inherited flag, the forced short
string path asserts against it, and the generated declarations say why the pointer may be
null.
2026-09-07 23:23:19 +02:00
J. Nick Koston 144cd419ad [api] Test which string fields get the null default 2026-09-07 23:11:47 +02:00
J. Nick Koston 097d487261 [api] Cover truncated bodies in the decode integration test and pin the 64 bit field rejection
Three malformed frames (a tag with a dangling continuation bit, a length prefix past the
payload, a two byte fixed32) must stop the decode loop without taking the connection down;
a generator test records that a double field is rejected before it could reach the loop.
2026-09-07 15:58:28 +02:00
J. Nick Koston 89082c7b2d [api] Cover repeated and sub message fields in the decode case tests 2026-09-07 15:57:14 +02:00
J. Nick Koston fb3befe377 [api] Second cleanup pass over the decode generator
Repeated fields encode their elements through one encode_element() hook instead of two
isinstance ladders, the fixed32 precomputed tag path owns its own guard, the generated
switches drop the dead default case, StringRef takes the byte pointer directly, the
three hand written tag expressions in proto.h go through proto_tag(), and stale comments
about the previous decode design go. The compiled functions are unchanged.
2026-09-07 15:57:14 +02:00
J. Nick Koston 5aec93d7e8 [api] Trim the decode path: void decode_field, bool from the low word, slow varint out parameter
decode_field() no longer returns a bool that only fed a verbose log; unknown fields are
skipped silently like every other protobuf decoder does, and each message loses the
return value materialisation. Bools read the low 32 bits of the varint, which drops the
second compare on 64 bit varint builds. The multi byte varint path writes its value
through an out parameter instead of returning a 16 byte struct, which takes the spills
out of the decode loop and count_repeated_field.
2026-09-07 15:57:13 +02:00
J. Nick Koston a79cd1550c [api] Collapse the generator's per wire type decode hooks into one
A type now sets a single decode_expr; the wire type it already declares picks the case
label. Drops the unused force_str helper, two dead decode_length overrides, a duplicate
field builder in the generator tests and a needless list copy in the state waiter. The
generated files are unchanged.
2026-09-07 15:57:13 +02:00
J. Nick Koston 6d23e4c842 [api] Key the generated decode switch on the wire tag on every target
The host no longer gets its own switch shape through USE_HOST; every build compiles the
same switch on the field's wire tag.
2026-09-07 15:57:13 +02:00
J. Nick Koston 2bc20d8721 [api] Derive decode cases from the type's wire type
decode_case() reads wire_type instead of taking it at every call, the
expression and the store statement are two small hooks that repeated
fields override, and message fields build one body. No generated case
declares a local any more, so the braced form and its test go; the
compiler rejects a jump over a local if one ever appears. The wire
type test now proves a dropped frame with an ordering marker instead
of assuming it, and shares StateWaiter.
2026-09-07 15:57:13 +02:00
J. Nick Koston e600180417 [api] Emit one decode case per field from a single generator property
With one decode_field() switch per message, the three per wire type
content properties only differed in the attribute they read; a single
decode_content built from decode_expr() replaces them, and repeated
fields reuse the element type's expression. Case bodies with several
statements get their block from the body itself instead of a caller
flag, the fixed byte array body copies straight from the payload
instead of through a heap std::string, and the decode comments no
longer restate the switch keying explained next to the macros.
2026-09-07 15:57:13 +02:00
J. Nick Koston 0641dae9d2 [api] Scope generated decode cases that declare locals
A case body with a declaration or several statements now gets its own
block, as the per wire type overrides had, so no jump to a later case
label crosses an initialization.
2026-09-07 15:57:13 +02:00
J. Nick Koston bcf812d62b [api] Keep the decode loop register resident and inline the varint fast path
CodSpeed showed the single virtual costing 7 to 18 percent on the
decode benchmarks. The x86-64 disassembly pointed at the call, not the
switch: passing the field number and wire type alongside the tag plus
a 16 byte union payload kept five values live across the call, so the
compiler spilled this, the end pointer and half of the payload to the
stack and reloaded them for every field.

decode_field() now takes only the tag, the payload pointer (already
the loop cursor) and one scalar that holds the varint or fixed32 value
or the payload length. The generated override wraps them in a
ProtoFieldValue that never exists in memory. On the host the switch
key is the field number derived with one shift and the guard compares
the whole tag against the constant the case declares, which is the
same two instructions the old per wire type dispatch cost.

The loop also handles single byte varints inline instead of going
through the parse result struct, which drops the materialized consumed
count and its add on every tag and small value.
2026-09-07 15:57:13 +02:00
J. Nick Koston b0ce7f58f3 [api] Collapse the three protobuf decode virtuals into one
Every decodable message overrode up to three virtuals, one per wire
type, so each carried a five slot vtable and up to three functions
with their own prologue and return tails. The shared decode loop now
parses the payload for the wire type into a ProtoFieldValue and calls
a single decode_field() virtual with the tag, the field number and the
wire type; the generated override is one switch.

The switch key is chosen per target through PROTO_DECODE_KEY. Embedded
builds compile switches to compare chains (ESP-IDF passes
-fno-jump-tables), so they key on the full wire tag, one compare per
field with no separate wire type check. The host compiler builds a
jump table for the dense field number switch, so there the key is the
field number and PROTO_DECODE_GUARD rejects a mismatched wire type.
Both forms drop a field that arrives with a wire type it does not
declare, exactly as the per wire type virtuals did.

Per decodable message the vtable shrinks from 20 to 12 bytes on
xtensa and the extra decode functions fold into one; the shared loop
shrinks as well. Host instruction counts per decoded field are
unchanged apart from the guard compare, which replaces the prologue of
the separate function it used to call.
2026-09-07 15:57:12 +02:00
J. Nick Koston 1b070629bc [api] Make generated encode and size entry points type erased
Every message sent through send_message or the entity paths needed a
proto_encode_msg<T> thunk (17 bytes on xtensa) and, for entity state
and info messages, a calc_size<T> thunk, because the generated encode
and calculate_size were member functions and the connection code wants
plain function pointers over const void *.

The generator now emits the bodies as static encode_msg(const void *)
and calc_size_msg(const void *) functions, so &T::encode_msg is
already a MessageEncodeFn and the thunks disappear. The member
encode() and calculate_size() remain as inline forwarders for direct
callers. ProtoMessage carries the same static defaults for messages
without fields, which also removes the separate no-op encode thunk.
2026-09-07 15:57:12 +02:00
J. Nick Koston c4e1360cdf [api] Check the encoded end against the reserved size under ESPHOME_DEBUG_API
The fixed32 store helper moves to a private section since it neither bounds checks nor
advances the cursor, its comment describes the path each target takes, the generated
file scan flags any ProtoEncode call that does not assign the cursor, and StateWaiter
timeouts can carry a label so gathered waits are told apart.
2026-09-07 15:57:10 +02:00
J. Nick Koston 709a1e1eb6 [api] Mark the raw encode helpers nodiscard too and drop a duplicate cursor test
The generated file scan already covers every emitted call, so the parametrized copy of
the same assertion goes.
2026-09-07 13:48:53 +02:00
J. Nick Koston 822b701792 [api] Mark the cursor returning encode helpers nodiscard
A call that drops the returned cursor would silently truncate the message, so the
compiler now warns on it and a unit test scans the generated file for the same mistake.
Also corrects the outlining comment for ESP8266, where the inline write is a few byte
stores rather than one, and the RAW_ENCODE_MAP annotation.
2026-09-07 12:37:48 +02:00
J. Nick Koston adbbda4072 [api] Emit every encode call through one generator helper
_encode_call() owns the cursor assignment and the _force suffix, so
the convention lives in one place instead of at every emission site;
the fixed32 fast path is an arm of the generic encode_content keyed by
a per type value template. write_fixed32_le uses convert_little_endian
instead of its own byte order switch. The integration test shares a
StateWaiter from state_utils and leaves the disconnect to the fixture.
2026-09-07 11:09:07 +02:00
J. Nick Koston 490aca17e6 [api] Share the fixed32 emission between float and fixed32 fields
One helper next to the other precomputed tag paths decides how a
single byte tag fixed32 field is written; the float and fixed32 types
only differ in the value expression. Drop the non forced std::string
encode_string overload, which the generator never emits, and build the
generator tests from one block of field type constants.
2026-09-07 10:33:21 +02:00
J. Nick Koston 3b14f4dfc8 [api] Pass the encode cursor by value through the protobuf helpers
The ProtoEncode helpers took the write cursor by reference and a
bool force flag. At -Os the compiler outlines most of them, so every
call site had to keep pos in a stack slot and pass its address, plus
a constant for the flag. The helpers now take the cursor by value and
return the advanced cursor, so consecutive calls chain through the
return register; forced fields call a _force overload instead of
passing a flag.

The fixed32 writers use __builtin_memcpy, which stays a builtin under
ESP-IDF's -fno-builtin-memcpy, and are outlined on embedded targets so
each fixed32 or float field is a short call instead of an inline
memcpy call. Non-forced float and fixed32 fields with a single-byte
tag share the same writer behind a zero check.

Generated encode bodies shrink by 18 percent on an ESP32 IDF proxy
build (2360 to 1932 bytes for 27 messages); entity messages gain the
most, for example ListEntitiesSensorResponse::encode 190 to 134 bytes
and SensorStateResponse::encode 78 to 49 bytes.
2026-09-07 09:21:54 +02:00
J. Nick Koston 145373ed13 [api] Widen message type storage to uint16_t (#18526) 2026-08-25 18:04:41 +00:00
Keith BurzinskiandJ. Nick Koston b97c877992 [api] Acknowledge proxy subscribe and configuration requests (#18312)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-08-24 20:27:24 +00:00
J. Nick Koston 37eae9b466 [api] Gate the bluetooth connection messages on their own define (#18281) 2026-08-11 21:31:13 -05:00
Jesse Hills f697cf2011 [api] Add DeviceCapabilities message for optional-feature flags (#17984) 2026-08-11 14:52:19 +12:00
J. Nick Koston 4424af8c5b [api] Move runtime log client out of the component package (#18043) 2026-08-03 19:57:52 -05:00
J. Nick Koston 2a59c0ad9e [logs] Cap the logs reconnect backoff for deep-sleep devices (#17656) 2026-07-17 17:16:14 -10:00
J. Nick Koston 5b4ae22f58 [api] Don't spam tracebacks when decoding a crash without a local build (#17597) 2026-07-16 08:08:28 -04:00
Diorcet Yann 690a197346 [main] Move stacktrace handling out of platformio_api and FlashImage into platform components/util (#16186) 2026-05-04 15:07:31 -04:00
J. Nick Koston 9ddb828da3 [api] Don't tear down log connection on stack-trace decode failure (#16196) 2026-05-03 20:05:13 -05:00