1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 00:31:58 +00:00

[api] Proto code generator changes for #12985 (#13100)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Keith Burzinski
2026-01-10 19:43:27 -06:00
committed by GitHub
parent 5725a4840e
commit f2eb61a767
2 changed files with 96 additions and 0 deletions

View File

@@ -80,4 +80,15 @@ extend google.protobuf.FieldOptions {
// Example: [(container_pointer_no_template) = "light::ColorModeMask"]
// generates: const light::ColorModeMask *supported_color_modes{};
optional string container_pointer_no_template = 50014;
// packed_buffer: Expose raw packed buffer instead of decoding into container
// When set on a packed repeated field, the generated code stores a pointer
// to the raw protobuf buffer instead of decoding values. This enables
// zero-copy passthrough when the consumer can decode on-demand.
// The field must be a packed repeated field (packed=true).
// Generates three fields:
// - const uint8_t *<field>_data_{nullptr};
// - uint16_t <field>_length_{0};
// - uint16_t <field>_count_{0};
optional bool packed_buffer = 50015 [default=false];
}

View File

@@ -339,6 +339,9 @@ def create_field_type_info(
) -> TypeInfo:
"""Create the appropriate TypeInfo instance for a field, handling repeated fields and custom options."""
if field.label == FieldDescriptorProto.LABEL_REPEATED:
# Check if this is a packed_buffer field (zero-copy packed repeated)
if get_field_opt(field, pb.packed_buffer, False):
return PackedBufferTypeInfo(field)
# Check if this repeated field has fixed_array_with_length_define option
if (
fixed_size := get_field_opt(field, pb.fixed_array_with_length_define)
@@ -947,6 +950,88 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string
class PackedBufferTypeInfo(TypeInfo):
"""Type for packed repeated fields that expose raw buffer instead of decoding.
When a repeated field is marked with [(packed_buffer) = true], this type
generates code that stores a pointer to the raw protobuf buffer along with
its length and the count of values. This enables zero-copy passthrough when
the consumer can decode the packed varints on-demand.
"""
def __init__(self, field: descriptor.FieldDescriptorProto) -> None:
# packed_buffer is decode-only (SOURCE_CLIENT messages)
super().__init__(field, needs_decode=True, needs_encode=False)
@property
def cpp_type(self) -> str:
# Not used - we have multiple fields
return "const uint8_t*"
@property
def wire_type(self) -> WireType:
"""Packed fields use LENGTH_DELIMITED wire type."""
return WireType.LENGTH_DELIMITED
@property
def public_content(self) -> list[str]:
"""Generate three fields: data pointer, length, and count."""
return [
f"const uint8_t *{self.field_name}_data_{{nullptr}};",
f"uint16_t {self.field_name}_length_{{0}};",
f"uint16_t {self.field_name}_count_{{0}};",
]
@property
def decode_length_content(self) -> str:
"""Store pointer to buffer and calculate count of packed varints."""
return f"""case {self.number}: {{
this->{self.field_name}_data_ = value.data();
this->{self.field_name}_length_ = value.size();
this->{self.field_name}_count_ = count_packed_varints(value.data(), value.size());
break;
}}"""
@property
def encode_content(self) -> str:
"""No encoding - this is decode-only for SOURCE_CLIENT messages."""
return None
@property
def dump_content(self) -> str:
"""Dump shows buffer info but not decoded values."""
return (
f'out.append(" {self.name}: ");\n'
+ 'out.append("packed buffer [");\n'
+ f"out.append(std::to_string(this->{self.field_name}_count_));\n"
+ 'out.append(" values, ");\n'
+ f"out.append(std::to_string(this->{self.field_name}_length_));\n"
+ 'out.append(" bytes]\\n");'
)
def dump(self, name: str) -> str:
"""Dump method for packed buffer - not typically used but required by abstract base."""
return 'out.append("packed buffer");'
def get_size_calculation(self, name: str, force: bool = False) -> str:
"""No size calculation needed - decode-only."""
return ""
def get_estimated_size(self) -> int:
"""Estimate size for packed buffer field.
Typical IR/RF timing array has ~50-200 values, each encoded as 1-3 bytes.
Estimate 100 values * 2 bytes = 200 bytes typical.
"""
return (
self.calculate_field_id_size() + 2 + 200
) # field ID + length varint + data
@classmethod
def can_use_dump_field(cls) -> bool:
return False
class FixedArrayBytesType(TypeInfo):
"""Special type for fixed-size byte arrays."""