[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.
This commit is contained in:
J. Nick Koston
2026-09-07 15:24:33 +02:00
parent 1f8ed8915b
commit 88b0e51536
4 changed files with 347 additions and 610 deletions
File diff suppressed because it is too large Load Diff
+3 -14
View File
@@ -170,20 +170,9 @@ class ProtoVarInt {
class ProtoMessage;
class ProtoSize;
// Switch key for generated decode_field() bodies. Embedded builds use compare chains
// (-fno-jump-tables), so the full tag is one compare per field with no guard. The host gets a
// jump table from the dense field number switch plus a tag compare that rejects the wrong wire type.
#ifdef USE_HOST
#define PROTO_DECODE_KEY(tag) ((tag) >> 3)
#define PROTO_DECODE_CASE(field_id, wire_type) (field_id)
#define PROTO_DECODE_GUARD(tag, field_id, wire_type) \
if ((tag) != (((field_id) << 3) | (wire_type))) \
return false
#else
#define PROTO_DECODE_KEY(tag) (tag)
#define PROTO_DECODE_CASE(field_id, wire_type) (((field_id) << 3) | (wire_type))
#define PROTO_DECODE_GUARD(tag, field_id, wire_type) (void) 0
#endif
/// 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.
+9 -5
View File
@@ -28,6 +28,11 @@ class WireType(IntEnum):
END_GROUP = 4 # groups (deprecated)
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
# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto
@@ -230,10 +235,9 @@ class TypeInfo(ABC):
return f"{self.cpp_type} {self.field_name}{{{self.default_value}}};"
def decode_case(self, body: str) -> str:
"""Emit one decode_field() case, keyed through the PROTO_DECODE_* macros in proto.h."""
wire_type = int(self.wire_type)
return f"case PROTO_DECODE_CASE({self.number}, {wire_type}):\n" + indent(
f"PROTO_DECODE_GUARD(tag, {self.number}, {wire_type});\n{body}\nbreak;"
"""Emit one decode_field() case, keyed on the field's wire tag."""
return f"case proto_tag({self.number}, {self.wire_type.cpp_name}):\n" + indent(
f"{body}\nbreak;"
)
# Decode expression per wire type; a decodable type sets exactly one.
@@ -2685,7 +2689,7 @@ def build_message_type(
if decode:
o = f"bool {desc.name}::decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {{\n"
o += " const ProtoFieldValue value(data, scalar);\n"
o += " switch (PROTO_DECODE_KEY(tag)) {\n"
o += " switch (tag) {\n"
o += indent("\n".join(decode), " ") + "\n"
o += " default: return false;\n"
o += " }\n"
@@ -197,22 +197,22 @@ def _decode_case(field_type: int, number: int) -> str:
@pytest.mark.parametrize(
("field_type", "number", "wire_type", "accessor"),
[
(UINT32, 2, 0, "value.as_varint()"),
(BOOL, 3, 0, "value.as_varint() != 0"),
(STRING, 1, 2, "value.data()"),
(FLOAT, 4, 5, "value.as_float()"),
(FIXED32, 5, 5, "value.as_fixed32()"),
(UINT32, 2, "WIRE_TYPE_VARINT", "value.as_varint()"),
(BOOL, 3, "WIRE_TYPE_VARINT", "value.as_varint() != 0"),
(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: int, accessor: str
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_DECODE_CASE({number}, {wire_type}):", case
assert lines[1].strip() == f"PROTO_DECODE_GUARD(tag, {number}, {wire_type});", case
assert accessor in case, case
assert lines[0] == f"case proto_tag({number}, {wire_type}):", case
assert accessor in lines[1], case
assert lines[-1].strip() == "break;", case
def test_message_gets_a_single_decode_field_override() -> None:
@@ -231,7 +231,11 @@ def test_message_gets_a_single_decode_field_override() -> None:
)
== 1
)
assert "switch (PROTO_DECODE_KEY(tag)) {" in cpp
assert "switch (tag) {" in cpp
assert "const ProtoFieldValue value(data, scalar);" in cpp
for number, wire_type in ((1, 2), (2, 0), (3, 5)):
assert f"case PROTO_DECODE_CASE({number}, {wire_type}):" in cpp, 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