Compare commits

...
Author SHA1 Message Date
J. Nick Koston c2a4981e25 [api] Order the wrong wire type assertions on the raw socket
The barrier was a command on the aioesphomeapi connection, which nothing orders against the
raw frames under test; a well formed frame on the raw socket itself now closes each block and
the assertion checks the exact states seen since the marker.
2026-09-07 16:22:42 +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 4a3a787756 [api] Return the varint parse result as a struct again
The out parameter form regressed the host, where the 16 byte result already travels in
registers, by about 20 percent on the direct varint parse benchmarks. The void
decode_field and low word bool changes stay.
2026-09-07 15:57:13 +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 cbdcfe640c [api] Read fixed32 fields with byte loads and test the varint wire type first
ESP-IDF passes -fno-builtin-memcpy, so the four byte memcpy in the decode loop was an
out of line call on every fixed32 field; host compilers fold the byte loads back into one
load. Checking the varint wire type first keeps the common path to one taken branch on
xtensa.
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 2dbb3e055b [api] Add an integration test for decode wire type handling
Hand built frames check that decode_field() takes a field with its
declared wire type, drops the same field sent length delimited or as
fixed32, ignores a varint key, and skips an unknown field with a two
byte tag before decoding the rest. Client commands cover two byte tags
and varints, a two byte length prefix and a negative fixed32 float.
2026-09-07 15:57:13 +02:00
J. Nick Koston 7549309ad3 [api] Trim the decode dispatch comments 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 455e1d6374 [api] Assert the switch frame count instead of reading for it separately 2026-09-07 15:57:12 +02:00
J. Nick Koston 3af1d50bce [api] Trim the field free message test and a duplicated generator note 2026-09-07 15:57:12 +02:00
J. Nick Koston 842f354a05 [api] Add an integration test for field free messages
Ping, device info, list entities done and disconnect all travel through
the ProtoMessage static entry points now that the no-op thunk is gone.
2026-09-07 15:57:12 +02:00
J. Nick Koston af9b59d4bd [api] Trim the type erased entry point comments 2026-09-07 15:57:12 +02:00
J. Nick Koston 55fc5a10de [api] Tighten the ProtoMessage default entry point comment 2026-09-07 15:57:12 +02:00
J. Nick Koston 79927b918b [api] Clarify which encode entry points forward on ProtoMessage
The base class defaults are independent no-ops; only generated message
classes forward encode() and calculate_size() to their statics.
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 7c774699d7 [api] Outline the fixed32 writers on ARM cores without unaligned access too
Cortex-M0+ and ARM9 turn the four byte unaligned store into a memcpy call with a stack
temporary at every fixed32 field, and the outlined helper itself became a memcpy call
there, so the helper now spells out the byte stores. Xtensa and host objects are byte for
byte unchanged; on the RP2040 bench config the api object loses 28 bytes and the fixed32
memcpy calls.
2026-09-07 15:24:30 +02:00
J. Nick Koston ea71a24a9b [api] Mark the last two raw varint writers nodiscard and make StateWaiter failures visible
A predicate that raises now fails its wait instead of dying inside the state callback,
and a timeout names the predicate it was waiting for.
2026-09-07 14:01:34 +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 d2e4d2c46a [api] Outline the fixed32 writers only where memcpy is a call
On the ESP8266 the inline write was already a single store, so the
outlined helper cost a call per fixed32 field: sensor state encode went
from 615 to 864 ns on a d1 mini. ESP32 builds pass -fno-builtin-memcpy,
where the shared copy is both smaller and faster (562 to 328 ns on an
atom), so the gate is now USE_ESP32.
2026-09-07 11:40:01 +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 252bf6ea6a [api] Add an integration test for the encode branch boundaries
Covers a zero float that is skipped on the wire, a fixed32 state, a
negative int32, list entity strings and text states whose length
prefix needs two varint bytes, a two byte field tag through the
device info area, and the field free disconnect exchange.
2026-09-07 10:58:31 +02:00
J. Nick Koston 8ec9305688 [api] Trim the encode helper comments 2026-09-07 10:48:18 +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 b77e2441d4 [api] Undefine PROTO_OUTLINE_FOR_SIZE after the encode helpers
The macro only exists for the two fixed32 writers in ProtoEncode, so
drop it once the class is complete instead of leaking it into every
translation unit that includes proto.h.
2026-09-07 10:09:46 +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
Jesse Hills d34d3994e1 Merge branch 'beta' into dev 2026-09-07 12:50:44 +12:00
Jesse Hills d58b37faa1 [esp32_hosted] Add ESP-NOW-over-hosted shim for the ESP32-P4 (#17712) 2026-09-07 10:29:20 +12:00
J. Nick Koston 8966567be0 [core] Show the other downloader's progress while a prefetch job waits on its lock (#18983) 2026-09-07 10:28:31 +12:00
J. Nick Koston 20c7dcb1dd [mdns] Guard LEAmDNS main loop calls against lwIP re-entrancy on ESP8266 (#18990) 2026-09-07 10:26:44 +12:00
J. Nick Koston 688af60cbf [noise] Bump noise-c to 0.1.24 and libsodium to 1.10021.6 (#18989) 2026-09-07 10:12:52 +12:00
esphome[bot] 9c00f13606 Bump bundled esphome-device-builder to 1.14.4 (#19006) 2026-09-06 22:05:16 +00:00
J. Nick Koston 833dd0e812 [ota] Offer encryption with the api key so enabling it works over OTA (#18979) 2026-09-06 23:59:40 +02:00
Ricardo Sanz 8e1044e8ea [climate][template] New template climate component (#14455) 2026-09-06 14:03:07 -07:00
esphome[bot] e5200db6fd Bump bundled esphome-device-builder to 1.14.3 (#18996) 2026-09-06 09:28:02 +02:00
e3dd2f44a4 [mipi_dsi] Let IDF pick the DPHY PLL reference clock (#18984)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2026-09-05 21:08:21 +00:00
esphome[bot] 3ef7460fca Bump bundled esphome-device-builder to 1.14.2 (#18988) 2026-09-05 15:25:58 +00:00
Clyde Stubbspre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>Claude
ae187f81f2 [wifi] Allow a forced roam check (#17349)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-09-05 21:44:37 +10:00
esphome[bot] 84f78831f9 Bump bundled esphome-device-builder to 1.14.1 (#18981) 2026-09-05 13:07:15 +02:00
Keith Burzinski 13dbbcaa32 [usb_uart] Keep the comm interface number valid when its claim fails (#18968) 2026-09-05 13:00:25 +02:00
Clyde Stubbs b66822d9bd [ai] Advice to agents to limit verbiage (#18980) 2026-09-05 12:21:47 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d1829c495d Bump prek from 0.5.0 to 0.5.1 (#18977)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-04 19:05:36 -04:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ce87bf9b17 Bump platformdirs from 4.11.5 to 4.11.7 (#18976)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-04 19:05:26 -04:00
Jesse Hills 51ea97deff [esp32_ble] Reference count BLE advertising (#18943) 2026-09-05 08:38:55 +12:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ab800dc09d Bump filelock from 3.32.4 to 3.32.5 (#18963)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-03 17:19:29 -04:00
J. Nick Kostonandpre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> f65ab5629e [esp8266] Drop Arduino framework versions before 3.0.0 (#18917) to
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-09-03 15:16:36 -04:00
esphome[bot]esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>Jonathan Swoboda
b84532d254 Bump bundled esphome-device-builder to 1.14.0 (#18960)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-09-03 12:15:06 +00:00
Keith Burzinski 6b11636491 [remote_transmitter] Fix BK7231N build by limiting the PWM path to BK7238 (#18958) 2026-09-03 08:12:36 -04:00
Jesse Hills 2bb98f2d64 Merge branch 'beta' into dev 2026-09-03 14:07:41 +12:00
Jesse Hills f3c786c784 Bump version to 2026.10.0-dev 2026-09-03 13:07:21 +12:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2250430999 Bump zeroconf from 0.151.2 to 0.151.3 (#18951)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-02 20:56:51 -04:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d1068d582f Bump ninja from 1.13.0 to 1.13.2 (#18952)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-02 20:56:42 -04:00
31 changed files with 4343 additions and 3557 deletions
+1
View File
@@ -553,6 +553,7 @@ file does, and it is the authority when they disagree. The most useful starting
4. **Lint:** Run `prek` to ensure code is compliant. 4. **Lint:** Run `prek` to ensure code is compliant.
5. **Commit:** Commit your changes. There is no strict format for commit messages. 5. **Commit:** Commit your changes. There is no strict format for commit messages.
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title must start with a `[tag]` prefix. For component work, use the component name (e.g., `[display] Fix bug`, `[abc123] Add new component`); for changes to shared/core code that isn't tied to a single component, use `[core]` (e.g., `[core] Add validator`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template. 6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title must start with a `[tag]` prefix. For component work, use the component name (e.g., `[display] Fix bug`, `[abc123] Add new component`); for changes to shared/core code that isn't tied to a single component, use `[core]` (e.g., `[core] Add validator`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.
7. **Comments:** When commenting on GitHub PRs or issues, don't tag contributors, especially bots. Avoid referring to list items (e.g. from reviews) with the form #nn - this will be interpreted by GitHub as a reference to issue or PR nn. Keep comments short and exclude irrelevant details, backstories, restatement of previous comments and anything that is already obvious to the reader.
* **Documentation Contributions:** * **Documentation Contributions:**
* Documentation is hosted in the separate `esphome/esphome.io` repository. * Documentation is hosted in the separate `esphome/esphome.io` repository.
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2026.9.0b2 PROJECT_NUMBER = 2026.10.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a
+6 -1
View File
@@ -2255,7 +2255,12 @@ bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type,
// Capacity reserved above, cannot fail // Capacity reserved above, cannot fail
(void) shared_buf.resize(write_start + payload_size); (void) shared_buf.resize(write_start + payload_size);
ProtoWriteBuffer buffer{&shared_buf, write_start}; ProtoWriteBuffer buffer{&shared_buf, write_start};
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf)); uint8_t *end = encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
#ifdef ESPHOME_DEBUG_API
assert(end == shared_buf.data() + shared_buf.size());
#else
(void) end;
#endif
return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type);
} }
// encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE) // encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE)
+6 -24
View File
@@ -345,11 +345,7 @@ class APIConnection final : public APIServerConnectionBase {
/// Returns false as soon as the TCP buffer is full. Marked nodiscard so we /// Returns false as soon as the TCP buffer is full. Marked nodiscard so we
/// have no silent failures: every caller must handle (or log) a refusal. /// have no silent failures: every caller must handle (or log) a refusal.
template<typename T> [[nodiscard]] bool send_message(const T &msg) { template<typename T> [[nodiscard]] bool send_message(const T &msg) {
if constexpr (T::ESTIMATED_SIZE == 0) { return this->send_message_(T::calc_size_msg(&msg), T::MESSAGE_TYPE, &T::encode_msg, &msg);
return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg);
} else {
return this->send_message_(msg.calculate_size(), T::MESSAGE_TYPE, &proto_encode_msg<T>, &msg);
}
} }
/// Clear the shared write buffer and reserve space for the first message. /// Clear the shared write buffer and reserve space for the first message.
@@ -405,16 +401,6 @@ class APIConnection final : public APIServerConnectionBase {
void process_state_subscriptions_(); void process_state_subscriptions_();
#endif #endif
// Size thunk — converts void* back to concrete type for direct calculate_size() call
template<typename T> static uint32_t calc_size(const void *msg) {
return static_cast<const T *>(msg)->calculate_size();
}
// Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0)
static uint8_t *encode_msg_noop(const void *, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) {
return buf.get_pos();
}
// Non-template buffer management for send_message // Non-template buffer management for send_message
bool send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn, const void *msg); bool send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn, const void *msg);
@@ -433,11 +419,7 @@ class APIConnection final : public APIServerConnectionBase {
// Hot paths (state/info) go through fill_and_encode_entity_state/info instead. // Hot paths (state/info) go through fill_and_encode_entity_state/info instead.
// batch_message_type_ is already set by dispatch_message_ before reaching here. // batch_message_type_ is already set by dispatch_message_ before reaching here.
template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) { template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) {
if constexpr (T::ESTIMATED_SIZE == 0) { return encode_to_buffer_slow(T::calc_size_msg(&msg), &T::encode_msg, &msg, conn, remaining_size);
return encode_to_buffer_slow(0, &encode_msg_noop, &msg, conn, remaining_size);
} else {
return encode_to_buffer_slow(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
}
} }
// Non-template core — fills state fields and encodes // Non-template core — fills state fields and encodes
@@ -449,7 +431,7 @@ class APIConnection final : public APIServerConnectionBase {
template<typename T> template<typename T>
static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn, static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn,
uint32_t remaining_size) { uint32_t remaining_size) {
return fill_and_encode_entity_state(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size); return fill_and_encode_entity_state(entity, msg, &T::calc_size_msg, &T::encode_msg, conn, remaining_size);
} }
// Non-template core — fills info fields, allocates buffers, and encodes // Non-template core — fills info fields, allocates buffers, and encodes
@@ -461,7 +443,7 @@ class APIConnection final : public APIServerConnectionBase {
template<typename T> template<typename T>
static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn, static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn,
uint32_t remaining_size) { uint32_t remaining_size) {
return fill_and_encode_entity_info(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size); return fill_and_encode_entity_info(entity, msg, &T::calc_size_msg, &T::encode_msg, conn, remaining_size);
} }
// Non-template core — fills device_class, then delegates to fill_and_encode_entity_info // Non-template core — fills device_class, then delegates to fill_and_encode_entity_info
@@ -475,8 +457,8 @@ class APIConnection final : public APIServerConnectionBase {
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg, static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg,
StringRef &device_class_field, APIConnection *conn, StringRef &device_class_field, APIConnection *conn,
uint32_t remaining_size) { uint32_t remaining_size) {
return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &calc_size<T>, return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &T::calc_size_msg,
&proto_encode_msg<T>, conn, remaining_size); &T::encode_msg, conn, remaining_size);
} }
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
@@ -46,7 +46,13 @@ inline uint16_t ESPHOME_ALWAYS_INLINE APIConnection::encode_to_buffer(uint32_t c
return 0; return 0;
} }
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size}; ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf)); uint8_t *end = encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
#ifdef ESPHOME_DEBUG_API
// A body that writes fewer bytes than calculate_size() promised would ship stale buffer bytes
assert(end == shared_buf.data() + shared_buf.size());
#else
(void) end;
#endif
return total_calculated_size; return total_calculated_size;
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+59 -58
View File
@@ -214,73 +214,74 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
const uint8_t *ptr = buffer; const uint8_t *ptr = buffer;
const uint8_t *end = buffer + length; const uint8_t *end = buffer + length;
while (ptr < end) { // Single-byte varints dominate, so that case advances the cursor inline.
// Parse field header - ptr < end guarantees len >= 1 auto read_varint = [&](proto_varint_value_t &value) ESPHOME_ALWAYS_INLINE {
if (ptr == end)
return false;
if (*ptr < 0x80) [[likely]] {
value = *ptr++;
return true;
}
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr); auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
if (!res.has_value()) { if (!res.has_value())
return false;
value = res.value;
ptr += res.consumed;
return true;
};
while (ptr < end) {
proto_varint_value_t tag_value;
if (!read_varint(tag_value)) {
ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer)); ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
return; return;
} }
uint32_t tag = static_cast<uint32_t>(res.value); uint32_t tag = static_cast<uint32_t>(tag_value);
uint32_t field_type = tag & WIRE_TYPE_MASK; uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3; // Length-delimited fields move this past the length prefix
ptr += res.consumed; const uint8_t *data = ptr;
proto_varint_value_t scalar;
switch (field_type) { if (field_type == WIRE_TYPE_VARINT) [[likely]] {
case WIRE_TYPE_VARINT: { // VarInt if (!read_varint(scalar)) {
res = ProtoVarInt::parse(ptr, end - ptr); ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
return;
}
if (!this->decode_varint(field_id, res.value)) {
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id,
static_cast<uint64_t>(res.value));
}
ptr += res.consumed;
break;
}
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t field_length = static_cast<uint32_t>(res.value);
ptr += res.consumed;
if (field_length > static_cast<size_t>(end - ptr)) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
if (!this->decode_length(field_id, ProtoLengthDelimited(ptr, field_length))) {
ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id);
}
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit
if (end - ptr < 4) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t val;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
// Protobuf fixed32 is little-endian — direct load on LE platforms
memcpy(&val, ptr, 4);
#else
val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
#endif
if (!this->decode_32bit(field_id, Proto32Bit(val))) {
ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val);
}
ptr += 4;
break;
}
default:
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
return; return;
}
} else {
switch (field_type) {
case WIRE_TYPE_LENGTH_DELIMITED: {
proto_varint_value_t length_value;
if (!read_varint(length_value)) {
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t field_length = static_cast<uint32_t>(length_value);
if (field_length > static_cast<size_t>(end - ptr)) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
data = ptr;
scalar = field_length;
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: {
if (end - ptr < 4) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;
}
// Byte loads instead of memcpy: ESP-IDF passes -fno-builtin-memcpy, which made this a call
scalar = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
ptr += 4;
break;
}
default:
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
return;
}
} }
this->decode_field(tag, data, scalar);
} }
} }
+228 -166
View File
@@ -170,40 +170,43 @@ class ProtoVarInt {
class ProtoMessage; class ProtoMessage;
class ProtoSize; class ProtoSize;
class ProtoLengthDelimited { /// Case label for decode_field(): the wire tag of a field, so a field that arrives with another wire
/// type matches no case.
constexpr uint32_t proto_tag(uint32_t field_id, uint32_t wire_type) { return (field_id << 3) | wire_type; }
/// One decoded field: the payload pointer and a scalar holding the varint or fixed32 value, or the
/// length of a length-delimited field. The wire type in the tag says which applies; accessors do not check.
class ProtoFieldValue {
public: public:
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {} ProtoFieldValue(const uint8_t *data, proto_varint_value_t scalar) : data_(data), scalar_(scalar) {}
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
// Direct access to raw data without string allocation proto_varint_value_t as_varint() const { return this->scalar_; }
const uint8_t *data() const { return this->value_; } // A bool is sent as 0 or 1, so the low word is enough and saves a second compare with 64 bit varints
size_t size() const { return this->length_; } bool as_bool() const { return static_cast<uint32_t>(this->scalar_) != 0; }
/// Decode the length-delimited data into a message instance. // Length-delimited accessors
const uint8_t *data() const { return this->data_; }
size_t size() const { return static_cast<size_t>(this->scalar_); }
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->data_), this->size()); }
/// Decode the length-delimited payload into a message instance.
/// Template preserves concrete type so decode() resolves statically. /// Template preserves concrete type so decode() resolves statically.
template<typename T> void decode_to_message(T &msg) const; template<typename T> void decode_to_message(T &msg) const { msg.decode(this->data_, this->size()); }
protected: // Fixed32 accessors
const uint8_t *const value_; uint32_t as_fixed32() const { return static_cast<uint32_t>(this->scalar_); }
const size_t length_; int32_t as_sfixed32() const { return static_cast<int32_t>(this->as_fixed32()); }
};
class Proto32Bit {
public:
explicit Proto32Bit(uint32_t value) : value_(value) {}
uint32_t as_fixed32() const { return this->value_; }
int32_t as_sfixed32() const { return static_cast<int32_t>(this->value_); }
float as_float() const { float as_float() const {
union { union {
uint32_t raw; uint32_t raw;
float value; float value;
} s{}; } s{};
s.raw = this->value_; s.raw = this->as_fixed32();
return s.value; return s.value;
} }
protected: private:
const uint32_t value_; const uint8_t *data_;
proto_varint_value_t scalar_;
}; };
// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported // NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
@@ -252,7 +255,7 @@ class ProtoWriteBuffer {
* *
* Following https://protobuf.dev/programming-guides/encoding/#structure * Following https://protobuf.dev/programming-guides/encoding/#structure
*/ */
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw(proto_tag(field_id, type)); }
/// Single-pass encode for repeated submessage elements. /// Single-pass encode for repeated submessage elements.
/// Thin template wrapper; all buffer work is in the non-template core. /// Thin template wrapper; all buffer work is in the non-template core.
template<typename T> void encode_sub_message(uint32_t field_id, const T &value); template<typename T> void encode_sub_message(uint32_t field_id, const T &value);
@@ -287,19 +290,31 @@ class ProtoWriteBuffer {
uint8_t *pos_; uint8_t *pos_;
}; };
// A four byte unaligned store is a memcpy call on ESP-IDF (-fno-builtin-memcpy) and on ARM cores without
// unaligned access (Cortex-M0+, ARM9), so those targets share one outlined byte store helper per fixed32
// field. Elsewhere the write inlines to a single store, or on ESP8266 to a few stores that measured
// faster than a call, so it stays inline.
#if defined(USE_ESP32) || (defined(__arm__) && !defined(__ARM_FEATURE_UNALIGNED))
#define PROTO_OUTLINE_FOR_SIZE __attribute__((noinline))
#define PROTO_FIXED32_BYTE_STORES true
#else
#define PROTO_OUTLINE_FOR_SIZE inline
#define PROTO_FIXED32_BYTE_STORES false
#endif
// Varint encoding thresholds — used by both proto_encode_* free functions and ProtoSize. // Varint encoding thresholds — used by both proto_encode_* free functions and ProtoSize.
constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128 constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128
constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384 constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384
/// Static encode helpers for generated encode() functions. /// Static encode helpers for the generated encode bodies. Each takes the write cursor by value and
/// Generated code hoists buffer.pos_ into a local uint8_t *__restrict__ pos, /// returns it advanced, so outlined calls at -Os chain through the return register instead of a
/// then calls these methods which take pos by reference. No struct, no overhead. /// stack slot. Helpers without a _force suffix skip fields holding the proto3 default.
/// For sub-messages, pos is synced back to buffer before the call and reloaded after.
class ProtoEncode { class ProtoEncode {
public: public:
/// Write a multi-byte varint directly through a pos pointer. /// Write a multi-byte varint directly through a pos pointer.
template<typename T> template<typename T>
static inline void encode_varint_raw_loop(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, T value) { [[nodiscard]] static inline uint8_t *encode_varint_raw_loop(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
T value) {
do { do {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value | 0x80); *pos++ = static_cast<uint8_t>(value | 0x80);
@@ -307,48 +322,49 @@ class ProtoEncode {
} while (value > 0x7F); } while (value > 0x7F);
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value); *pos++ = static_cast<uint8_t>(value);
return pos;
} }
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
uint32_t value) { encode_varint_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t value) {
if (value < VARINT_MAX_1_BYTE) [[likely]] { if (value < VARINT_MAX_1_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value); *pos++ = static_cast<uint8_t>(value);
return; return pos;
} }
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value); return encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
} }
/// Encode a varint that is expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths). /// Encode a varint that is expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths).
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_short(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
uint32_t value) { encode_varint_raw_short(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t value) {
if (value < VARINT_MAX_1_BYTE) [[likely]] { if (value < VARINT_MAX_1_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value); *pos++ = static_cast<uint8_t>(value);
return; return pos;
} }
if (value < VARINT_MAX_2_BYTE) [[likely]] { if (value < VARINT_MAX_2_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 2); PROTO_ENCODE_CHECK_BOUNDS(pos, 2);
*pos++ = static_cast<uint8_t>(value | 0x80); *pos++ = static_cast<uint8_t>(value | 0x80);
*pos++ = static_cast<uint8_t>(value >> 7); *pos++ = static_cast<uint8_t>(value >> 7);
return; return pos;
} }
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value); return encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
} }
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
uint64_t value) { encode_varint_raw_64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint64_t value) {
if (value < VARINT_MAX_1_BYTE) [[likely]] { if (value < VARINT_MAX_1_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value); *pos++ = static_cast<uint8_t>(value);
return; return pos;
} }
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value); return encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
} }
/// Encode a 48-bit MAC address (stored in a uint64) as varint. /// Encode a 48-bit MAC address (stored in a uint64) as varint.
/// Real MAC addresses occupy the full 48 bits (OUI in upper 24), so the /// Real MAC addresses occupy the full 48 bits (OUI in upper 24), so the
/// fast path -- any non-zero bit in the top 6 of 48 -- emits exactly 7 bytes /// fast path -- any non-zero bit in the top 6 of 48 -- emits exactly 7 bytes
/// with no per-byte branch. Falls back to the general loop otherwise. /// with no per-byte branch. Falls back to the general loop otherwise.
/// Caller must guarantee value fits in 48 bits (checked in debug builds). /// Caller must guarantee value fits in 48 bits (checked in debug builds).
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_48bit(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
uint64_t value) { encode_varint_raw_48bit(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint64_t value) {
#ifdef ESPHOME_DEBUG_API #ifdef ESPHOME_DEBUG_API
assert(value < (1ULL << (MAC_ADDRESS_SIZE * 8)) && "encode_varint_raw_48bit: value exceeds 48 bits"); assert(value < (1ULL << (MAC_ADDRESS_SIZE * 8)) && "encode_varint_raw_48bit: value exceeds 48 bits");
#endif #endif
@@ -363,38 +379,39 @@ class ProtoEncode {
pos[4] = static_cast<uint8_t>((value >> 28) | 0x80); pos[4] = static_cast<uint8_t>((value >> 28) | 0x80);
pos[5] = static_cast<uint8_t>((value >> 35) | 0x80); pos[5] = static_cast<uint8_t>((value >> 35) | 0x80);
pos[6] = static_cast<uint8_t>(value >> 42); pos[6] = static_cast<uint8_t>(value >> 42);
pos += 7; return pos + 7;
return;
} }
encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value); return encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
} }
static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
uint32_t field_id, uint32_t type) { encode_field_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, uint32_t type) {
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type); return encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, proto_tag(field_id, type));
} }
/// Write a single precomputed tag byte. Tag must be < 128. /// Write a single precomputed tag byte. Tag must be < 128.
static inline void ESPHOME_ALWAYS_INLINE write_raw_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
uint8_t b) { write_raw_byte(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t b) {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = b; *pos++ = b;
return pos;
} }
/// Reserve one byte for later backpatch (e.g., sub-message length). /// Reserve one byte for later backpatch (e.g., sub-message length).
/// Advances pos past the reserved byte without writing a value. /// Advances pos past the reserved byte without writing a value.
static inline void ESPHOME_ALWAYS_INLINE reserve_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM) { [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
reserve_byte(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
pos++; return pos + 1;
} }
/// Write raw bytes to the buffer (no tag, no length prefix). /// Write raw bytes to the buffer (no tag, no length prefix).
static inline void ESPHOME_ALWAYS_INLINE encode_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
const void *data, size_t len) { encode_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, const void *data, size_t len) {
PROTO_ENCODE_CHECK_BOUNDS(pos, len); PROTO_ENCODE_CHECK_BOUNDS(pos, len);
std::memcpy(pos, data, len); std::memcpy(pos, data, len);
pos += len; return pos + len;
} }
/// Encode tag + 1-byte length + raw string data. For strings with max_data_length < 128. /// Encode tag + 1-byte length + raw string data. For strings with max_data_length < 128.
/// Tag must be a single-byte varint (< 128). Always encodes (no zero check). /// Tag must be a single-byte varint (< 128). Always encodes (no zero check).
static inline void encode_short_string_force(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, [[nodiscard]] static inline uint8_t *encode_short_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
const StringRef &ref) { uint8_t tag, const StringRef &ref) {
#ifdef ESPHOME_DEBUG_API #ifdef ESPHOME_DEBUG_API
assert(ref.size() < 128 && "encode_short_string_force: string exceeds max_data_length < 128"); assert(ref.size() < 128 && "encode_short_string_force: string exceeds max_data_length < 128");
#endif #endif
@@ -402,137 +419,191 @@ class ProtoEncode {
pos[0] = tag; pos[0] = tag;
pos[1] = static_cast<uint8_t>(ref.size()); pos[1] = static_cast<uint8_t>(ref.size());
std::memcpy(pos + 2, ref.c_str(), ref.size()); std::memcpy(pos + 2, ref.c_str(), ref.size());
pos += 2 + ref.size(); return pos + 2 + ref.size();
} }
/// Write a precomputed tag byte + 32-bit value in one operation. /// Write a precomputed tag byte + 32-bit value. Outlined on embedded: one copy beats inline stores per field.
static inline void ESPHOME_ALWAYS_INLINE write_tag_and_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *write_tag_and_fixed32(
uint8_t tag, uint32_t value) { uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, uint32_t value) {
PROTO_ENCODE_CHECK_BOUNDS(pos, 5); PROTO_ENCODE_CHECK_BOUNDS(pos, 5);
pos[0] = tag; pos[0] = tag;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ write_fixed32_le(pos + 1, value);
std::memcpy(pos + 1, &value, 4); return pos + 5;
#else
pos[1] = static_cast<uint8_t>(value & 0xFF);
pos[2] = static_cast<uint8_t>((value >> 8) & 0xFF);
pos[3] = static_cast<uint8_t>((value >> 16) & 0xFF);
pos[4] = static_cast<uint8_t>((value >> 24) & 0xFF);
#endif
pos += 5;
} }
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
const char *string, size_t len, bool force = false) { uint32_t field_id, const char *string, size_t len) {
if (len == 0 && !force) pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string
return;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string
// NOLINTNEXTLINE(readability-inconsistent-ifelse-braces) -- false positive on [[likely]] attribute // NOLINTNEXTLINE(readability-inconsistent-ifelse-braces) -- false positive on [[likely]] attribute
if (len < VARINT_MAX_1_BYTE) [[likely]] { if (len < VARINT_MAX_1_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len); PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len);
*pos++ = static_cast<uint8_t>(len); *pos++ = static_cast<uint8_t>(len);
} else { } else {
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len); pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len);
PROTO_ENCODE_CHECK_BOUNDS(pos, len); PROTO_ENCODE_CHECK_BOUNDS(pos, len);
} }
std::memcpy(pos, string, len); std::memcpy(pos, string, len);
pos += len; return pos + len;
} }
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_string(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
const std::string &value, bool force = false) { uint32_t field_id, const char *string, size_t len) {
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, value.data(), value.size(), force); if (len == 0)
return pos;
return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, string, len);
} }
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
const StringRef &ref, bool force = false) { uint32_t field_id, const std::string &value) {
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size(), force); return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value.data(), value.size());
} }
static inline void encode_bytes(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_string(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
const uint8_t *data, size_t len, bool force = false) { uint32_t field_id, const StringRef &ref) {
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len, force); return encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size());
} }
static inline void encode_uint32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t value, bool force = false) { uint32_t field_id, const StringRef &ref) {
if (value == 0 && !force) return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size());
return;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, value);
} }
static inline void encode_uint64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_bytes(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint64_t value, bool force = false) { uint32_t field_id, const uint8_t *data, size_t len) {
if (value == 0 && !force) return encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len);
return;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
} }
static inline void encode_bool(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, bool value, [[nodiscard]] static inline uint8_t *encode_bytes_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
bool force = false) { uint32_t field_id, const uint8_t *data, size_t len) {
if (!value && !force) return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len);
return; }
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0); [[nodiscard]] static inline uint8_t *encode_uint32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint32_t value) {
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
return encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, value);
}
[[nodiscard]] static inline uint8_t *encode_uint32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint32_t value) {
if (value == 0)
return pos;
return encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
}
[[nodiscard]] static inline uint8_t *encode_uint64_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint64_t value) {
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
return encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
}
[[nodiscard]] static inline uint8_t *encode_uint64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint64_t value) {
if (value == 0)
return pos;
return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
}
[[nodiscard]] static inline uint8_t *encode_bool_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, bool value) {
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = value ? 0x01 : 0x00; *pos++ = value ? 0x01 : 0x00;
return pos;
} }
static inline void encode_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_bool(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t value, bool force = false) { uint32_t field_id, bool value) {
if (value == 0 && !force) if (!value)
return; return pos;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 5); return encode_bool_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
}
/// Tag + fixed32 for multi-byte tags; single-byte tags use write_tag_and_fixed32.
[[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *encode_fixed32_force(
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, uint32_t value) {
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 5);
PROTO_ENCODE_CHECK_BOUNDS(pos, 4); PROTO_ENCODE_CHECK_BOUNDS(pos, 4);
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ write_fixed32_le(pos, value);
std::memcpy(pos, &value, 4); return pos + 4;
pos += 4; }
#else [[nodiscard]] static inline uint8_t *encode_fixed32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
*pos++ = (value >> 0) & 0xFF; uint32_t field_id, uint32_t value) {
*pos++ = (value >> 8) & 0xFF; if (value == 0)
*pos++ = (value >> 16) & 0xFF; return pos;
*pos++ = (value >> 24) & 0xFF; return encode_fixed32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
#endif
} }
// NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
// not supported to reduce overhead on embedded systems. All ESPHome devices are // not supported to reduce overhead on embedded systems. All ESPHome devices are
// 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support // 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support
// is needed in the future, the necessary encoding/decoding functions must be added. // is needed in the future, the necessary encoding/decoding functions must be added.
static inline void encode_float(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, float value, [[nodiscard]] static inline uint8_t *encode_float(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
bool force = false) { uint32_t field_id, float value) {
uint32_t raw = float_to_raw(value); return encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, field_id, float_to_raw(value));
if (raw == 0 && !force)
return;
encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, field_id, raw);
} }
static inline void encode_int32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int32_t value, [[nodiscard]] static inline uint8_t *encode_float_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
bool force = false) { uint32_t field_id, float value) {
return encode_fixed32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, float_to_raw(value));
}
[[nodiscard]] static inline uint8_t *encode_int32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, int32_t value) {
if (value < 0) { if (value < 0) {
// negative int32 is always 10 byte long // negative int32 is always 10 byte long
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value), force); return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value));
return;
} }
encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint32_t>(value), force); return encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint32_t>(value));
} }
static inline void encode_int64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int64_t value, [[nodiscard]] static inline uint8_t *encode_int32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
bool force = false) { uint32_t field_id, int32_t value) {
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value), force); if (value == 0)
return pos;
return encode_int32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
} }
static inline void encode_sint32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_int64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
int32_t value, bool force = false) { uint32_t field_id, int64_t value) {
encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value), force); return encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value));
} }
static inline void encode_sint64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_int64_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
int64_t value, bool force = false) { uint32_t field_id, int64_t value) {
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value), force); return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value));
} }
/// Sub-message encoding: sync pos to buffer, delegate, get pos from return value. [[nodiscard]] static inline uint8_t *encode_sint32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, int32_t value) {
return encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value));
}
[[nodiscard]] static inline uint8_t *encode_sint32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, int32_t value) {
return encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value));
}
[[nodiscard]] static inline uint8_t *encode_sint64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, int64_t value) {
return encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value));
}
[[nodiscard]] static inline uint8_t *encode_sint64_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, int64_t value) {
return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value));
}
/// Sub-message encoding: sync pos to buffer, delegate, read the cursor back.
template<typename T> template<typename T>
static inline void encode_sub_message(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, ProtoWriteBuffer &buffer, [[nodiscard]] static inline uint8_t *encode_sub_message(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, const T &value) { ProtoWriteBuffer &buffer, uint32_t field_id, const T &value) {
buffer.set_pos(pos); buffer.set_pos(pos);
buffer.encode_sub_message(field_id, value); buffer.encode_sub_message(field_id, value);
pos = buffer.get_pos(); return buffer.get_pos();
} }
template<typename T> template<typename T>
static inline void encode_optional_sub_message(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *encode_optional_sub_message(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
ProtoWriteBuffer &buffer, uint32_t field_id, const T &value) { ProtoWriteBuffer &buffer, uint32_t field_id,
const T &value) {
buffer.set_pos(pos); buffer.set_pos(pos);
buffer.encode_optional_sub_message(field_id, value); buffer.encode_optional_sub_message(field_id, value);
pos = buffer.get_pos(); return buffer.get_pos();
}
private:
/// Unaligned little endian store of four bytes: byte stores where the outlined helper lives (ESP-IDF, ARM
/// without unaligned access), otherwise a memcpy the compiler folds into one store. Callers bounds check
/// and advance the cursor themselves.
static inline void ESPHOME_ALWAYS_INLINE write_fixed32_le(uint8_t *__restrict__ pos, uint32_t value) {
if constexpr (PROTO_FIXED32_BYTE_STORES) {
// Spelled out so the outlined helper does not itself become a memcpy call
pos[0] = static_cast<uint8_t>(value);
pos[1] = static_cast<uint8_t>(value >> 8);
pos[2] = static_cast<uint8_t>(value >> 16);
pos[3] = static_cast<uint8_t>(value >> 24);
} else {
const uint32_t le = convert_little_endian(value);
__builtin_memcpy(pos, &le, 4);
}
} }
}; };
#undef PROTO_OUTLINE_FOR_SIZE
#undef PROTO_FIXED32_BYTE_STORES
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
/** /**
@@ -624,11 +695,12 @@ class DumpBuffer {
class ProtoMessage { class ProtoMessage {
public: public:
// Non-virtual defaults for messages with no fields. // Non-virtual defaults for messages with no fields; generated classes hide all four. The
// Concrete message classes hide these with their own implementations. // static encode_msg/calc_size_msg take const void * so &T::encode_msg needs no thunk.
// All call sites use templates to preserve the concrete type, so virtual static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) {
// dispatch is not needed. This eliminates per-message vtable entries for return buffer.get_pos();
// encode/calculate_size, saving ~1.3 KB of flash across all message types. }
static uint32_t calc_size_msg(const void *self) { return 0; }
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { return buffer.get_pos(); } uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { return buffer.get_pos(); }
uint32_t calculate_size() const { return 0; } uint32_t calculate_size() const { return 0; }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -663,10 +735,10 @@ class ProtoDecodableMessage : public ProtoMessage {
protected: protected:
~ProtoDecodableMessage() = default; ~ProtoDecodableMessage() = default;
virtual bool decode_varint(uint32_t field_id, proto_varint_value_t value) { return false; } /// Store one decoded field; \p scalar is the varint or fixed32 value, or the length of the
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; } /// length-delimited payload at \p data. An unknown field or wrong wire type matches no case and is skipped.
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; } /// Three register arguments keep the decode loop free of spills.
// NOTE: decode_64bit removed - wire type 1 not supported virtual void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {}
}; };
class ProtoSize { class ProtoSize {
@@ -792,7 +864,7 @@ class ProtoSize {
* @return The number of bytes needed to encode the field ID and wire type * @return The number of bytes needed to encode the field ID and wire type
*/ */
static constexpr uint32_t field(uint32_t field_id, uint32_t type) { static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
uint32_t tag = (field_id << 3) | (type & WIRE_TYPE_MASK); uint32_t tag = proto_tag(field_id, type & WIRE_TYPE_MASK);
return varint(tag); return varint(tag);
} }
@@ -876,24 +948,14 @@ class ProtoSize {
// Implementation of methods that depend on ProtoSize being fully defined // Implementation of methods that depend on ProtoSize being fully defined
// Encode thunk — converts void* back to concrete type for direct encode() call
template<typename T> uint8_t *proto_encode_msg(const void *msg, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) {
return static_cast<const T *>(msg)->encode(buf PROTO_ENCODE_DEBUG_ARG);
}
// Thin template wrapper; delegates to non-template core in proto.cpp. // Thin template wrapper; delegates to non-template core in proto.cpp.
template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) { template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) {
this->encode_sub_message(field_id, &value, &proto_encode_msg<T>); this->encode_sub_message(field_id, &value, &T::encode_msg);
} }
// Thin template wrapper; delegates to non-template core. // Thin template wrapper; delegates to non-template core.
template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) { template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) {
this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>); this->encode_optional_sub_message(field_id, T::calc_size_msg(&value), &value, &T::encode_msg);
}
// Template decode_to_message - preserves concrete type so decode() resolves statically
template<typename T> void ProtoLengthDelimited::decode_to_message(T &msg) const {
msg.decode(this->value_, this->length_);
} }
template<typename T> const char *proto_enum_to_string(T value); template<typename T> const char *proto_enum_to_string(T value);
+2 -2
View File
@@ -35,8 +35,8 @@ void MipiDsi::setup() {
.bus_id = 0, // index from 0, specify the DSI host to use .bus_id = 0, // index from 0, specify the DSI host to use
.num_data_lanes = .num_data_lanes =
this->lanes_, // Number of data lanes to use, can't set a value that exceeds the chip's capability this->lanes_, // Number of data lanes to use, can't set a value that exceeds the chip's capability
.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, // Clock source for the DPHY // phy_clk_src left at 0 to enable runtime auto-select.
.lane_bit_rate_mbps = this->lane_bit_rate_, // Bit rate of the data lanes, in Mbps .lane_bit_rate_mbps = this->lane_bit_rate_, // Bit rate of the data lanes, in Mbps
}; };
auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_); auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_);
if (err != ESP_OK) { if (err != ESP_OK) {
+15 -1
View File
@@ -66,13 +66,14 @@ from esphome.const import (
) )
from esphome.core import ( from esphome.core import (
CORE, CORE,
ID,
CoroPriority, CoroPriority,
EsphomeError, EsphomeError,
HexInt, HexInt,
coroutine_with_priority, coroutine_with_priority,
) )
import esphome.final_validate as fv import esphome.final_validate as fv
from esphome.types import ConfigType from esphome.types import ConfigType, TemplateArgsType
from . import wpa2_eap from . import wpa2_eap
@@ -208,6 +209,7 @@ WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition)
WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition) WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition)
WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action) WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action)
WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action) WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action)
WiFiRoamAction = wifi_ns.class_("WiFiRoamAction", automation.Action)
WiFiConfigureAction = wifi_ns.class_( WiFiConfigureAction = wifi_ns.class_(
"WiFiConfigureAction", automation.Action, cg.Component "WiFiConfigureAction", automation.Action, cg.Component
) )
@@ -820,6 +822,18 @@ async def wifi_disable_to_code(config, action_id, template_arg, args):
return cg.new_Pvariable(action_id, template_arg) return cg.new_Pvariable(action_id, template_arg)
@automation.register_action(
"wifi.roam", WiFiRoamAction, cv.Schema({}), synchronous=True
)
async def wifi_roam_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> cg.MockObj:
return cg.new_Pvariable(action_id, template_arg)
KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results"
RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save" RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save"
RUNTIME_ROAMING_SUPPRESSION_KEY = "wifi_runtime_roaming_suppression" RUNTIME_ROAMING_SUPPRESSION_KEY = "wifi_runtime_roaming_suppression"
+5
View File
@@ -31,6 +31,11 @@ template<typename... Ts> class WiFiDisableAction final : public Action<Ts...> {
void play(const Ts &...x) override { global_wifi_component->disable(); } void play(const Ts &...x) override { global_wifi_component->disable(); }
}; };
template<typename... Ts> class WiFiRoamAction final : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_wifi_component->force_roam_check(); }
};
template<typename... Ts> class WiFiConfigureAction final : public Action<Ts...>, public Component { template<typename... Ts> class WiFiConfigureAction final : public Action<Ts...>, public Component {
public: public:
TEMPLATABLE_VALUE(std::string, ssid) TEMPLATABLE_VALUE(std::string, ssid)
+27 -11
View File
@@ -846,17 +846,18 @@ void WiFiComponent::loop() {
this->notify_connect_state_listeners_(); this->notify_connect_state_listeners_();
#endif #endif
// Post-connect roaming: check for better AP // Post-connect roaming: check for better AP. A scan may have been started by an
if (this->post_connect_roaming_) { // explicit force_roam_check() even when post_connect_roaming_ is disabled, so the
if (this->is_roaming_scan_active()) { // scan must always be consumed here to avoid leaving roaming_state_ stuck.
if (this->scan_done_) { if (this->is_roaming_scan_active()) {
this->process_roaming_scan_(); if (this->scan_done_) {
} this->process_roaming_scan_();
// else: scan in progress, wait
} else if (this->roaming_state_ == RoamingState::IDLE && this->roaming_attempts_ < ROAMING_MAX_ATTEMPTS &&
now - this->roaming_last_check_ >= ROAMING_CHECK_INTERVAL && !this->roaming_suppressed_()) {
this->check_roaming_(now);
} }
// else: scan in progress, wait
} else if (this->post_connect_roaming_ && this->roaming_state_ == RoamingState::IDLE &&
this->roaming_attempts_ < ROAMING_MAX_ATTEMPTS &&
now - this->roaming_last_check_ >= ROAMING_CHECK_INTERVAL && !this->roaming_suppressed_()) {
this->check_roaming_(now);
} }
} }
break; break;
@@ -2463,6 +2464,17 @@ void WiFiComponent::notify_scan_results_listeners_() {
} }
#endif // USE_WIFI_SCAN_RESULTS_LISTENERS #endif // USE_WIFI_SCAN_RESULTS_LISTENERS
void WiFiComponent::force_roam_check() {
if (!this->is_connected() || this->roaming_state_ != RoamingState::IDLE || this->roaming_suppressed_()) {
ESP_LOGD(TAG, "Roam check requested, but not able to check now");
return;
}
// Reset the attempt counter so a prior run of failed roams doesn't block this explicit request
// Note that this re-arms automatic roaming if enabled.
this->roaming_attempts_ = 0;
this->check_roaming_(millis());
}
void WiFiComponent::check_roaming_(uint32_t now) { void WiFiComponent::check_roaming_(uint32_t now) {
// Guard: not for hidden networks (may not appear in scan) // Guard: not for hidden networks (may not appear in scan)
const WiFiAP *selected = this->get_selected_sta_(); const WiFiAP *selected = this->get_selected_sta_();
@@ -2484,7 +2496,11 @@ void WiFiComponent::check_roaming_(uint32_t now) {
ESP_LOGD(TAG, "Roam scan (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); ESP_LOGD(TAG, "Roam scan (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
this->roaming_state_ = RoamingState::SCANNING; this->roaming_state_ = RoamingState::SCANNING;
this->wifi_scan_start_(this->passive_scan_); if (!this->wifi_scan_start_(this->passive_scan_)) {
// Scan failed to start (e.g. busy) - don't get stuck in SCANNING forever
ESP_LOGD(TAG, "Roam scan failed to start");
this->roaming_state_ = RoamingState::IDLE;
}
} }
void WiFiComponent::process_roaming_scan_() { void WiFiComponent::process_roaming_scan_() {
+6
View File
@@ -565,6 +565,12 @@ class WiFiComponent final : public Component {
void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; } void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; }
void set_post_connect_roaming(bool enabled) { this->post_connect_roaming_ = enabled; } void set_post_connect_roaming(bool enabled) { this->post_connect_roaming_ = enabled; }
/** Force an immediate post-connect roaming check, bypassing the periodic interval and the
* per-connection attempt limit. Does nothing (besides a debug log) if not connected, if a
* roam scan or connect is already in progress, or if roaming is currently suppressed.
*/
void force_roam_check();
#ifdef USE_WIFI_CONNECT_TRIGGER #ifdef USE_WIFI_CONNECT_TRIGGER
Trigger<> *get_connect_trigger() { return &this->connect_trigger_; } Trigger<> *get_connect_trigger() { return &this->connect_trigger_; }
#endif #endif
+1 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum from esphome.enum import StrEnum
__version__ = "2026.9.0b2" __version__ = "2026.10.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = ( VALID_SUBSTITUTIONS_CHARACTERS = (
+4 -4
View File
@@ -14,7 +14,7 @@ esptool==5.3.1
click==8.3.3 click==8.3.3
aioesphomeapi==46.3.0 aioesphomeapi==46.3.0
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
zeroconf==0.151.2 zeroconf==0.151.3
puremagic==2.2.0 puremagic==2.2.0
ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml==0.19.1 # dashboard_import
ruamel.yaml.clib==0.2.15 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import
@@ -27,9 +27,9 @@ bleak==3.0.2
smpclient==7.2.0 smpclient==7.2.0
requests==2.34.2 requests==2.34.2
py7zr==1.1.3 py7zr==1.1.3
platformdirs==4.11.5 # native esp-idf toolchain global cache dir platformdirs==4.11.7 # native esp-idf toolchain global cache dir
ninja==1.13.0 # native esp8266 arduino toolchain build driver ninja==1.13.2 # native esp8266 arduino toolchain build driver
filelock==3.32.4 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg filelock==3.32.5 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg
# esp-idf >= 5.0 requires this # esp-idf >= 5.0 requires this
pyparsing >= 3.3.2 pyparsing >= 3.3.2
+1 -1
View File
@@ -2,7 +2,7 @@ pylint==4.0.8
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.16.5 # also change in .pre-commit-config.yaml when updating ruff==0.16.5 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
prek==0.5.0 # also change in .github/workflows/ci.yml when updating prek==0.5.1 # also change in .github/workflows/ci.yml when updating
# Unit tests # Unit tests
pytest==9.1.1 pytest==9.1.1
+213 -292
View File
@@ -28,6 +28,11 @@ class WireType(IntEnum):
END_GROUP = 4 # groups (deprecated) END_GROUP = 4 # groups (deprecated)
FIXED32 = 5 # fixed32, sfixed32, float FIXED32 = 5 # fixed32, sfixed32, float
@property
def cpp_name(self) -> str:
"""The matching constant in proto.h."""
return f"WIRE_TYPE_{self.name}"
# Generate with # Generate with
# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto # protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto
@@ -126,9 +131,10 @@ def camel_to_snake(name: str) -> str:
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
def force_str(force: bool) -> str: def _encode_call(func: str, *args: str, force: bool = False) -> str:
"""Convert a boolean force value to string format for C++ code.""" """Emit one ProtoEncode call; every helper takes the cursor and returns it advanced."""
return str(force).lower() suffix = "_force" if force else ""
return f"pos = ProtoEncode::{func}{suffix}({', '.join(('pos', *args))});"
class TypeInfo(ABC): class TypeInfo(ABC):
@@ -223,55 +229,39 @@ class TypeInfo(ABC):
def class_member(self) -> str: def class_member(self) -> str:
return f"{self.cpp_type} {self.field_name}{{{self.default_value}}};" return f"{self.cpp_type} {self.field_name}{{{self.default_value}}};"
@property def decode_case(self, body: str) -> str:
def decode_varint_content(self) -> str: """Emit one decode_field() case, keyed on the field's wire tag."""
content = self.decode_varint return f"case proto_tag({self.number}, {self.wire_type.cpp_name}):\n" + indent(
if content is None: f"{body}\nbreak;"
return None )
return f"case {self.number}: this->{self.field_name} = {content}; break;"
decode_varint = None # Expression that reads this field from `value`; None when the type is never decoded.
decode_expr: str | None = None
def _decode_store(self, expr: str) -> str:
return f"this->{self.field_name} = {expr};"
@property @property
def decode_length_content(self) -> str: def decode_content(self) -> str | None:
content = self.decode_length """The decode_field() case for this field, or None when it is never decoded."""
if content is None: expr = self.decode_expr
return None return None if expr is None else self.decode_case(self._decode_store(expr))
return f"case {self.number}: this->{self.field_name} = {content}; break;"
decode_length = None
@property
def decode_32bit_content(self) -> str:
content = self.decode_32bit
if content is None:
return None
return f"case {self.number}: this->{self.field_name} = {content}; break;"
decode_32bit = None
@property
def decode_64bit_content(self) -> str:
content = self.decode_64bit
if content is None:
return None
return f"case {self.number}: this->{self.field_name} = {content}; break;"
decode_64bit = None
# Mapping from encode_func to raw encode expression template. # Mapping from encode_func to raw encode expression template.
# When a forced field has a single-byte tag, the code generator emits # When a forced field has a single-byte tag, the code generator emits
# write_raw_byte(tag) + raw encode instead of the full encode_* method, # write_raw_byte(tag) + raw encode instead of the full encode_* method,
# eliminating the zero-check branch and encode_field_raw indirection. # eliminating the zero-check branch and encode_field_raw indirection.
# {value} is replaced with the actual field expression. # {value} is replaced with the actual field expression.
RAW_ENCODE_MAP: dict[str, str] = { RAW_ENCODE_MAP: dict[str, tuple[str, str]] = {
"encode_uint32": "ProtoEncode::encode_varint_raw(pos, {value});", "encode_uint32": ("encode_varint_raw", "{value}"),
"encode_uint64": "ProtoEncode::encode_varint_raw_64(pos, {value});", "encode_uint64": ("encode_varint_raw_64", "{value}"),
"encode_sint32": "ProtoEncode::encode_varint_raw_short(pos, encode_zigzag32({value}));", "encode_sint32": ("encode_varint_raw_short", "encode_zigzag32({value})"),
"encode_sint64": "ProtoEncode::encode_varint_raw_64(pos, encode_zigzag64({value}));", "encode_sint64": ("encode_varint_raw_64", "encode_zigzag64({value})"),
"encode_int64": "ProtoEncode::encode_varint_raw_64(pos, static_cast<uint64_t>({value}));", "encode_int64": ("encode_varint_raw_64", "static_cast<uint64_t>({value})"),
"encode_bool": "ProtoEncode::write_raw_byte(pos, {value} ? 0x01 : 0x00);", "encode_bool": ("write_raw_byte", "{value} ? 0x01 : 0x00"),
} }
# Fixed32 value expression for the shared tag+fixed32 writer; None for other wire types
fixed32_value_template: str | None = None
def _encode_with_precomputed_tag(self, value_expr: str) -> str | None: def _encode_with_precomputed_tag(self, value_expr: str) -> str | None:
"""Try to emit a precomputed-tag encode for a field. """Try to emit a precomputed-tag encode for a field.
@@ -288,12 +278,17 @@ class TypeInfo(ABC):
return None return None
max_val = self.max_value max_val = self.max_value
# Only use RAW_ENCODE_MAP for forced fields or fields with max_value # Only use RAW_ENCODE_MAP for forced fields or fields with max_value
raw_expr = None raw = None
if self.force or max_val is not None: if self.force or max_val is not None:
raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func) raw = self.RAW_ENCODE_MAP.get(self.encode_func)
if raw_expr is None: if raw is None:
return None return None
body = f"ProtoEncode::write_raw_byte(pos, {tag});\n{raw_expr.format(value=value_expr)}" func, arg = raw
body = (
_encode_call("write_raw_byte", str(tag))
+ "\n"
+ _encode_call(func, arg.format(value=value_expr))
)
if self.force: if self.force:
return body return body
# Non-forced with max_value: inline zero-check + raw encode # Non-forced with max_value: inline zero-check + raw encode
@@ -314,23 +309,44 @@ class TypeInfo(ABC):
return None return None
# When max_len < 128, length varint is always 1 byte # When max_len < 128, length varint is always 1 byte
len_encode = ( len_encode = (
f"ProtoEncode::write_raw_byte(pos, static_cast<uint8_t>({len_expr}));" _encode_call("write_raw_byte", f"static_cast<uint8_t>({len_expr})")
if max_len is not None and max_len < 128 if max_len is not None and max_len < 128
else f"ProtoEncode::encode_varint_raw(pos, {len_expr});" else _encode_call("encode_varint_raw", len_expr)
) )
return "\n".join(
(
_encode_call("write_raw_byte", str(tag)),
len_encode,
_encode_call("encode_raw", data_expr, len_expr),
)
)
def _encode_fixed32_with_precomputed_tag(self, value: str) -> str | None:
"""Single-byte tag fixed32 write, or None for other types and multi-byte tags."""
tag = self.calculate_tag()
if self.fixed32_value_template is None or tag >= 128:
return None
value_expr = self.fixed32_value_template.format(value=value)
if self.force:
return _encode_call("write_tag_and_fixed32", str(tag), value_expr)
return ( return (
f"ProtoEncode::write_raw_byte(pos, {tag});\n" f"if (uint32_t raw = {value_expr}; raw != 0) [[likely]] {{\n"
f"{len_encode}\n" f" {_encode_call('write_tag_and_fixed32', str(tag), 'raw')}\n"
f"ProtoEncode::encode_raw(pos, {data_expr}, {len_expr});" "}"
) )
@property @property
def encode_content(self) -> str: def encode_content(self) -> str:
if result := self._encode_with_precomputed_tag(f"this->{self.field_name}"): value = f"this->{self.field_name}"
if result := self._encode_with_precomputed_tag(value):
return result return result
if self.force: if result := self._encode_fixed32_with_precomputed_tag(value):
return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name}, true);" return result
return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name});" return _encode_call(self.encode_func, str(self.number), value, force=self.force)
def encode_element(self, number: int, element: str) -> str:
"""Encode one element of a repeated field; elements are always written."""
return _encode_call(self.encode_func, str(number), element, force=True)
encode_func = None encode_func = None
@@ -605,7 +621,6 @@ class DoubleType(FixedSizeTypeMixin, TypeInfo):
# Unsupported but defined for completeness # Unsupported but defined for completeness
cpp_type = "double" cpp_type = "double"
default_value = "0.0" default_value = "0.0"
decode_64bit = "value.as_double()"
encode_func = "encode_double" encode_func = "encode_double"
wire_type = WireType.FIXED64 # Uses wire type 1 according to protobuf spec wire_type = WireType.FIXED64 # Uses wire type 1 according to protobuf spec
@@ -631,10 +646,12 @@ class DoubleType(FixedSizeTypeMixin, TypeInfo):
class FloatType(FixedSizeTypeMixin, TypeInfo): class FloatType(FixedSizeTypeMixin, TypeInfo):
cpp_type = "float" cpp_type = "float"
default_value = "0.0f" default_value = "0.0f"
decode_32bit = "value.as_float()" decode_expr = "value.as_float()"
encode_func = "encode_float" encode_func = "encode_float"
wire_type = WireType.FIXED32 # Uses wire type 5 wire_type = WireType.FIXED32 # Uses wire type 5
fixed32_value_template = "float_to_raw({value})"
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
o = f'snprintf(buffer, sizeof(buffer), "%g", {name});\n' o = f'snprintf(buffer, sizeof(buffer), "%g", {name});\n'
o += "out.append(buffer);" o += "out.append(buffer);"
@@ -658,7 +675,7 @@ class Int64Type(VarintTypeMixin, TypeInfo):
cpp_type = "int64_t" cpp_type = "int64_t"
_varint_max_bits = 64 _varint_max_bits = 64
default_value = "0" default_value = "0"
decode_varint = "static_cast<int64_t>(value)" decode_expr = "static_cast<int64_t>(value.as_varint())"
encode_func = "encode_int64" encode_func = "encode_int64"
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -679,7 +696,7 @@ class UInt64Type(VarintTypeMixin, TypeInfo):
cpp_type = "uint64_t" cpp_type = "uint64_t"
_varint_max_bits = 64 _varint_max_bits = 64
default_value = "0" default_value = "0"
decode_varint = "value" decode_expr = "value.as_varint()"
encode_func = "encode_uint64" encode_func = "encode_uint64"
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -697,11 +714,11 @@ class UInt64Type(VarintTypeMixin, TypeInfo):
return self._get_simple_size_calculation(name, force, "uint64") return self._get_simple_size_calculation(name, force, "uint64")
@property @property
def RAW_ENCODE_MAP(self) -> dict[str, str]: # noqa: N802 def RAW_ENCODE_MAP(self) -> dict[str, tuple[str, str]]: # noqa: N802
if self.mac_address: if self.mac_address:
return { return {
**TypeInfo.RAW_ENCODE_MAP, **TypeInfo.RAW_ENCODE_MAP,
"encode_uint64": "ProtoEncode::encode_varint_raw_48bit(pos, {value});", "encode_uint64": ("encode_varint_raw_48bit", "{value}"),
} }
return TypeInfo.RAW_ENCODE_MAP return TypeInfo.RAW_ENCODE_MAP
@@ -714,7 +731,7 @@ class Int32Type(VarintTypeMixin, TypeInfo):
cpp_type = "int32_t" cpp_type = "int32_t"
_varint_max_bits = 64 # int32 is sign-extended to 64 bits in protobuf _varint_max_bits = 64 # int32 is sign-extended to 64 bits in protobuf
default_value = "0" default_value = "0"
decode_varint = "static_cast<int32_t>(value)" decode_expr = "static_cast<int32_t>(value.as_varint())"
encode_func = "encode_int32" encode_func = "encode_int32"
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -734,7 +751,6 @@ class Int32Type(VarintTypeMixin, TypeInfo):
class Fixed64Type(FixedSizeTypeMixin, TypeInfo): class Fixed64Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "uint64_t" cpp_type = "uint64_t"
default_value = "0" default_value = "0"
decode_64bit = "value.as_fixed64()"
encode_func = "encode_fixed64" encode_func = "encode_fixed64"
wire_type = WireType.FIXED64 # Uses wire type 1 wire_type = WireType.FIXED64 # Uses wire type 1
@@ -760,7 +776,7 @@ class Fixed64Type(FixedSizeTypeMixin, TypeInfo):
class Fixed32Type(FixedSizeTypeMixin, TypeInfo): class Fixed32Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "uint32_t" cpp_type = "uint32_t"
default_value = "0" default_value = "0"
decode_32bit = "value.as_fixed32()" decode_expr = "value.as_fixed32()"
encode_func = "encode_fixed32" encode_func = "encode_fixed32"
wire_type = WireType.FIXED32 # Uses wire type 5 wire_type = WireType.FIXED32 # Uses wire type 5
@@ -769,15 +785,7 @@ class Fixed32Type(FixedSizeTypeMixin, TypeInfo):
o += "out.append(buffer);" o += "out.append(buffer);"
return o return o
@property fixed32_value_template = "{value}"
def encode_content(self) -> str:
tag = self.calculate_tag()
if self.force and tag < 128:
# Emit combined tag+value write: precomputed tag + direct memcpy
return f"ProtoEncode::write_tag_and_fixed32(pos, {tag}, this->{self.field_name});"
if self.force:
return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name}, true);"
return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name});"
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() field_id_size = self.calculate_field_id_size()
@@ -797,7 +805,7 @@ class BoolType(VarintTypeMixin, TypeInfo):
_varint_max_bits = 1 _varint_max_bits = 1
cpp_type = "bool" cpp_type = "bool"
default_value = "false" default_value = "false"
decode_varint = "value != 0" decode_expr = "value.as_bool()"
encode_func = "encode_bool" encode_func = "encode_bool"
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -817,7 +825,7 @@ class StringType(TypeInfo):
default_value = "" default_value = ""
reference_type = "std::string &" reference_type = "std::string &"
const_reference_type = "const std::string &" const_reference_type = "const std::string &"
decode_length = "value.as_string()" decode_expr = "value.as_string()"
encode_func = "encode_string" encode_func = "encode_string"
wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2 wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2
@@ -851,9 +859,12 @@ class StringType(TypeInfo):
f"this->{self.field_name}_ref_.size()", f"this->{self.field_name}_ref_.size()",
): ):
return result return result
if self.force: return _encode_call(
return f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name}_ref_, true);" "encode_string",
return f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name}_ref_);" str(self.number),
f"this->{self.field_name}_ref_",
force=self.force,
)
def dump(self, name): def dump(self, name):
# If name is 'it', this is a repeated field element - always use string # If name is 'it', this is a repeated field element - always use string
@@ -929,6 +940,9 @@ class MessageType(TypeInfo):
def can_use_dump_field(cls) -> bool: def can_use_dump_field(cls) -> bool:
return False return False
def encode_element(self, number: int, element: str) -> str:
return _encode_call("encode_sub_message", "buffer", str(number), element)
@property @property
def cpp_type(self) -> str: def cpp_type(self) -> str:
return self._field.type_name[1:] return self._field.type_name[1:]
@@ -951,15 +965,9 @@ class MessageType(TypeInfo):
@property @property
def encode_content(self) -> str: def encode_content(self) -> str:
# Sub-message encoding needs buffer for backpatch/sync # Sub-message encoding needs buffer for backpatch/sync
return f"ProtoEncode::{self.encode_func}(pos, buffer, {self.number}, this->{self.field_name});" return _encode_call(
self.encode_func, "buffer", str(self.number), f"this->{self.field_name}"
@property )
def decode_length(self) -> str:
# Override to return None for message types because we can't use template-based
# decoding when the specific message type isn't known at compile time.
# Instead, we use the non-template decode_to_message() method which allows
# runtime polymorphism through virtual function calls.
return None
@property @property
def public_content(self) -> list[str]: def public_content(self) -> list[str]:
@@ -976,19 +984,14 @@ class MessageType(TypeInfo):
) )
@property @property
def decode_length_content(self) -> str: def decode_content(self) -> str:
# Custom decode that doesn't use templates body = f"value.decode_to_message(this->{self.field_name});"
if self._track_presence: if self._track_presence:
# decode_to_message() cannot report failure, so setting the flag # decode_to_message() cannot report failure, so setting the flag
# afterwards only documents intent; a status-returning decode could # afterwards only documents intent; a status-returning decode could
# gate it for real without touching callers. # gate it for real without touching callers.
return ( body += f"\nthis->has_{self.name} = true;"
f"case {self.number}:\n" return self.decode_case(body)
f" value.decode_to_message(this->{self.field_name});\n"
f" this->has_{self.name} = true;\n"
f" break;"
)
return f"case {self.number}: value.decode_to_message(this->{self.field_name}); break;"
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
return f"{name}.dump_to(out);" return f"{name}.dump_to(out);"
@@ -1027,7 +1030,7 @@ class BytesType(TypeInfo):
reference_type = "std::string &" reference_type = "std::string &"
const_reference_type = "const std::string &" const_reference_type = "const std::string &"
encode_func = "encode_bytes" encode_func = "encode_bytes"
decode_length = "value.as_string()" decode_expr = "value.as_string()"
wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2 wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2
@property @property
@@ -1058,9 +1061,13 @@ class BytesType(TypeInfo):
f"this->{self.field_name}_ptr_", f"this->{self.field_name}_len_" f"this->{self.field_name}_ptr_", f"this->{self.field_name}_len_"
): ):
return result return result
if self.force: return _encode_call(
return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_, true);" "encode_bytes",
return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);" str(self.number),
f"this->{self.field_name}_ptr_",
f"this->{self.field_name}_len_",
force=self.force,
)
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
ptr_dump = f"format_hex_pretty(this->{self.field_name}_ptr_, this->{self.field_name}_len_)" ptr_dump = f"format_hex_pretty(this->{self.field_name}_ptr_, this->{self.field_name}_len_)"
@@ -1133,11 +1140,6 @@ class PointerToBufferTypeBase(TypeInfo):
super().__init__(field) super().__init__(field)
self.array_size = 0 self.array_size = 0
@property
def decode_length(self) -> str | None:
# This is handled in decode_length_content
return None
@property @property
def wire_type(self) -> WireType: def wire_type(self) -> WireType:
"""Get the wire type for this field.""" """Get the wire type for this field."""
@@ -1170,17 +1172,20 @@ class PointerToBytesBufferType(PointerToBufferTypeBase):
f"this->{self.field_name}", f"this->{self.field_name}_len" f"this->{self.field_name}", f"this->{self.field_name}_len"
): ):
return result return result
if self.force: return _encode_call(
return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" "encode_bytes",
return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len);" str(self.number),
f"this->{self.field_name}",
f"this->{self.field_name}_len",
force=self.force,
)
@property @property
def decode_length_content(self) -> str | None: def decode_content(self) -> str:
return f"""case {self.number}: {{ return self.decode_case(
this->{self.field_name} = value.data(); f"this->{self.field_name} = value.data();\n"
this->{self.field_name}_len = value.size(); f"this->{self.field_name}_len = value.size();",
break; )
}}"""
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
return ( return (
@@ -1224,24 +1229,26 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
if max_len is not None and max_len < 128 and self.force: if max_len is not None and max_len < 128 and self.force:
tag = self.calculate_tag() tag = self.calculate_tag()
if tag < 128: if tag < 128:
return f"ProtoEncode::encode_short_string_force(pos, {tag}, this->{self.field_name});" return _encode_call(
"encode_short_string_force", str(tag), f"this->{self.field_name}"
)
if result := self._encode_bytes_with_precomputed_tag( if result := self._encode_bytes_with_precomputed_tag(
f"this->{self.field_name}.c_str()", f"this->{self.field_name}.c_str()",
f"this->{self.field_name}.size()", f"this->{self.field_name}.size()",
): ):
return result return result
if self.force: return _encode_call(
return f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name}, true);" "encode_string",
return ( str(self.number),
f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name});" f"this->{self.field_name}",
force=self.force,
) )
@property @property
def decode_length_content(self) -> str | None: def decode_content(self) -> str:
return f"""case {self.number}: {{ return self.decode_case(
this->{self.field_name} = StringRef(reinterpret_cast<const char *>(value.data()), value.size()); f"this->{self.field_name} = StringRef(value.data(), value.size());",
break; )
}}"""
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
# Not used since we use dump_field, but required by abstract base class # Not used since we use dump_field, but required by abstract base class
@@ -1310,14 +1317,13 @@ class PackedBufferTypeInfo(TypeInfo):
] ]
@property @property
def decode_length_content(self) -> str: def decode_content(self) -> str:
"""Store pointer to buffer and calculate count of packed varints.""" """Store pointer to buffer and calculate count of packed varints."""
return f"""case {self.number}: {{ return self.decode_case(
this->{self.field_name}_data_ = value.data(); f"this->{self.field_name}_data_ = value.data();\n"
this->{self.field_name}_length_ = value.size(); f"this->{self.field_name}_length_ = value.size();\n"
this->{self.field_name}_count_ = count_packed_varints(value.data(), value.size()); f"this->{self.field_name}_count_ = count_packed_varints(value.data(), value.size());",
break; )
}}"""
@property @property
def encode_content(self) -> str: def encode_content(self) -> str:
@@ -1402,17 +1408,11 @@ class FixedArrayBytesType(TypeInfo):
] ]
@property @property
def decode_length_content(self) -> str: def decode_content(self) -> str:
o = f"case {self.number}: {{\n" return self.decode_case(
o += " const std::string &data_str = value.as_string();\n" f"this->{self.field_name}_len = std::min<size_t>(value.size(), {self.array_size});\n"
o += f" this->{self.field_name}_len = data_str.size();\n" f"memcpy(this->{self.field_name}, value.data(), this->{self.field_name}_len);",
o += f" if (this->{self.field_name}_len > {self.array_size}) {{\n" )
o += f" this->{self.field_name}_len = {self.array_size};\n"
o += " }\n"
o += f" memcpy(this->{self.field_name}, data_str.data(), this->{self.field_name}_len);\n"
o += " break;\n"
o += "}"
return o
@property @property
def encode_content(self) -> str: def encode_content(self) -> str:
@@ -1421,9 +1421,13 @@ class FixedArrayBytesType(TypeInfo):
f"this->{self.field_name}", f"this->{self.field_name}_len", max_len=max_len f"this->{self.field_name}", f"this->{self.field_name}_len", max_len=max_len
): ):
return result return result
if self.force: return _encode_call(
return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" "encode_bytes",
return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len);" str(self.number),
f"this->{self.field_name}",
f"this->{self.field_name}_len",
force=self.force,
)
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
return f"out.append(format_hex_pretty({name}, {name}_len));" return f"out.append(format_hex_pretty({name}, {name}_len));"
@@ -1471,7 +1475,7 @@ class UInt32Type(VarintTypeMixin, TypeInfo):
cpp_type = "uint32_t" cpp_type = "uint32_t"
_varint_max_bits = 32 _varint_max_bits = 32
default_value = "0" default_value = "0"
decode_varint = "value" decode_expr = "value.as_varint()"
encode_func = "encode_uint32" encode_func = "encode_uint32"
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -1494,13 +1498,21 @@ class UInt32Type(VarintTypeMixin, TypeInfo):
class EnumType(VarintTypeMixin, TypeInfo): class EnumType(VarintTypeMixin, TypeInfo):
_varint_max_bits = 32 _varint_max_bits = 32
def encode_element(self, number: int, element: str) -> str:
return _encode_call(
self.encode_func,
str(number),
f"static_cast<uint32_t>({element})",
force=True,
)
@property @property
def cpp_type(self) -> str: def cpp_type(self) -> str:
return f"enums::{self._field.type_name[1:]}" return f"enums::{self._field.type_name[1:]}"
@property @property
def decode_varint(self) -> str: def decode_expr(self) -> str:
return f"static_cast<{self.cpp_type}>(value)" return f"static_cast<{self.cpp_type}>(value.as_varint())"
default_value = "" default_value = ""
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -1520,9 +1532,9 @@ class EnumType(VarintTypeMixin, TypeInfo):
@property @property
def encode_content(self) -> str: def encode_content(self) -> str:
value_expr = f"static_cast<uint32_t>(this->{self.field_name})" value_expr = f"static_cast<uint32_t>(this->{self.field_name})"
if self.force: return _encode_call(
return f"ProtoEncode::{self.encode_func}(pos, {self.number}, {value_expr}, true);" self.encode_func, str(self.number), value_expr, force=self.force
return f"ProtoEncode::{self.encode_func}(pos, {self.number}, {value_expr});" )
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
return f"out.append_p(proto_enum_to_string<{self.cpp_type}>({name}));" return f"out.append_p(proto_enum_to_string<{self.cpp_type}>({name}));"
@@ -1547,7 +1559,7 @@ class EnumType(VarintTypeMixin, TypeInfo):
class SFixed32Type(FixedSizeTypeMixin, TypeInfo): class SFixed32Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "int32_t" cpp_type = "int32_t"
default_value = "0" default_value = "0"
decode_32bit = "value.as_sfixed32()" decode_expr = "value.as_sfixed32()"
encode_func = "encode_sfixed32" encode_func = "encode_sfixed32"
wire_type = WireType.FIXED32 # Uses wire type 5 wire_type = WireType.FIXED32 # Uses wire type 5
@@ -1573,7 +1585,6 @@ class SFixed32Type(FixedSizeTypeMixin, TypeInfo):
class SFixed64Type(FixedSizeTypeMixin, TypeInfo): class SFixed64Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "int64_t" cpp_type = "int64_t"
default_value = "0" default_value = "0"
decode_64bit = "value.as_sfixed64()"
encode_func = "encode_sfixed64" encode_func = "encode_sfixed64"
wire_type = WireType.FIXED64 # Uses wire type 1 wire_type = WireType.FIXED64 # Uses wire type 1
@@ -1600,7 +1611,7 @@ class SInt32Type(VarintTypeMixin, TypeInfo):
cpp_type = "int32_t" cpp_type = "int32_t"
_varint_max_bits = 32 # zigzag encoding keeps it 32-bit _varint_max_bits = 32 # zigzag encoding keeps it 32-bit
default_value = "0" default_value = "0"
decode_varint = "decode_zigzag32(static_cast<uint32_t>(value))" decode_expr = "decode_zigzag32(static_cast<uint32_t>(value.as_varint()))"
encode_func = "encode_sint32" encode_func = "encode_sint32"
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -1621,7 +1632,7 @@ class SInt64Type(VarintTypeMixin, TypeInfo):
cpp_type = "int64_t" cpp_type = "int64_t"
_varint_max_bits = 64 _varint_max_bits = 64
default_value = "0" default_value = "0"
decode_varint = "decode_zigzag64(value)" decode_expr = "decode_zigzag64(value.as_varint())"
encode_func = "encode_sint64" encode_func = "encode_sint64"
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -1701,9 +1712,9 @@ def _generate_inline_encode_block(
lines = [] lines = []
lines.append(f"auto &sub_msg = {element};") lines.append(f"auto &sub_msg = {element};")
lines.append(f"ProtoEncode::write_raw_byte(pos, {tag});") lines.append(_encode_call("write_raw_byte", str(tag)))
lines.append("uint8_t *len_pos = pos;") lines.append("uint8_t *len_pos = pos;")
lines.append("ProtoEncode::reserve_byte(pos);") lines.append(_encode_call("reserve_byte"))
# Generate inline field encoding for each sub-message field # Generate inline field encoding for each sub-message field
for field in sub_desc.field: for field in sub_desc.field:
@@ -1774,18 +1785,11 @@ class FixedArrayRepeatedType(TypeInfo):
def _encode_element(self, element: str) -> str: def _encode_element(self, element: str) -> str:
"""Helper to generate encode statement for a single element.""" """Helper to generate encode statement for a single element."""
if isinstance(self._ti, EnumType): if isinstance(self._ti, MessageType) and _is_inline_encode(self._ti.cpp_type):
return f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, static_cast<uint32_t>({element}), true);" return _generate_inline_encode_block(
# Repeated message elements use encode_sub_message (force=true is default) self.number, self._ti.cpp_type, element
if isinstance(self._ti, MessageType): )
if _is_inline_encode(self._ti.cpp_type): return self._ti.encode_element(self.number, element)
return _generate_inline_encode_block(
self.number, self._ti.cpp_type, element
)
return f"ProtoEncode::encode_sub_message(pos, buffer, {self.number}, {element});"
return (
f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, {element}, true);"
)
@property @property
def cpp_type(self) -> str: def cpp_type(self) -> str:
@@ -2079,55 +2083,23 @@ class RepeatedTypeInfo(TypeInfo):
return self._ti.wire_type return self._ti.wire_type
@property @property
def decode_varint_content(self) -> str: def decode_expr(self) -> str | None:
# Pointer fields don't support decoding return self._ti.decode_expr
if self._use_pointer:
return None def _decode_store(self, expr: str) -> str:
content = self._ti.decode_varint return f"this->{self.field_name}.push_back({expr});"
if content is None:
return None
return (
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
)
@property @property
def decode_length_content(self) -> str: def decode_content(self) -> str | None:
# Pointer fields don't support decoding # Pointer fields don't support decoding
if self._use_pointer: if self._use_pointer:
return None return None
content = self._ti.decode_length if isinstance(self._ti, MessageType):
if content is None and isinstance(self._ti, MessageType): return self.decode_case(
# Special handling for non-template message decoding f"this->{self.field_name}.emplace_back();\n"
return f"case {self.number}: this->{self.field_name}.emplace_back(); value.decode_to_message(this->{self.field_name}.back()); break;" f"value.decode_to_message(this->{self.field_name}.back());"
if content is None: )
return None return super().decode_content
return (
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
)
@property
def decode_32bit_content(self) -> str:
# Pointer fields don't support decoding
if self._use_pointer:
return None
content = self._ti.decode_32bit
if content is None:
return None
return (
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
)
@property
def decode_64bit_content(self) -> str:
# Pointer fields don't support decoding
if self._use_pointer:
return None
content = self._ti.decode_64bit
if content is None:
return None
return (
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
)
@property @property
def _ti_is_bool(self) -> bool: def _ti_is_bool(self) -> bool:
@@ -2135,15 +2107,7 @@ class RepeatedTypeInfo(TypeInfo):
return isinstance(self._ti, BoolType) return isinstance(self._ti, BoolType)
def _encode_element_call(self, element: str) -> str: def _encode_element_call(self, element: str) -> str:
"""Helper to generate encode call for a single element.""" return self._ti.encode_element(self.number, element)
if isinstance(self._ti, EnumType):
return f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, static_cast<uint32_t>({element}), true);"
# Repeated message elements use encode_sub_message (force=true is default)
if isinstance(self._ti, MessageType):
return f"ProtoEncode::encode_sub_message(pos, buffer, {self.number}, {element});"
return (
f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, {element}, true);"
)
@property @property
def encode_content(self) -> str: def encode_content(self) -> str:
@@ -2152,7 +2116,7 @@ class RepeatedTypeInfo(TypeInfo):
# Special handling for const char* elements (when container_no_template contains "const char") # Special handling for const char* elements (when container_no_template contains "const char")
if "const char" in self._container_no_template: if "const char" in self._container_no_template:
o = f"for (const char *it : *this->{self.field_name}) {{\n" o = f"for (const char *it : *this->{self.field_name}) {{\n"
o += f" ProtoEncode::{self._ti.encode_func}(pos, {self.number}, it, strlen(it), true);\n" o += f" {_encode_call(self._ti.encode_func, str(self.number), 'it', 'strlen(it)', force=True)}\n"
else: else:
o = f"for (const auto &it : *this->{self.field_name}) {{\n" o = f"for (const auto &it : *this->{self.field_name}) {{\n"
o += f" {self._encode_element_call('it')}\n" o += f" {self._encode_element_call('it')}\n"
@@ -2538,10 +2502,7 @@ def build_message_type(
) -> tuple[str, str, str]: ) -> tuple[str, str, str]:
public_content: list[str] = [] public_content: list[str] = []
protected_content: list[str] = [] protected_content: list[str] = []
decode_varint: list[str] = [] decode: list[str] = []
decode_length: list[str] = []
decode_32bit: list[str] = []
decode_64bit: list[str] = []
encode: list[str] = [] encode: list[str] = []
dump: list[str] = [] dump: list[str] = []
size_calc: list[str] = [] size_calc: list[str] = []
@@ -2670,22 +2631,8 @@ def build_message_type(
if field.options.HasExtension(pb.field_ifdef): if field.options.HasExtension(pb.field_ifdef):
field_ifdef = field.options.Extensions[pb.field_ifdef] field_ifdef = field.options.Extensions[pb.field_ifdef]
if ti.decode_varint_content: if case := ti.decode_content:
decode_varint.extend( decode.extend(wrap_with_ifdef(case, field_ifdef))
wrap_with_ifdef(ti.decode_varint_content, field_ifdef)
)
if ti.decode_length_content:
decode_length.extend(
wrap_with_ifdef(ti.decode_length_content, field_ifdef)
)
if ti.decode_32bit_content:
decode_32bit.extend(
wrap_with_ifdef(ti.decode_32bit_content, field_ifdef)
)
if ti.decode_64bit_content:
decode_64bit.extend(
wrap_with_ifdef(ti.decode_64bit_content, field_ifdef)
)
if ti.dump_content: if ti.dump_content:
# Check for field_ifdef option for dump as well # Check for field_ifdef option for dump as well
field_ifdef = None field_ifdef = None
@@ -2695,49 +2642,15 @@ def build_message_type(
dump.extend(wrap_with_ifdef(ti.dump_content, field_ifdef)) dump.extend(wrap_with_ifdef(ti.dump_content, field_ifdef))
cpp = "" cpp = ""
if decode_varint: if decode:
o = f"bool {desc.name}::decode_varint(uint32_t field_id, proto_varint_value_t value) {{\n" o = f"void {desc.name}::decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {{\n"
o += " switch (field_id) {\n" o += " const ProtoFieldValue value(data, scalar);\n"
o += indent("\n".join(decode_varint), " ") + "\n" o += " switch (tag) {\n"
o += " default: return false;\n" o += indent("\n".join(decode), " ") + "\n"
o += " }\n" o += " }\n"
o += " return true;\n"
o += "}\n" o += "}\n"
cpp += o cpp += o
prot = "bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;" prot = "void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;"
protected_content.insert(0, prot)
if decode_length:
o = f"bool {desc.name}::decode_length(uint32_t field_id, ProtoLengthDelimited value) {{\n"
o += " switch (field_id) {\n"
o += indent("\n".join(decode_length), " ") + "\n"
o += " default: return false;\n"
o += " }\n"
o += " return true;\n"
o += "}\n"
cpp += o
prot = "bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;"
protected_content.insert(0, prot)
if decode_32bit:
o = f"bool {desc.name}::decode_32bit(uint32_t field_id, Proto32Bit value) {{\n"
o += " switch (field_id) {\n"
o += indent("\n".join(decode_32bit), " ") + "\n"
o += " default: return false;\n"
o += " }\n"
o += " return true;\n"
o += "}\n"
cpp += o
prot = "bool decode_32bit(uint32_t field_id, Proto32Bit value) override;"
protected_content.insert(0, prot)
if decode_64bit:
o = f"bool {desc.name}::decode_64bit(uint32_t field_id, Proto64Bit value) {{\n"
o += " switch (field_id) {\n"
o += indent("\n".join(decode_64bit), " ") + "\n"
o += " default: return false;\n"
o += " }\n"
o += " return true;\n"
o += "}\n"
cpp += o
prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;"
protected_content.insert(0, prot) protected_content.insert(0, prot)
# Generate custom decode() override for messages with FixedVector fields # Generate custom decode() override for messages with FixedVector fields
@@ -2784,28 +2697,36 @@ def build_message_type(
) )
for line in encode for line in encode
] ]
o = f"{speed_attr}uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n" o = f"{speed_attr}uint8_t *{desc.name}::encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) {{\n"
o += f" const auto &msg = *static_cast<const {desc.name} *>(self);\n"
o += " uint8_t *__restrict__ pos = buffer.get_pos();\n" o += " uint8_t *__restrict__ pos = buffer.get_pos();\n"
o += indent("\n".join(encode_debug)) + "\n" o += indent("\n".join(encode_debug)).replace("this->", "msg.") + "\n"
o += " return pos;\n" o += " return pos;\n"
o += "}\n" o += "}\n"
cpp += o cpp += o
prot = ( public_content.append(
"uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;" "static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);"
)
public_content.append(
"uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {\n"
" return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);\n"
"}"
) )
public_content.append(prot)
# If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used # If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used
# Add calculate_size method only if this message needs encoding and has fields # Add calculate_size method only if this message needs encoding and has fields
if needs_encode and size_calc and not is_inline_only: if needs_encode and size_calc and not is_inline_only:
o = f"{speed_attr}uint32_t {desc.name}::calculate_size() const {{\n" o = f"{speed_attr}uint32_t {desc.name}::calc_size_msg(const void *self) {{\n"
o += f" const auto &msg = *static_cast<const {desc.name} *>(self);\n"
o += " uint32_t size = 0;\n" o += " uint32_t size = 0;\n"
o += indent("\n".join(size_calc)) + "\n" o += indent("\n".join(size_calc)).replace("this->", "msg.") + "\n"
o += " return size;\n" o += " return size;\n"
o += "}\n" o += "}\n"
cpp += o cpp += o
prot = "uint32_t calculate_size() const;" public_content.append("static uint32_t calc_size_msg(const void *self);")
public_content.append(prot) public_content.append(
"uint32_t calculate_size() const { return calc_size_msg(this); }"
)
# If no fields to calculate size for or message doesn't need encoding, the default implementation in ProtoMessage will be used # If no fields to calculate size for or message doesn't need encoding, the default implementation in ProtoMessage will be used
# dump_to method declaration in header # dump_to method declaration in header
@@ -249,7 +249,7 @@ static APIBuffer build_infrared_rf_transmit_wire() {
std::memcpy(bytes + len, packed, packed_len); std::memcpy(bytes + len, packed, packed_len);
len += packed_len; len += packed_len;
// field 6: modulation = 1 (non-zero so it's actually emitted and exercises // field 6: modulation = 1 (non-zero so it's actually emitted and exercises
// decode_varint for this field, matching the documented layout above). // decode_field for this field, matching the documented layout above).
put_byte(0x30); put_byte(0x30);
put_varint(1); put_varint(1);
@@ -59,7 +59,7 @@ static void verify_mac(uint64_t mac, size_t expected_bytes) {
#ifdef ESPHOME_DEBUG_API #ifdef ESPHOME_DEBUG_API
uint8_t *proto_debug_end_ = api_buf.data() + api_buf.size(); uint8_t *proto_debug_end_ = api_buf.data() + api_buf.size();
#endif #endif
ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, mac); pos = ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, mac);
size_t new_len = pos - api_buf.data(); size_t new_len = pos - api_buf.data();
EXPECT_EQ(new_len, expected_bytes) << "mac=0x" << std::hex << mac << std::dec; EXPECT_EQ(new_len, expected_bytes) << "mac=0x" << std::hex << mac << std::dec;
+1
View File
@@ -14,6 +14,7 @@ esphome:
condition: wifi.ap_active condition: wifi.ap_active
then: then:
- logger.log: "WiFi AP is active!" - logger.log: "WiFi AP is active!"
- wifi.roam
wifi: wifi:
networks: networks:
@@ -0,0 +1,43 @@
esphome:
name: api-decode-wire-types-test
host:
api:
logger:
level: DEBUG
switch:
- platform: template
name: "Wire Switch"
optimistic: true
output:
- platform: template
id: wire_dim
type: float
write_action:
- lambda: ""
light:
- platform: monochromatic
name: "Wire Light"
output: wire_dim
default_transition_length: 0s
effects:
- pulse:
name: Pulse
text:
- platform: template
name: "Wire Text"
optimistic: true
mode: text
min_length: 0
max_length: 255
number:
- platform: template
name: "Wire Number"
optimistic: true
min_value: -1000
max_value: 1000
step: 0.5
@@ -0,0 +1,11 @@
esphome:
name: api-empty-message-test
host:
api:
logger:
level: DEBUG
switch:
- platform: template
name: "Empty Message Switch"
optimistic: true
@@ -0,0 +1,58 @@
esphome:
name: api-encode-boundaries-test
# Top-level area fills DeviceInfoResponse.suggested_area (field 16, a two-byte tag)
area:
id: kitchen_area
name: Kitchen
on_boot:
- sensor.template.publish:
id: zero_then_value
state: 0.0
host:
api:
logger:
level: DEBUG
sensor:
- platform: template
name: "Zero Then Value"
id: zero_then_value
# Negative int32 takes the ten byte varint path
accuracy_decimals: -2
update_interval: never
text_sensor:
- platform: template
name: "Long Text"
id: long_text
update_interval: never
number:
- platform: template
name: "Negative Number"
optimistic: true
min_value: -1000
max_value: 1000
step: 0.5
initial_value: -123.5
select:
- platform: template
name: "Long Option Select"
optimistic: true
options:
- short
- "option-with-a-name-long-enough-that-its-length-prefix-needs-two-varint-bytes-when-the-list-entities-response-is-encoded-xxxxxxxxxx"
initial_option: short
button:
- platform: template
name: "Publish Values"
on_press:
- sensor.template.publish:
id: zero_then_value
state: 12.5
- text_sensor.template.publish:
id: long_text
state: !lambda return std::string(200, 'y');
+5 -4
View File
@@ -125,11 +125,12 @@ class RawApiClient:
await self.read_until_frame(MESSAGE_TYPE_OF[api_pb2.HelloResponse]) await self.read_until_frame(MESSAGE_TYPE_OF[api_pb2.HelloResponse])
async def send_message(self, msg: message.Message) -> None: async def send_message(self, msg: message.Message) -> None:
await self.send_raw(MESSAGE_TYPE_OF[type(msg)], msg.SerializeToString())
async def send_raw(self, msg_type: int, payload: bytes) -> None:
"""Send a frame with a hand built payload, for shapes protobuf will not serialize."""
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
await loop.sock_sendall( await loop.sock_sendall(self._sock, encode_frame(msg_type, payload))
self._sock,
encode_frame(MESSAGE_TYPE_OF[type(msg)], msg.SerializeToString()),
)
async def read_until_frame(self, msg_type: int, timeout: float = 10.0) -> None: async def read_until_frame(self, msg_type: int, timeout: float = 10.0) -> None:
"""Read until at least one frame of msg_type has been received.""" """Read until at least one frame of msg_type has been received."""
+40
View File
@@ -57,6 +57,46 @@ async def wait_for_state(
return await asyncio.wait_for(future, timeout=timeout) return await asyncio.wait_for(future, timeout=timeout)
class StateWaiter:
"""Route one state subscription to any number of predicate waits."""
def __init__(self) -> None:
self._waiters: list[
tuple[Callable[[EntityState], bool], asyncio.Future[EntityState]]
] = []
def on_state(self, state: EntityState) -> None:
for predicate, future in self._waiters:
if future.done():
continue
try:
matched = predicate(state)
except Exception as exc: # noqa: BLE001 the wait re-raises it, the callback must not die
future.set_exception(exc)
continue
if matched:
future.set_result(state)
async def expect(
self,
predicate: Callable[[EntityState], bool],
timeout: float = 5.0,
label: str | None = None,
) -> EntityState:
"""Wait for the next state matching ``predicate``; states seen before this call do not count."""
entry = (predicate, asyncio.get_running_loop().create_future())
self._waiters.append(entry)
try:
async with asyncio.timeout(timeout):
return await entry[1]
except TimeoutError:
raise TimeoutError(
f"no state matched {label or predicate} within {timeout}s"
) from None
finally:
self._waiters.remove(entry)
def find_entity[T: EntityInfo]( def find_entity[T: EntityInfo](
entities: list[EntityInfo], entities: list[EntityInfo],
object_id_substring: str, object_id_substring: str,
@@ -0,0 +1,142 @@
"""decode_field() must take fields that match their declared wire type, drop the ones that do
not, skip unknown fields, and handle two byte tags, varints and length prefixes."""
from __future__ import annotations
from collections.abc import Callable
import struct
from aioesphomeapi import (
EntityState,
LightState,
NumberState,
SwitchState,
TextState,
api_pb2,
)
import pytest
from .raw_api_client import MESSAGE_TYPE_OF, RawApiClient, encode_varint
from .state_utils import InitialStateHelper, StateWaiter, require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
SWITCH_COMMAND = MESSAGE_TYPE_OF[api_pb2.SwitchCommandRequest]
WIRE_VARINT, WIRE_LENGTH, WIRE_FIXED32 = 0, 2, 5
def tag(field: int, wire_type: int) -> bytes:
return encode_varint((field << 3) | wire_type)
@pytest.mark.asyncio
async def test_api_decode_wire_types(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
unused_tcp_port: int,
) -> None:
async with (
run_compiled(yaml_config),
api_client_connected() as client,
RawApiClient(unused_tcp_port) as raw,
):
entities, _ = await client.list_entities_services()
switch = require_entity(entities, "wire_switch")
light = require_entity(entities, "wire_light")
text = require_entity(entities, "wire_text")
number = require_entity(entities, "wire_number")
key = tag(1, WIRE_FIXED32) + struct.pack("<I", switch.key)
on, off = tag(2, WIRE_VARINT) + b"\x01", tag(2, WIRE_VARINT) + b"\x00"
switch_states: list[bool] = []
waiter = StateWaiter()
def on_state(state: EntityState) -> None:
if isinstance(state, SwitchState) and state.key == switch.key:
switch_states.append(state.state)
waiter.on_state(state)
def switch_is(value: bool) -> Callable[[EntityState], bool]:
return lambda s: (
isinstance(s, SwitchState) and s.key == switch.key and s.state is value
)
def number_is(value: float) -> Callable[[EntityState], bool]:
return lambda s: (
isinstance(s, NumberState) and s.key == number.key and s.state == value
)
initial = InitialStateHelper(entities)
client.subscribe_states(initial.on_state_wrapper(on_state))
await initial.wait_for_initial_states()
await raw.connect()
# A well formed command: fixed32 key, varint state
await raw.send_raw(SWITCH_COMMAND, key + on)
await waiter.expect(switch_is(True))
await raw.send_raw(SWITCH_COMMAND, key + off)
await waiter.expect(switch_is(False))
# The same field with the wrong wire type is dropped, and a varint key never matches an
# entity; each of these would turn the switch on if the payload were read as a varint
seen = len(switch_states)
await raw.send_raw(SWITCH_COMMAND, key + tag(2, WIRE_LENGTH) + b"\x01\x01")
await raw.send_raw(
SWITCH_COMMAND, key + tag(2, WIRE_FIXED32) + b"\x01\x00\x00\x00"
)
await raw.send_raw(
SWITCH_COMMAND, tag(1, WIRE_VARINT) + encode_varint(switch.key) + on
)
# Ordered on the raw socket itself: this frame cannot be parsed before the bad ones, so
# the only switch state since the marker must be the one it produces
await raw.send_raw(SWITCH_COMMAND, key + on)
await waiter.expect(switch_is(True), label="switch on after wrong wire types")
assert switch_states[seen:] == [True]
await raw.send_raw(SWITCH_COMMAND, key + off)
await waiter.expect(switch_is(False))
# Truncated bodies stop the decode loop without taking the connection down: a tag with its
# continuation bit set and nothing after it, a length prefix past the end of the payload,
# and a fixed32 with two of its four bytes
seen = len(switch_states)
await raw.send_raw(SWITCH_COMMAND, key + b"\x80")
await raw.send_raw(SWITCH_COMMAND, key + tag(2, WIRE_LENGTH) + b"\x7f" + b"ab")
await raw.send_raw(SWITCH_COMMAND, tag(1, WIRE_FIXED32) + b"\x01\x02")
await raw.send_raw(SWITCH_COMMAND, key + on)
await waiter.expect(switch_is(True), label="switch on after truncated frames")
assert switch_states[seen:] == [True]
await raw.send_raw(SWITCH_COMMAND, key + off)
await waiter.expect(switch_is(False))
# A negative number goes through the fixed32 float path of a normal client
client.number_command(number.key, -77.5)
await waiter.expect(number_is(-77.5))
# An unknown field ahead of the known ones is skipped; field 200 needs a two byte tag
await raw.send_raw(
SWITCH_COMMAND, tag(200, WIRE_VARINT) + encode_varint(300) + key + on
)
await waiter.expect(switch_is(True))
# Two byte tags (effect fields 18 and 19) and a two byte varint (300 ms transition)
client.light_command(
light.key, state=True, brightness=0.5, transition_length=0.3, effect="Pulse"
)
await waiter.expect(
lambda s: (
isinstance(s, LightState) and s.key == light.key and s.effect == "Pulse"
)
)
client.light_command(light.key, effect="None", state=False)
await waiter.expect(
lambda s: isinstance(s, LightState) and s.key == light.key and not s.state
)
# A string whose length prefix needs two varint bytes
long_text = "w" * 200
client.text_command(text.key, long_text)
await waiter.expect(
lambda s: (
isinstance(s, TextState) and s.key == text.key and s.state == long_text
)
)
@@ -0,0 +1,37 @@
"""Messages without fields go through the shared ProtoMessage entry points on both directions."""
from __future__ import annotations
from aioesphomeapi import api_pb2
import pytest
from .raw_api_client import MESSAGE_TYPE_OF, RawApiClient
from .types import RunCompiledFunction
@pytest.mark.asyncio
async def test_api_empty_message_roundtrip(
yaml_config: str,
run_compiled: RunCompiledFunction,
unused_tcp_port: int,
) -> None:
async with run_compiled(yaml_config), RawApiClient(unused_tcp_port) as client:
await client.connect()
# Field free request and reply on the plain send path
await client.send_message(api_pb2.PingRequest())
await client.read_until_frame(MESSAGE_TYPE_OF[api_pb2.PingResponse])
# Field free request answered by a message with fields, and a list that ends with
# the field free ListEntitiesDoneResponse through the batching path
await client.send_message(api_pb2.DeviceInfoRequest())
await client.read_until_frame(MESSAGE_TYPE_OF[api_pb2.DeviceInfoResponse])
await client.send_message(api_pb2.ListEntitiesRequest())
await client.read_until_frame(MESSAGE_TYPE_OF[api_pb2.ListEntitiesDoneResponse])
assert (
client.frame_counts[MESSAGE_TYPE_OF[api_pb2.ListEntitiesSwitchResponse]]
== 1
)
await client.send_message(api_pb2.DisconnectRequest())
await client.read_until_frame(MESSAGE_TYPE_OF[api_pb2.DisconnectResponse])
@@ -0,0 +1,78 @@
"""Encode paths at their branch boundaries: zero skipped float, fixed32 state, negative int32,
length prefixes of two varint bytes and two byte field tags."""
from __future__ import annotations
import asyncio
from aioesphomeapi import (
NumberState,
SelectInfo,
SensorInfo,
SensorState,
TextSensorState,
)
import pytest
from .state_utils import InitialStateHelper, StateWaiter, require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
LONG_OPTION = (
"option-with-a-name-long-enough-that-its-length-prefix-needs-two-varint-bytes-"
"when-the-list-entities-response-is-encoded-xxxxxxxxxx"
)
@pytest.mark.asyncio
async def test_api_encode_boundaries(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
async with run_compiled(yaml_config), api_client_connected() as client:
device_info, (entities, _) = await asyncio.gather(
client.device_info(), client.list_entities_services()
)
assert device_info.suggested_area == "Kitchen"
sensor = require_entity(entities, "zero_then_value", SensorInfo)
assert sensor.accuracy_decimals == -2
select = require_entity(entities, "long_option_select", SelectInfo)
assert len(LONG_OPTION) >= 128
assert select.options == ["short", LONG_OPTION]
text = require_entity(entities, "long_text")
number = require_entity(entities, "negative_number")
button = require_entity(entities, "publish_values")
initial = InitialStateHelper(entities)
waiter = StateWaiter()
client.subscribe_states(initial.on_state_wrapper(waiter.on_state))
await initial.wait_for_initial_states()
# A float of exactly zero is skipped on the wire and must still read as 0.0, not missing
first = initial.initial_states[sensor.key]
assert isinstance(first, SensorState)
assert first.state == 0.0 and not first.missing_state
first_number = initial.initial_states[number.key]
assert isinstance(first_number, NumberState)
assert first_number.state == -123.5
client.button_command(button.key)
await asyncio.gather(
waiter.expect(
lambda s: (
isinstance(s, SensorState)
and s.key == sensor.key
and s.state == 12.5
),
label="sensor 12.5",
),
waiter.expect(
lambda s: (
isinstance(s, TextSensorState)
and s.key == text.key
and s.state == "y" * 200
),
label="text 200 x y",
),
)
@@ -194,17 +194,17 @@ def test_superseded_device_info_fields_still_declared_in_header() -> None:
def test_superseded_device_info_fields_still_encoded_and_sized() -> None: def test_superseded_device_info_fields_still_encoded_and_sized() -> None:
"""Each superseded field must still be touched by DeviceInfoResponse's """Each superseded field must still be touched by DeviceInfoResponse's
generated encode() and calculate_size(), i.e. it is still put on the wire. generated encode_msg() and calc_size_msg(), i.e. it is still put on the wire.
""" """
encode_body = _extract_function_body(CPP_TEXT, "DeviceInfoResponse::encode") encode_body = _extract_function_body(CPP_TEXT, "DeviceInfoResponse::encode_msg")
size_body = _extract_function_body(CPP_TEXT, "DeviceInfoResponse::calculate_size") size_body = _extract_function_body(CPP_TEXT, "DeviceInfoResponse::calc_size_msg")
for field_name in SUPERSEDED_FIELDS: for field_name in SUPERSEDED_FIELDS:
assert f"this->{field_name}" in encode_body, ( assert f"msg.{field_name}" in encode_body, (
f"DeviceInfoResponse::encode() no longer references {field_name}. " f"DeviceInfoResponse::encode_msg() no longer references {field_name}. "
f"{DEPRECATED_FIELD_TRAP}" f"{DEPRECATED_FIELD_TRAP}"
) )
assert f"this->{field_name}" in size_body, ( assert f"msg.{field_name}" in size_body, (
f"DeviceInfoResponse::calculate_size() no longer references " f"DeviceInfoResponse::calc_size_msg() no longer references "
f"{field_name}. {DEPRECATED_FIELD_TRAP}" f"{field_name}. {DEPRECATED_FIELD_TRAP}"
) )
@@ -380,3 +380,13 @@ def test_api_version_minor_is_at_least_15() -> None:
"clients to see api_version >= 1.15 in HelloResponse before they will " "clients to see api_version >= 1.15 in HelloResponse before they will "
"ever request it." "ever request it."
) )
def test_generated_encode_calls_keep_the_cursor() -> None:
"""No generated ProtoEncode call may drop the returned cursor."""
dropped = [
line
for line in CPP_TEXT.splitlines()
if "ProtoEncode::" in line and "pos = ProtoEncode::" not in line
]
assert not dropped, dropped[:5]
@@ -15,9 +15,13 @@ import pytest
sys.path.insert(0, str(Path(__file__).parents[4] / "script" / "api_protobuf")) sys.path.insert(0, str(Path(__file__).parents[4] / "script" / "api_protobuf"))
import aioesphomeapi.api_options_pb2 as pb # noqa: E402
from api_protobuf import ( # noqa: E402 from api_protobuf import ( # noqa: E402
MAX_MESSAGE_ID, MAX_MESSAGE_ID,
SOURCE_CLIENT,
_make_ifdef_line, _make_ifdef_line,
build_message_type,
create_field_type_info,
get_varint64_ifdef, get_varint64_ifdef,
validate_message_id, validate_message_id,
) )
@@ -34,16 +38,26 @@ def _file_with_messages(
file_desc = descriptor_pb2.FileDescriptorProto(name="test.proto") file_desc = descriptor_pb2.FileDescriptorProto(name="test.proto")
for name, field_type, deprecated in messages: for name, field_type, deprecated in messages:
msg = file_desc.message_type.add(name=name) msg = file_desc.message_type.add(name=name)
field = msg.field.add(name="value", number=1, type=field_type) field = msg.field.add()
field.CopyFrom(_field(field_type))
field.options.deprecated = deprecated field.options.deprecated = deprecated
return file_desc return file_desc
UINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT64 UINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT64
MESSAGE = descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE
DOUBLE = descriptor_pb2.FieldDescriptorProto.TYPE_DOUBLE
INT64 = descriptor_pb2.FieldDescriptorProto.TYPE_INT64 INT64 = descriptor_pb2.FieldDescriptorProto.TYPE_INT64
SINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_SINT64 SINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_SINT64
UINT32 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT32 UINT32 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT32
INT32 = descriptor_pb2.FieldDescriptorProto.TYPE_INT32
SINT32 = descriptor_pb2.FieldDescriptorProto.TYPE_SINT32
FIXED64 = descriptor_pb2.FieldDescriptorProto.TYPE_FIXED64 FIXED64 = descriptor_pb2.FieldDescriptorProto.TYPE_FIXED64
FIXED32 = descriptor_pb2.FieldDescriptorProto.TYPE_FIXED32
FLOAT = descriptor_pb2.FieldDescriptorProto.TYPE_FLOAT
BOOL = descriptor_pb2.FieldDescriptorProto.TYPE_BOOL
STRING = descriptor_pb2.FieldDescriptorProto.TYPE_STRING
BYTES = descriptor_pb2.FieldDescriptorProto.TYPE_BYTES
def test_no_varint64_fields() -> None: def test_no_varint64_fields() -> None:
@@ -107,3 +121,172 @@ def test_message_id_at_maximum_is_accepted() -> None:
def test_message_id_above_maximum_is_rejected() -> None: def test_message_id_above_maximum_is_rejected() -> None:
with pytest.raises(ValueError, match="exceeds the plaintext"): with pytest.raises(ValueError, match="exceeds the plaintext"):
validate_message_id(MAX_MESSAGE_ID + 1, "TooBigMessage") validate_message_id(MAX_MESSAGE_ID + 1, "TooBigMessage")
def _field(
field_type: int, number: int = 1, *, force: bool = False, repeated: bool = False
) -> descriptor_pb2.FieldDescriptorProto:
field = descriptor_pb2.FieldDescriptorProto(
name="value", number=number, type=field_type
)
if repeated:
field.label = descriptor_pb2.FieldDescriptorProto.LABEL_REPEATED
if force:
field.options.Extensions[pb.force] = True
return field
def _encode_field(
field_type: int, number: int = 1, force: bool = False, repeated: bool = False
) -> str:
"""Return the encode statement the generator emits for one encode-only field."""
field = _field(field_type, number, force=force, repeated=repeated)
return create_field_type_info(
field, needs_decode=False, needs_encode=True
).encode_content
SCALAR_TYPES = [
BOOL,
UINT32,
INT32,
UINT64,
INT64,
SINT32,
FLOAT,
FIXED32,
STRING,
BYTES,
]
@pytest.mark.parametrize("field_type", SCALAR_TYPES)
def test_forced_fields_use_the_force_overload_or_raw_writes(field_type: int) -> None:
content = _encode_field(field_type, force=True)
assert (
"_force(" in content
or "write_raw_byte(" in content
or "write_tag_and_fixed32(" in content
), content
@pytest.mark.parametrize("field_type", [FLOAT, FIXED32])
def test_single_byte_tag_fixed32_shares_the_outlined_writer(field_type: int) -> None:
unconditional = _encode_field(field_type, force=True)
assert unconditional.count("write_tag_and_fixed32(pos, 13,") == 1, unconditional
guarded = _encode_field(field_type, force=False)
assert guarded.startswith("if ("), guarded
assert "[[likely]]" in guarded
assert "write_tag_and_fixed32(pos, 13," in guarded
@pytest.mark.parametrize("field_type", [FLOAT, FIXED32])
def test_multi_byte_tag_fixed32_falls_back_to_the_generic_helper(
field_type: int,
) -> None:
content = _encode_field(field_type, number=16)
assert "write_tag_and_fixed32" not in content, content
assert content.startswith("pos = ProtoEncode::encode_"), content
def _decode_case(field_type: int, number: int, *, repeated: bool = False) -> str:
"""Return the decode_field() case the generator emits for one decoded field."""
field = _field(field_type, number, repeated=repeated)
if field_type == MESSAGE:
field.type_name = ".Sub"
return create_field_type_info(
field, needs_decode=True, needs_encode=False
).decode_content
@pytest.mark.parametrize(
("field_type", "number", "wire_type", "accessor"),
[
(UINT32, 2, "WIRE_TYPE_VARINT", "value.as_varint()"),
(BOOL, 3, "WIRE_TYPE_VARINT", "value.as_bool()"),
(STRING, 1, "WIRE_TYPE_LENGTH_DELIMITED", "value.data()"),
(FLOAT, 4, "WIRE_TYPE_FIXED32", "value.as_float()"),
(FIXED32, 5, "WIRE_TYPE_FIXED32", "value.as_fixed32()"),
],
)
def test_decode_cases_carry_field_number_and_wire_type(
field_type: int, number: int, wire_type: str, accessor: str
) -> None:
"""Each decoded field yields one case keyed on its number and declared wire type."""
case = _decode_case(field_type, number)
lines = case.splitlines()
assert lines[0] == f"case proto_tag({number}, {wire_type}):", case
assert accessor in lines[1], case
assert lines[-1].strip() == "break;", case
@pytest.mark.parametrize(
("field_type", "repeated", "wire_type", "store"),
[
(UINT32, True, "WIRE_TYPE_VARINT", "this->value.push_back(value.as_varint());"),
(
STRING,
True,
"WIRE_TYPE_LENGTH_DELIMITED",
"this->value.push_back(value.as_string());",
),
(
MESSAGE,
False,
"WIRE_TYPE_LENGTH_DELIMITED",
"value.decode_to_message(this->value);",
),
(
MESSAGE,
True,
"WIRE_TYPE_LENGTH_DELIMITED",
"value.decode_to_message(this->value.back());",
),
],
)
def test_repeated_and_message_fields_decode_through_the_same_case_shape(
field_type: int, repeated: bool, wire_type: str, store: str
) -> None:
"""Repeated and sub message fields land in the one switch with their own store."""
case = _decode_case(field_type, 7, repeated=repeated)
lines = case.splitlines()
assert lines[0] == f"case proto_tag(7, {wire_type}):", case
assert store in case, case
if field_type == MESSAGE and repeated:
assert "this->value.emplace_back();" in case, case
assert lines[-1].strip() == "break;", case
def test_a_fixed64_field_fails_at_generation_time() -> None:
"""The decode loop has no 64 bit wire type path, so such a field must never reach it silently."""
desc = descriptor_pb2.DescriptorProto(name="Wide")
desc.field.add(name="ratio", number=1, type=DOUBLE)
with pytest.raises(
ValueError, match="64-bit type 'double' .*ratio.* not supported"
):
build_message_type(desc, {}, {"Wide": SOURCE_CLIENT})
def test_message_gets_a_single_decode_field_override() -> None:
"""All wire types of a decoded message land in one decode_field() switch."""
desc = descriptor_pb2.DescriptorProto(name="Mixed")
desc.field.add(name="name", number=1, type=STRING)
desc.field.add(name="count", number=2, type=UINT32)
desc.field.add(name="level", number=3, type=FLOAT)
header, cpp, _ = build_message_type(desc, {}, {"Mixed": SOURCE_CLIENT})
decl = "void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;"
assert header.count(decl) == 1
assert (
cpp.count(
"void Mixed::decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {"
)
== 1
)
assert "switch (tag) {" in cpp
assert "const ProtoFieldValue value(data, scalar);" in cpp
for number, wire_type in (
(1, "WIRE_TYPE_LENGTH_DELIMITED"),
(2, "WIRE_TYPE_VARINT"),
(3, "WIRE_TYPE_FIXED32"),
):
assert f"case proto_tag({number}, {wire_type}):" in cpp, cpp