From ab5a5b5276369896fd1d8bc1e8c265d3bf05dc30 Mon Sep 17 00:00:00 2001 From: Ryan Brewster Date: Sat, 18 Apr 2026 18:48:47 +0000 Subject: [PATCH 01/13] view: ViewEncode<'a> sub-trait for borrowed-field serialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ViewEncode<'a>: MessageView<'a> with compute_size / write_to / cached_size, mirroring Message's two-pass model. Provided encode/encode_to_vec/encode_to_bytes. Field bytes are written by borrow — no String/Vec allocations. MessageFieldView gains compute_size/write_to/cached_size forwarding when V: ViewEncode. MapView gains new()/From/FromIterator so callers can build a view map from borrowed (&str, &str) pairs (duplicate keys are encoded as-is). UnknownFieldsView gains write_to (concatenate raw spans). MessageView itself is unchanged. View remains the zero-copy read path by default; encode is opt-in via codegen. Targets 0.4.0: enabling view_encode on buffa-types adds a __buffa_cached_size field to WKT view structs (see CHANGELOG). --- CHANGELOG.md | 18 +++++ buffa/src/lib.rs | 2 +- buffa/src/view.rs | 199 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 217 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f01b80..961c94d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Breaking changes +- **WKT view structs gain a `__buffa_cached_size` field** when `buffa-types` + is built with the new `view_encode` capability enabled (which it is, so + nested-WKT views are encodable out of the box). Code that constructs + `TimestampView { seconds, nanos, __buffa_unknown_fields }` without + `..Default::default()` will fail to compile; use the trailing + `..Default::default()` per the documented convention. - **`google.protobuf.Any.value` is now `::bytes::Bytes` instead of `Vec`.** Makes `Any::clone()` a cheap refcount bump (up to ~170x faster for large payloads) instead of a full memcpy. Call sites constructing an `Any` by hand @@ -15,6 +21,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), directly). Reading `any.value` is unchanged — `Bytes` derefs to `&[u8]`. `buffa-types` now depends on `bytes` unconditionally. +### Added + +- **`ViewEncode<'a>` — opt-in serialization from borrowed view types.** + `Config::view_encode(true)` makes generated `*View<'a>` types implement + `ViewEncode` with the same two-pass `compute_size`/`write_to` model as + `Message`. Views can be constructed from borrowed `&'a str` / `&'a [u8]` + and encoded without intermediate `String`/`Vec` allocation. Benchmarks: + parity on serialize-only; ~6× on build+encode for a 15-label string-map + message. +- **`MapView::new(Vec)` / `From` / `FromIterator`** for constructing + map views directly (for `ViewEncode`). + ## [0.3.0] - 2026-04-01 ### Breaking changes diff --git a/buffa/src/lib.rs b/buffa/src/lib.rs index 1173c54..800af10 100644 --- a/buffa/src/lib.rs +++ b/buffa/src/lib.rs @@ -207,7 +207,7 @@ pub use unknown_fields::{UnknownField, UnknownFieldData, UnknownFields}; pub use text::TextFormat; pub use view::{ DefaultViewInstance, HasDefaultViewInstance, MapView, MessageFieldView, MessageView, OwnedView, - RepeatedView, UnknownFieldsView, + RepeatedView, UnknownFieldsView, ViewEncode, }; /// Private re-exports used exclusively by generated code. diff --git a/buffa/src/view.rs b/buffa/src/view.rs index de795b9..8c8d458 100644 --- a/buffa/src/view.rs +++ b/buffa/src/view.rs @@ -76,7 +76,7 @@ use crate::error::DecodeError; use crate::message::Message as _; -use bytes::Bytes; +use bytes::{BufMut, Bytes}; /// Trait for zero-copy borrowed message views. /// @@ -112,6 +112,86 @@ pub trait MessageView<'a>: Sized { fn to_owned_message(&self) -> Self::Owned; } +/// Serialize a [`MessageView`] directly from its borrowed fields. +/// +/// Symmetric with [`Message`](crate::Message)'s two-pass +/// `compute_size` / `write_to` model, but the `&'a str` / `&'a [u8]` / +/// [`MapView`] / [`RepeatedView`] fields are written by borrow — no +/// owned-struct intermediary, no per-field `String`/`Vec` allocations. +/// +/// Generated `*View<'a>` types implement this trait when codegen is +/// configured with `view_encode(true)`; off by default. Each implementing +/// view struct gains a `__buffa_cached_size` field for the cached-size +/// pass — same `AtomicU32`-backed [`CachedSize`](crate::__private::CachedSize) +/// as owned messages, so views remain `Send + Sync`. +/// +/// ## When to use +/// +/// Reach for `ViewEncode` when the source data is already in memory and +/// you would otherwise allocate an owned message just to encode-then-drop +/// it — e.g. an RPC handler serializing from app state. If you already +/// hold the owned message, use [`Message::encode`](crate::Message::encode) +/// instead; the wire output is identical. +/// +/// ```rust,ignore +/// let view = PersonView { +/// name: "borrowed", +/// tags: ["a", "b"].iter().copied().collect(), +/// ..Default::default() +/// }; +/// let bytes = view.encode_to_vec(); +/// ``` +#[diagnostic::on_unimplemented( + message = "`{Self}` does not implement `ViewEncode` — view encoding was not generated for this type", + note = "enable `view_encode(true)` in your buffa-build / buffa-codegen config to generate encode methods on view types" +)] +pub trait ViewEncode<'a>: MessageView<'a> { + /// Compute and cache the encoded byte size of this view. + /// + /// Recursively computes sizes for sub-message views and stores them in + /// each view's `CachedSize` field. Must be called before + /// [`write_to`](Self::write_to). + fn compute_size(&self) -> u32; + + /// Write this view's encoded bytes to a buffer. + /// + /// Assumes [`compute_size`](Self::compute_size) has already been called. + /// Uses cached sizes for length-delimited sub-message headers. + fn write_to(&self, buf: &mut impl BufMut); + + /// Return the size cached by the most recent [`compute_size`](Self::compute_size). + fn cached_size(&self) -> u32; + + /// Convenience: compute size, then write. Primary view-encode entry point. + fn encode(&self, buf: &mut impl BufMut) { + self.compute_size(); + self.write_to(buf); + } + + /// Encode this view as a length-delimited byte sequence. + fn encode_length_delimited(&self, buf: &mut impl BufMut) { + let len = self.compute_size(); + crate::encoding::encode_varint(len as u64, buf); + self.write_to(buf); + } + + /// Encode this view to a new `Vec`. + fn encode_to_vec(&self) -> alloc::vec::Vec { + let size = self.compute_size() as usize; + let mut buf = alloc::vec::Vec::with_capacity(size); + self.write_to(&mut buf); + buf + } + + /// Encode this view to a new [`bytes::Bytes`]. + fn encode_to_bytes(&self) -> Bytes { + let size = self.compute_size() as usize; + let mut buf = bytes::BytesMut::with_capacity(size); + self.write_to(&mut buf); + buf.freeze() + } +} + /// Provides access to a lazily-initialized default view instance. /// /// View types implement this trait so that [`MessageFieldView`] can @@ -182,6 +262,13 @@ impl MessageFieldView { } } + /// Alias for [`set`](Self::set), mirroring owned + /// [`MessageField::some`](crate::MessageField::some). + #[inline] + pub fn some(v: V) -> Self { + Self::set(v) + } + /// Returns `true` if the field has a value. #[inline] pub fn is_set(&self) -> bool { @@ -210,6 +297,32 @@ impl MessageFieldView { } } +impl<'a, V: ViewEncode<'a>> MessageFieldView { + /// Forward to the inner view's [`compute_size`](ViewEncode::compute_size), + /// or `0` if unset. Generated `compute_size` calls this for nested-message + /// fields, mirroring [`MessageField`](crate::MessageField) on the owned side. + #[inline] + pub fn compute_size(&self) -> u32 { + self.inner.as_deref().map_or(0, V::compute_size) + } + + /// Forward to the inner view's [`cached_size`](ViewEncode::cached_size), + /// or `0` if unset. + #[inline] + pub fn cached_size(&self) -> u32 { + self.inner.as_deref().map_or(0, V::cached_size) + } + + /// Forward to the inner view's [`write_to`](ViewEncode::write_to); + /// no-op if unset. + #[inline] + pub fn write_to(&self, buf: &mut impl BufMut) { + if let Some(v) = self.inner.as_deref() { + v.write_to(buf); + } + } +} + impl Default for MessageFieldView { #[inline] fn default() -> Self { @@ -217,6 +330,13 @@ impl Default for MessageFieldView { } } +impl From for MessageFieldView { + #[inline] + fn from(v: V) -> Self { + Self::set(v) + } +} + /// Marker trait linking a lifetime-parameterized view type `V` (e.g., /// `FooView<'a>`) to its `'static` instantiation that implements /// [`DefaultViewInstance`]. Generated code implements this for every @@ -363,6 +483,18 @@ impl<'b, 'a, T> IntoIterator for &'b RepeatedView<'a, T> { } } +impl<'a, T> From> for RepeatedView<'a, T> { + fn from(elements: alloc::vec::Vec) -> Self { + Self::new(elements) + } +} + +impl<'a, T> FromIterator for RepeatedView<'a, T> { + fn from_iter>(iter: I) -> Self { + Self::new(iter.into_iter().collect()) + } +} + /// A borrowed view of a map field. /// /// Protobuf `map` fields are encoded as repeated sub-messages, each @@ -397,6 +529,17 @@ pub struct MapView<'a, K, V> { } impl<'a, K, V> MapView<'a, K, V> { + /// Construct from a `Vec` of entries, for [`ViewEncode`] use. + /// + /// Duplicate keys are kept and all encoded — valid protobuf wire data + /// (decoders apply last-write-wins). Mirrors [`RepeatedView::new`]. + pub fn new(entries: alloc::vec::Vec<(K, V)>) -> Self { + Self { + entries, + _marker: core::marker::PhantomData, + } + } + /// Returns the number of entries (including duplicates). pub fn len(&self) -> usize { self.entries.len() @@ -456,6 +599,19 @@ impl<'a, K, V> MapView<'a, K, V> { } } +impl<'a, K, V> From> for MapView<'a, K, V> { + fn from(entries: alloc::vec::Vec<(K, V)>) -> Self { + Self::new(entries) + } +} + +/// Duplicate keys are kept and all encoded; see [`MapView::new`]. +impl<'a, K, V> FromIterator<(K, V)> for MapView<'a, K, V> { + fn from_iter>(iter: I) -> Self { + Self::new(iter.into_iter().collect()) + } +} + impl<'a, K, V> Default for MapView<'a, K, V> { fn default() -> Self { Self { @@ -514,6 +670,15 @@ impl<'a> UnknownFieldsView<'a> { self.raw_spans.iter().map(|s| s.len()).sum() } + /// Write all unknown-field bytes verbatim. Each span is a complete + /// `(tag, value)` record as it appeared on the wire, so concatenating + /// them produces a valid encoding. + pub fn write_to(&self, buf: &mut impl BufMut) { + for span in &self.raw_spans { + buf.put_slice(span); + } + } + /// Convert to an owned [`UnknownFields`](crate::UnknownFields) by parsing all stored raw byte spans. /// /// Each span is a complete (tag + value) record as it appeared on the wire. @@ -1228,6 +1393,38 @@ mod tests { } } + impl<'a> ViewEncode<'a> for SimpleMessageView<'a> { + fn compute_size(&self) -> u32 { + let mut size = 0u32; + if self.id != 0 { + size += 1 + crate::types::int32_encoded_len(self.id) as u32; + } + if !self.name.is_empty() { + size += 1 + crate::types::string_encoded_len(self.name) as u32; + } + size + } + + fn write_to(&self, buf: &mut impl bytes::BufMut) { + if self.id != 0 { + crate::encoding::Tag::new(1, crate::encoding::WireType::Varint).encode(buf); + crate::types::encode_int32(self.id, buf); + } + if !self.name.is_empty() { + crate::encoding::Tag::new(2, crate::encoding::WireType::LengthDelimited) + .encode(buf); + crate::types::encode_string(self.name, buf); + } + } + + fn cached_size(&self) -> u32 { + // Test-only stub: SimpleMessageView has no nested messages, + // so nothing reads cached_size. Real impls return the value + // stored by compute_size. + 0 + } + } + /// Encode a SimpleMessage to Bytes for testing. fn encode_simple(id: i32, name: &str) -> Bytes { let msg = SimpleMessage { From 2610bdbb7dc16974334622ddbb8fa77d934cb70f Mon Sep 17 00:00:00 2001 From: Ryan Brewster Date: Sat, 18 Apr 2026 18:49:00 +0000 Subject: [PATCH 02/13] =?UTF-8?q?codegen:=20view=5Fencode=20opt-in=20flag?= =?UTF-8?q?=20=E2=86=92=20impl=20ViewEncode=20on=20*View<'a>?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeGenConfig::view_encode (default false). When set, codegen emits __buffa_cached_size on each view struct and a separate impl ViewEncode<'a> for FooView<'a> block. The per-field encode-stmt builders (scalar_/repeated_/map_/oneof_*_stmt) emit &self.field-relative code that already takes &str/&[u8], so they apply to view field types unchanged. Oneof: the view-side enum (mod::FooOneofView) has the same variant names as owned with borrowed payload types; oneof_{size,write}_arm dispatch by name and call duck-typed primitives (string_encoded_len(x), x.compute_size()), so they work once pointed at the view enum path. buffa_build::Config::view_encode(bool) setter; protoc-gen-buffa view_encode=true option. --- buffa-build/src/lib.rs | 12 ++ buffa-codegen/src/impl_message.rs | 206 ++++++++++++++++++++++++++++++ buffa-codegen/src/lib.rs | 15 +++ buffa-codegen/src/view.rs | 26 ++++ protoc-gen-buffa/src/main.rs | 1 + 5 files changed, 260 insertions(+) diff --git a/buffa-build/src/lib.rs b/buffa-build/src/lib.rs index 5b8c1e4..581c87a 100644 --- a/buffa-build/src/lib.rs +++ b/buffa-build/src/lib.rs @@ -125,6 +125,18 @@ impl Config { self } + /// Enable or disable `impl buffa::ViewEncode` on generated `*View<'a>` + /// types (default: false). + /// + /// When enabled, view types can be serialized directly from their + /// borrowed `&'a str` / `&'a [u8]` fields without building an owned + /// message. Each view struct gains a `__buffa_cached_size` field. + #[must_use] + pub fn view_encode(mut self, enabled: bool) -> Self { + self.codegen_config.view_encode = enabled; + self + } + /// Enable or disable `#[derive(arbitrary::Arbitrary)]` on generated /// types (default: false). /// diff --git a/buffa-codegen/src/impl_message.rs b/buffa-codegen/src/impl_message.rs index 66d8031..9b1e962 100644 --- a/buffa-codegen/src/impl_message.rs +++ b/buffa-codegen/src/impl_message.rs @@ -604,6 +604,212 @@ pub fn generate_message_impl( }) } +/// Build the `compute_size` / `write_to` / `cached_size` method tokens for a +/// **view** type. Reuses the same per-field stmt builders as +/// [`generate_message_impl`] — they emit `&self.field`-relative code that is +/// duck-type-compatible with view field types (`&'a str`, `RepeatedView`, +/// `MapView`, `MessageFieldView`). +pub(crate) fn build_view_encode_methods( + ctx: &CodeGenContext, + msg: &DescriptorProto, + preserve_unknown_fields: bool, + features: &ResolvedFeatures, + oneof_idents: &std::collections::HashMap, + mod_ident: &proc_macro2::Ident, +) -> Result { + let scalar_fields: Vec<_> = msg + .field + .iter() + .filter(|f| { + if is_real_oneof_member(f) { + return false; + } + if f.label.unwrap_or_default() == Label::LABEL_REPEATED { + return false; + } + is_supported_field_type(f.r#type.unwrap_or_default()) + }) + .collect(); + let repeated_fields: Vec<_> = msg + .field + .iter() + .filter(|f| { + f.label.unwrap_or_default() == Label::LABEL_REPEATED + && !crate::message::is_map_field(msg, f) + && is_supported_field_type(f.r#type.unwrap_or_default()) + }) + .collect(); + let oneof_groups: Vec<(String, proc_macro2::Ident, Vec<&FieldDescriptorProto>)> = msg + .oneof_decl + .iter() + .enumerate() + .filter_map(|(idx, oneof)| { + let enum_ident = oneof_idents.get(&idx)?; + let fields: Vec<_> = msg + .field + .iter() + .filter(|f| is_real_oneof_member(f) && f.oneof_index == Some(idx as i32)) + .collect(); + if fields.is_empty() { + return None; + } + Some(( + oneof.name.as_deref()?.to_string(), + enum_ident.clone(), + fields, + )) + }) + .collect(); + let map_fields: Vec<_> = msg + .field + .iter() + .filter(|f| { + f.label.unwrap_or_default() == Label::LABEL_REPEATED + && crate::message::is_map_field(msg, f) + }) + .collect(); + + let compute_stmts = scalar_fields + .iter() + .copied() + .map(|f| scalar_compute_size_stmt(ctx, f, features)) + .collect::, _>>()?; + let repeated_compute_stmts = repeated_fields + .iter() + .copied() + .map(|f| repeated_compute_size_stmt(ctx, f, features)) + .collect::, _>>()?; + let write_stmts = scalar_fields + .iter() + .copied() + .map(|f| scalar_write_to_stmt(ctx, f, features)) + .collect::, _>>()?; + let repeated_write_stmts = repeated_fields + .iter() + .copied() + .map(|f| repeated_write_to_stmt(ctx, f, features)) + .collect::, _>>()?; + + // The view-side oneof enum (`mod::FooOneofView<'a>`) has the same variant + // *names* as the owned `mod::FooOneof` but borrowed payload types + // (`&'a str` / `Box>` vs `String` / `Box`). The arm + // builders only emit the enum path + variant name and call duck-typed + // primitives (`string_encoded_len(x)`, `x.compute_size()`), so they work + // unchanged once pointed at the view enum. + let mut oneof_compute_stmts: Vec = Vec::new(); + let mut oneof_write_stmts: Vec = Vec::new(); + for (oneof_name, enum_ident, fields) in &oneof_groups { + let field_ident = make_field_ident(oneof_name); + let view_enum = format_ident!("{}View", enum_ident); + let qualified: TokenStream = quote! { #mod_ident::#view_enum }; + let mut size_arms: Vec = Vec::new(); + let mut write_arms: Vec = Vec::new(); + for field in fields { + let field_number = validated_field_number(field)?; + let ty = effective_type(ctx, field, features); + let variant = crate::oneof::oneof_variant_ident( + field + .name + .as_deref() + .ok_or(CodeGenError::MissingField("field.name"))?, + ); + let tag_len = tag_encoded_len(field_number, wire_type_byte(ty)); + let wire_type = wire_type_token(ty); + size_arms.push(oneof_size_arm(&qualified, &variant, tag_len, ty)); + write_arms.push(oneof_write_arm( + &qualified, + &variant, + field_number, + ty, + &wire_type, + )); + } + oneof_compute_stmts.push(quote! { + if let ::core::option::Option::Some(ref v) = self.#field_ident { + match v { #(#size_arms)* } + } + }); + oneof_write_stmts.push(quote! { + if let ::core::option::Option::Some(ref v) = self.#field_ident { + match v { #(#write_arms)* } + } + }); + } + + let mut map_compute_stmts: Vec = Vec::new(); + let mut map_write_stmts: Vec = Vec::new(); + for f in &map_fields { + map_compute_stmts.push(map_compute_size_stmt(ctx, msg, f, features)?); + map_write_stmts.push(map_write_to_stmt(ctx, msg, f, features)?); + } + + let unknown_fields_size_stmt = if preserve_unknown_fields { + quote! { size += self.__buffa_unknown_fields.encoded_len() as u32; } + } else { + quote! {} + }; + let unknown_fields_write_stmt = if preserve_unknown_fields { + quote! { self.__buffa_unknown_fields.write_to(buf); } + } else { + quote! {} + }; + + let has_compute = !scalar_fields.is_empty() + || !repeated_fields.is_empty() + || !oneof_compute_stmts.is_empty() + || !map_compute_stmts.is_empty() + || preserve_unknown_fields; + let size_decl = if has_compute { + quote! { let mut size = 0u32; } + } else { + quote! { let size = 0u32; } + }; + let has_write = !write_stmts.is_empty() + || !repeated_write_stmts.is_empty() + || !oneof_write_stmts.is_empty() + || !map_write_stmts.is_empty() + || preserve_unknown_fields; + let buf_param = if has_write { + quote! { buf: &mut impl ::buffa::bytes::BufMut } + } else { + quote! { _buf: &mut impl ::buffa::bytes::BufMut } + }; + + Ok(quote! { + // needless_borrow: stmt builders emit `&self.field` so they work on + // owned `String`/`Vec`; on view fields (`&'a str`/`&'a [u8]`) + // the borrow is redundant but harmless. + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + #size_decl + #(#compute_stmts)* + #(#repeated_compute_stmts)* + #(#oneof_compute_stmts)* + #(#map_compute_stmts)* + #unknown_fields_size_stmt + self.__buffa_cached_size.set(size); + size + } + + #[allow(clippy::needless_borrow)] + fn write_to(&self, #buf_param) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + #(#write_stmts)* + #(#repeated_write_stmts)* + #(#oneof_write_stmts)* + #(#map_write_stmts)* + #unknown_fields_write_stmt + } + + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } + }) +} + /// Generate a clear statement for a scalar (non-repeated, non-oneof) field. /// /// Returns a `TokenStream` that clears the field to its default value while diff --git a/buffa-codegen/src/lib.rs b/buffa-codegen/src/lib.rs index d290e63..9b6fb4e 100644 --- a/buffa-codegen/src/lib.rs +++ b/buffa-codegen/src/lib.rs @@ -196,6 +196,20 @@ pub struct CodeGenConfig { /// When this is `true`, the downstream crate must enable the `buffa/text` /// feature for the runtime encoder/decoder. pub generate_text: bool, + /// Whether to emit `impl buffa::ViewEncode` on generated `*View<'a>` types + /// so views can be serialized directly from borrowed fields. + /// + /// When `true`, each view struct gains a `__buffa_cached_size` field and + /// an `impl ViewEncode<'a>` block whose `compute_size` / `write_to` + /// reuse the same per-field encode logic as the owned `Message` impl — + /// the field encoders take `&str` / `&[u8]` so they apply to view field + /// types unchanged. + /// + /// Default `false`: View remains the zero-copy *read* path per + /// [DESIGN.md §2](../DESIGN.md). Enable for high-throughput servers + /// that build messages from borrowed data and want to skip the owned + /// `String` allocation per field on the encode path. + pub view_encode: bool, /// Whether the per-package `.mod.rs` stitcher emits /// `__buffa::register_types(&mut TypeRegistry)`. /// @@ -248,6 +262,7 @@ impl Default for CodeGenConfig { strict_utf8_mapping: false, allow_message_set: false, generate_text: false, + view_encode: false, emit_register_fn: true, type_attributes: Vec::new(), field_attributes: Vec::new(), diff --git a/buffa-codegen/src/view.rs b/buffa-codegen/src/view.rs index f28da60..7122ce0 100644 --- a/buffa-codegen/src/view.rs +++ b/buffa-codegen/src/view.rs @@ -149,6 +149,29 @@ pub(crate) fn generate_view_with_nesting( } else { quote! {} }; + let (cached_size_field, view_encode_impl) = if ctx.config.view_encode { + let methods = crate::impl_message::build_view_encode_methods( + ctx, + msg, + ctx.config.preserve_unknown_fields, + features, + &oneof_idents, + &mod_ident, + )?; + ( + quote! { + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, + }, + quote! { + impl<'a> ::buffa::ViewEncode<'a> for #view_ident<'a> { + #methods + } + }, + ) + } else { + (quote! {}, quote! {}) + }; // When preserving unknowns we capture `before_tag` so we can compute the // raw byte span after `skip_field` advances the cursor. @@ -205,6 +228,7 @@ pub(crate) fn generate_view_with_nesting( #(#direct_fields)* #(#oneof_struct_fields)* #unknown_fields_field + #cached_size_field #phantom_field } @@ -298,6 +322,8 @@ pub(crate) fn generate_view_with_nesting( } } + #view_encode_impl + // SAFETY: The static default instance is lazily initialized via OnceBox // and never mutated after publication. unsafe impl ::buffa::DefaultViewInstance for #view_ident<'static> { diff --git a/protoc-gen-buffa/src/main.rs b/protoc-gen-buffa/src/main.rs index 1398900..0fc7f1c 100644 --- a/protoc-gen-buffa/src/main.rs +++ b/protoc-gen-buffa/src/main.rs @@ -127,6 +127,7 @@ fn parse_config(params: &str) -> Result { if let Some((key, value)) = param.split_once('=') { match key.trim() { "views" => codegen.generate_views = value.trim() == "true", + "view_encode" => codegen.view_encode = value.trim() == "true", "unknown_fields" => codegen.preserve_unknown_fields = value.trim() != "false", "json" => codegen.generate_json = value.trim() == "true", "text" => codegen.generate_text = value.trim() == "true", From fae63613d621cf30d41661ae96ff6b33fc90fdca Mon Sep 17 00:00:00 2001 From: Ryan Brewster Date: Sat, 18 Apr 2026 18:49:16 +0000 Subject: [PATCH 03/13] buffa-types: enable view_encode for WKTs and regenerate WKTs are commonly nested inside application messages (Timestamp, Any, Struct/Value); a consumer that opts into view_encode needs the WKT view types to implement ViewEncode for nested-message dispatch to compile. The extra __buffa_cached_size field is 4 bytes per WKT view. `task gen-wkt-types` --- buffa-codegen/src/bin/gen_wkt_types.rs | 7 + buffa-codegen/src/impl_message.rs | 5 +- buffa-codegen/src/view.rs | 2 +- .../generated/google.protobuf.any.__view.rs | 44 +++ .../google.protobuf.duration.__view.rs | 38 +++ .../generated/google.protobuf.empty.__view.rs | 22 ++ .../google.protobuf.field_mask.__view.rs | 33 +++ .../google.protobuf.struct.__view.rs | 201 +++++++++++++ .../google.protobuf.timestamp.__view.rs | 38 +++ .../google.protobuf.wrappers.__view.rs | 276 ++++++++++++++++++ 10 files changed, 662 insertions(+), 4 deletions(-) diff --git a/buffa-codegen/src/bin/gen_wkt_types.rs b/buffa-codegen/src/bin/gen_wkt_types.rs index cc5ca0a..3780275 100644 --- a/buffa-codegen/src/bin/gen_wkt_types.rs +++ b/buffa-codegen/src/bin/gen_wkt_types.rs @@ -92,6 +92,13 @@ fn main() { config.generate_arbitrary = true; config.generate_json = false; config.generate_text = true; + // view_encode = true WKTs are commonly nested inside + // application messages (Timestamp, Any, Struct/Value); a + // consumer that opts into ViewEncode needs the WKT view + // types to implement it for the nested-message dispatch to + // compile. The extra `__buffa_cached_size` field is 4 bytes + // per WKT view — negligible. + config.view_encode = true; config.emit_register_fn = false; // `Any.value` carries arbitrary encoded payloads that callers commonly // cache and clone into `repeated google.protobuf.Any` response fields. diff --git a/buffa-codegen/src/impl_message.rs b/buffa-codegen/src/impl_message.rs index 9b1e962..6e1ffb2 100644 --- a/buffa-codegen/src/impl_message.rs +++ b/buffa-codegen/src/impl_message.rs @@ -615,7 +615,7 @@ pub(crate) fn build_view_encode_methods( preserve_unknown_fields: bool, features: &ResolvedFeatures, oneof_idents: &std::collections::HashMap, - mod_ident: &proc_macro2::Ident, + view_oneof_prefix: &TokenStream, ) -> Result { let scalar_fields: Vec<_> = msg .field @@ -700,8 +700,7 @@ pub(crate) fn build_view_encode_methods( let mut oneof_write_stmts: Vec = Vec::new(); for (oneof_name, enum_ident, fields) in &oneof_groups { let field_ident = make_field_ident(oneof_name); - let view_enum = format_ident!("{}View", enum_ident); - let qualified: TokenStream = quote! { #mod_ident::#view_enum }; + let qualified: TokenStream = quote! { #view_oneof_prefix #enum_ident }; let mut size_arms: Vec = Vec::new(); let mut write_arms: Vec = Vec::new(); for field in fields { diff --git a/buffa-codegen/src/view.rs b/buffa-codegen/src/view.rs index 7122ce0..ab759e6 100644 --- a/buffa-codegen/src/view.rs +++ b/buffa-codegen/src/view.rs @@ -156,7 +156,7 @@ pub(crate) fn generate_view_with_nesting( ctx.config.preserve_unknown_fields, features, &oneof_idents, - &mod_ident, + &view_oneof_prefix, )?; ( quote! { diff --git a/buffa-types/src/generated/google.protobuf.any.__view.rs b/buffa-types/src/generated/google.protobuf.any.__view.rs index 799446b..873b6c3 100644 --- a/buffa-types/src/generated/google.protobuf.any.__view.rs +++ b/buffa-types/src/generated/google.protobuf.any.__view.rs @@ -136,6 +136,8 @@ pub struct AnyView<'a> { /// Field 2: `value` pub value: &'a [u8], pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> AnyView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -233,6 +235,48 @@ impl<'a> ::buffa::MessageView<'a> for AnyView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for AnyView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if !self.type_url.is_empty() { + size += 1u32 + ::buffa::types::string_encoded_len(&self.type_url) as u32; + } + if !self.value.is_empty() { + size += 1u32 + ::buffa::types::bytes_encoded_len(&self.value) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.type_url.is_empty() { + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::types::encode_string(&self.type_url, buf); + } + if !self.value.is_empty() { + ::buffa::encoding::Tag::new( + 2u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::types::encode_bytes(&self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for AnyView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); diff --git a/buffa-types/src/generated/google.protobuf.duration.__view.rs b/buffa-types/src/generated/google.protobuf.duration.__view.rs index af0b2e4..45bd0c4 100644 --- a/buffa-types/src/generated/google.protobuf.duration.__view.rs +++ b/buffa-types/src/generated/google.protobuf.duration.__view.rs @@ -83,6 +83,8 @@ pub struct DurationView<'a> { /// Field 2: `nanos` pub nanos: i32, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> DurationView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -180,6 +182,42 @@ impl<'a> ::buffa::MessageView<'a> for DurationView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for DurationView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.seconds != 0i64 { + size += 1u32 + ::buffa::types::int64_encoded_len(self.seconds) as u32; + } + if self.nanos != 0i32 { + size += 1u32 + ::buffa::types::int32_encoded_len(self.nanos) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.seconds != 0i64 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_int64(self.seconds, buf); + } + if self.nanos != 0i32 { + ::buffa::encoding::Tag::new(2u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_int32(self.nanos, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for DurationView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); diff --git a/buffa-types/src/generated/google.protobuf.empty.__view.rs b/buffa-types/src/generated/google.protobuf.empty.__view.rs index 433f85b..e66a66b 100644 --- a/buffa-types/src/generated/google.protobuf.empty.__view.rs +++ b/buffa-types/src/generated/google.protobuf.empty.__view.rs @@ -13,6 +13,8 @@ #[derive(Clone, Debug, Default)] pub struct EmptyView<'a> { pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> EmptyView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -88,6 +90,26 @@ impl<'a> ::buffa::MessageView<'a> for EmptyView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for EmptyView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for EmptyView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); diff --git a/buffa-types/src/generated/google.protobuf.field_mask.__view.rs b/buffa-types/src/generated/google.protobuf.field_mask.__view.rs index 61c1de1..df18170 100644 --- a/buffa-types/src/generated/google.protobuf.field_mask.__view.rs +++ b/buffa-types/src/generated/google.protobuf.field_mask.__view.rs @@ -231,6 +231,8 @@ pub struct FieldMaskView<'a> { /// Field 1: `paths` pub paths: ::buffa::RepeatedView<'a, &'a str>, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> FieldMaskView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -317,6 +319,37 @@ impl<'a> ::buffa::MessageView<'a> for FieldMaskView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for FieldMaskView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + for v in &self.paths { + size += 1u32 + ::buffa::types::string_encoded_len(v) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + for v in &self.paths { + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::types::encode_string(v, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for FieldMaskView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); diff --git a/buffa-types/src/generated/google.protobuf.struct.__view.rs b/buffa-types/src/generated/google.protobuf.struct.__view.rs index 0ac2db9..8c386d5 100644 --- a/buffa-types/src/generated/google.protobuf.struct.__view.rs +++ b/buffa-types/src/generated/google.protobuf.struct.__view.rs @@ -20,6 +20,8 @@ pub struct StructView<'a> { super::super::__buffa::view::ValueView<'a>, >, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> StructView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -157,6 +159,64 @@ impl<'a> ::buffa::MessageView<'a> for StructView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for StructView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + for (k, v) in &self.fields { + let entry_size: u32 = 1u32 + ::buffa::types::string_encoded_len(k) as u32 + + 1u32 + + { + let inner = v.compute_size(); + ::buffa::encoding::varint_len(inner as u64) as u32 + inner + }; + size + += 1u32 + ::buffa::encoding::varint_len(entry_size as u64) as u32 + + entry_size; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + for (k, v) in &self.fields { + let entry_size: u32 = 1u32 + ::buffa::types::string_encoded_len(k) as u32 + + 1u32 + + { + let inner = v.compute_size(); + ::buffa::encoding::varint_len(inner as u64) as u32 + inner + }; + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::encoding::encode_varint(entry_size as u64, buf); + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::types::encode_string(k, buf); + ::buffa::encoding::Tag::new( + 2u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::encoding::encode_varint(v.cached_size() as u64, buf); + v.write_to(buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for StructView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -178,6 +238,8 @@ pub struct ValueView<'a> { super::super::__buffa::view::oneof::value::Kind<'a>, >, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> ValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -404,6 +466,108 @@ impl<'a> ::buffa::MessageView<'a> for ValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for ValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if let ::core::option::Option::Some(ref v) = self.kind { + match v { + super::super::__buffa::view::oneof::value::Kind::NullValue(x) => { + size += 1u32 + ::buffa::types::int32_encoded_len(x.to_i32()) as u32; + } + super::super::__buffa::view::oneof::value::Kind::NumberValue(_x) => { + size += 1u32 + ::buffa::types::FIXED64_ENCODED_LEN as u32; + } + super::super::__buffa::view::oneof::value::Kind::StringValue(x) => { + size += 1u32 + ::buffa::types::string_encoded_len(x) as u32; + } + super::super::__buffa::view::oneof::value::Kind::BoolValue(_x) => { + size += 1u32 + ::buffa::types::BOOL_ENCODED_LEN as u32; + } + super::super::__buffa::view::oneof::value::Kind::StructValue(x) => { + let inner = x.compute_size(); + size + += 1u32 + ::buffa::encoding::varint_len(inner as u64) as u32 + + inner; + } + super::super::__buffa::view::oneof::value::Kind::ListValue(x) => { + let inner = x.compute_size(); + size + += 1u32 + ::buffa::encoding::varint_len(inner as u64) as u32 + + inner; + } + } + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if let ::core::option::Option::Some(ref v) = self.kind { + match v { + super::super::__buffa::view::oneof::value::Kind::NullValue(x) => { + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::Varint, + ) + .encode(buf); + ::buffa::types::encode_int32(x.to_i32(), buf); + } + super::super::__buffa::view::oneof::value::Kind::NumberValue(x) => { + ::buffa::encoding::Tag::new( + 2u32, + ::buffa::encoding::WireType::Fixed64, + ) + .encode(buf); + ::buffa::types::encode_double(*x, buf); + } + super::super::__buffa::view::oneof::value::Kind::StringValue(x) => { + ::buffa::encoding::Tag::new( + 3u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::types::encode_string(x, buf); + } + super::super::__buffa::view::oneof::value::Kind::BoolValue(x) => { + ::buffa::encoding::Tag::new( + 4u32, + ::buffa::encoding::WireType::Varint, + ) + .encode(buf); + ::buffa::types::encode_bool(*x, buf); + } + super::super::__buffa::view::oneof::value::Kind::StructValue(x) => { + ::buffa::encoding::Tag::new( + 5u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::encoding::encode_varint(x.cached_size() as u64, buf); + x.write_to(buf); + } + super::super::__buffa::view::oneof::value::Kind::ListValue(x) => { + ::buffa::encoding::Tag::new( + 6u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::encoding::encode_varint(x.cached_size() as u64, buf); + x.write_to(buf); + } + } + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for ValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -423,6 +587,8 @@ pub struct ListValueView<'a> { /// Field 1: `values` pub values: ::buffa::RepeatedView<'a, super::super::__buffa::view::ValueView<'a>>, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> ListValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -519,6 +685,41 @@ impl<'a> ::buffa::MessageView<'a> for ListValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for ListValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + for v in &self.values { + let inner_size = v.compute_size(); + size + += 1u32 + ::buffa::encoding::varint_len(inner_size as u64) as u32 + + inner_size; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + for v in &self.values { + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::encoding::encode_varint(v.cached_size() as u64, buf); + v.write_to(buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for ListValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); diff --git a/buffa-types/src/generated/google.protobuf.timestamp.__view.rs b/buffa-types/src/generated/google.protobuf.timestamp.__view.rs index 5422c4b..1d2bf12 100644 --- a/buffa-types/src/generated/google.protobuf.timestamp.__view.rs +++ b/buffa-types/src/generated/google.protobuf.timestamp.__view.rs @@ -118,6 +118,8 @@ pub struct TimestampView<'a> { /// Field 2: `nanos` pub nanos: i32, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> TimestampView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -215,6 +217,42 @@ impl<'a> ::buffa::MessageView<'a> for TimestampView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for TimestampView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.seconds != 0i64 { + size += 1u32 + ::buffa::types::int64_encoded_len(self.seconds) as u32; + } + if self.nanos != 0i32 { + size += 1u32 + ::buffa::types::int32_encoded_len(self.nanos) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.seconds != 0i64 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_int64(self.seconds, buf); + } + if self.nanos != 0i32 { + ::buffa::encoding::Tag::new(2u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_int32(self.nanos, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for TimestampView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); diff --git a/buffa-types/src/generated/google.protobuf.wrappers.__view.rs b/buffa-types/src/generated/google.protobuf.wrappers.__view.rs index 5b1f0d8..3465b58 100644 --- a/buffa-types/src/generated/google.protobuf.wrappers.__view.rs +++ b/buffa-types/src/generated/google.protobuf.wrappers.__view.rs @@ -11,6 +11,8 @@ pub struct DoubleValueView<'a> { /// Field 1: `value` pub value: f64, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> DoubleValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -97,6 +99,34 @@ impl<'a> ::buffa::MessageView<'a> for DoubleValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for DoubleValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.value.to_bits() != 0u64 { + size += 1u32 + ::buffa::types::FIXED64_ENCODED_LEN as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.value.to_bits() != 0u64 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Fixed64) + .encode(buf); + ::buffa::types::encode_double(self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for DoubleValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -116,6 +146,8 @@ pub struct FloatValueView<'a> { /// Field 1: `value` pub value: f32, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> FloatValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -202,6 +234,34 @@ impl<'a> ::buffa::MessageView<'a> for FloatValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for FloatValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.value.to_bits() != 0u32 { + size += 1u32 + ::buffa::types::FIXED32_ENCODED_LEN as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.value.to_bits() != 0u32 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Fixed32) + .encode(buf); + ::buffa::types::encode_float(self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for FloatValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -221,6 +281,8 @@ pub struct Int64ValueView<'a> { /// Field 1: `value` pub value: i64, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> Int64ValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -307,6 +369,34 @@ impl<'a> ::buffa::MessageView<'a> for Int64ValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for Int64ValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.value != 0i64 { + size += 1u32 + ::buffa::types::int64_encoded_len(self.value) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.value != 0i64 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_int64(self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for Int64ValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -326,6 +416,8 @@ pub struct UInt64ValueView<'a> { /// Field 1: `value` pub value: u64, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> UInt64ValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -412,6 +504,34 @@ impl<'a> ::buffa::MessageView<'a> for UInt64ValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for UInt64ValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.value != 0u64 { + size += 1u32 + ::buffa::types::uint64_encoded_len(self.value) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.value != 0u64 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_uint64(self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for UInt64ValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -431,6 +551,8 @@ pub struct Int32ValueView<'a> { /// Field 1: `value` pub value: i32, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> Int32ValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -517,6 +639,34 @@ impl<'a> ::buffa::MessageView<'a> for Int32ValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for Int32ValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.value != 0i32 { + size += 1u32 + ::buffa::types::int32_encoded_len(self.value) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.value != 0i32 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_int32(self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for Int32ValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -536,6 +686,8 @@ pub struct UInt32ValueView<'a> { /// Field 1: `value` pub value: u32, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> UInt32ValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -622,6 +774,34 @@ impl<'a> ::buffa::MessageView<'a> for UInt32ValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for UInt32ValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.value != 0u32 { + size += 1u32 + ::buffa::types::uint32_encoded_len(self.value) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.value != 0u32 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_uint32(self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for UInt32ValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -641,6 +821,8 @@ pub struct BoolValueView<'a> { /// Field 1: `value` pub value: bool, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> BoolValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -727,6 +909,34 @@ impl<'a> ::buffa::MessageView<'a> for BoolValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for BoolValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.value { + size += 1u32 + ::buffa::types::BOOL_ENCODED_LEN as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.value { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_bool(self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for BoolValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -746,6 +956,8 @@ pub struct StringValueView<'a> { /// Field 1: `value` pub value: &'a str, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> StringValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -832,6 +1044,37 @@ impl<'a> ::buffa::MessageView<'a> for StringValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for StringValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if !self.value.is_empty() { + size += 1u32 + ::buffa::types::string_encoded_len(&self.value) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.value.is_empty() { + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::types::encode_string(&self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for StringValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -851,6 +1094,8 @@ pub struct BytesValueView<'a> { /// Field 1: `value` pub value: &'a [u8], pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> BytesValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -937,6 +1182,37 @@ impl<'a> ::buffa::MessageView<'a> for BytesValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for BytesValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if !self.value.is_empty() { + size += 1u32 + ::buffa::types::bytes_encoded_len(&self.value) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.value.is_empty() { + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::types::encode_bytes(&self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for BytesValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); From b2171b2622baacb3c8356552eef49dd351baecac Mon Sep 17 00:00:00 2001 From: Ryan Brewster Date: Sat, 18 Apr 2026 18:49:16 +0000 Subject: [PATCH 04/13] buffa-test: ViewEncode round-trip + construct-from-borrows tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable view_encode(true) for basic.proto. Add Address home_address = 15 to Person's contact oneof to cover ViewEncode dispatch through Box>. New tests: round-trip (decode_view → encode_to_vec byte-equal), construct-from-borrows for scalars/repeated/map/oneof, compute_size matches encoded len. --- buffa-test/build.rs | 11 +- buffa-test/protos/basic.proto | 4 +- buffa-test/src/tests/view.rs | 220 ++++++++++++++++++++++++++++++++-- 3 files changed, 226 insertions(+), 9 deletions(-) diff --git a/buffa-test/build.rs b/buffa-test/build.rs index 9863dbd..09f40fe 100644 --- a/buffa-test/build.rs +++ b/buffa-test/build.rs @@ -4,6 +4,7 @@ fn main() { .files(&["protos/basic.proto"]) .includes(&["protos/"]) .generate_text(true) + .view_encode(true) .compile() .expect("buffa_build failed for basic.proto"); @@ -12,6 +13,7 @@ fn main() { buffa_build::Config::new() .files(&["protos/proto3_semantics.proto"]) .includes(&["protos/"]) + .view_encode(true) .compile() .expect("buffa_build failed for proto3_semantics.proto"); @@ -29,6 +31,7 @@ fn main() { buffa_build::Config::new() .files(&["protos/nested_deep.proto"]) .includes(&["protos/"]) + .view_encode(true) .compile() .expect("buffa_build failed for nested_deep.proto"); @@ -36,6 +39,7 @@ fn main() { buffa_build::Config::new() .files(&["protos/wkt_usage.proto"]) .includes(&["protos/"]) + .view_encode(true) .compile() .expect("buffa_build failed for wkt_usage.proto"); @@ -63,6 +67,7 @@ fn main() { .files(&["protos/proto2_defaults.proto"]) .includes(&["protos/"]) .generate_text(true) + .view_encode(true) .compile() .expect("buffa_build failed for proto2_defaults.proto"); @@ -114,6 +119,7 @@ fn main() { .includes(&["protos/"]) .strict_utf8_mapping(true) .generate_json(true) + .view_encode(true) .compile() .expect("buffa_build failed for utf8_validation.proto"); @@ -123,7 +129,8 @@ fn main() { .files(&["protos/edge_cases.proto"]) .includes(&["protos/"]) .generate_json(true) - .generate_views(false) + .generate_views(true) + .view_encode(true) .compile() .expect("buffa_build failed for edge_cases.proto"); @@ -144,6 +151,7 @@ fn main() { .includes(&["protos/"]) .use_bytes_type() .generate_json(true) + .view_encode(true) .out_dir(bytes_out) .compile() .expect("buffa_build failed for basic.proto with use_bytes_type"); @@ -159,6 +167,7 @@ fn main() { .files(&["protos/basic.proto"]) .includes(&["protos/"]) .preserve_unknown_fields(false) + .view_encode(true) .out_dir(no_uf_out) .compile() .expect("buffa_build failed for basic.proto with preserve_unknown_fields=false"); diff --git a/buffa-test/protos/basic.proto b/buffa-test/protos/basic.proto index 92bb543..d9b7ed3 100644 --- a/buffa-test/protos/basic.proto +++ b/buffa-test/protos/basic.proto @@ -40,10 +40,12 @@ message Person { optional int32 maybe_age = 11; optional string maybe_nickname = 12; - // Oneof field + // Oneof field — string and message variants. The message variant + // exercises ViewEncode dispatch through Box>. oneof contact { string email = 13; string phone = 14; + Address home_address = 15; } } diff --git a/buffa-test/src/tests/view.rs b/buffa-test/src/tests/view.rs index c23d87a..ef5e40e 100644 --- a/buffa-test/src/tests/view.rs +++ b/buffa-test/src/tests/view.rs @@ -4,7 +4,7 @@ use crate::basic::__buffa::oneof; use crate::basic::__buffa::view::*; use crate::basic::*; -use buffa::{Message, MessageView}; +use buffa::{Message, MessageView, ViewEncode}; #[test] fn test_view_decodes_scalar_fields() { @@ -260,13 +260,29 @@ fn test_view_map_empty() { #[test] fn test_compute_size_matches_encode_len() { - let mut msg = Person::default(); - msg.id = 99; - msg.name = "Bob".into(); - msg.tags = vec!["a".into(), "b".into()]; - let size = msg.compute_size() as usize; - let bytes = msg.encode_to_vec(); + // View type, not owned — covers nested-message, repeated-message, + // and oneof-message cached-size dispatch through MessageFieldView/Box. + let addr = AddressView { + street: "1 Main St", + city: "Springfield", + zip_code: 12345, + ..Default::default() + }; + let view = PersonView { + id: 99, + name: "Bob", + tags: ["a", "b"].iter().copied().collect(), + address: addr.clone().into(), + addresses: [addr.clone()].into_iter().collect(), + contact: Some(person::ContactOneofView::HomeAddress(Box::new(addr))), + ..Default::default() + }; + let size = view.compute_size() as usize; + let bytes = view.encode_to_vec(); assert_eq!(size, bytes.len()); + // Round-trips through owned. + let owned = Person::decode(&mut bytes.as_slice()).expect("decode owned"); + assert_eq!(owned.encode_to_vec().len(), size); } #[test] @@ -339,3 +355,193 @@ fn test_view_no_unknown_fields_all_scalar_compiles() { let owned = view.to_owned_message(); assert_eq!(owned.f_int32, 42); } + +// ── view encode ────────────────────────────────────────────────────────── + +#[test] +fn test_view_encode_roundtrip_address() { + let owned = Address { + street: "1 Infinite Loop".into(), + city: "Cupertino".into(), + zip_code: 95014, + ..Default::default() + }; + let owned_bytes = owned.encode_to_vec(); + let view = AddressView::decode_view(&owned_bytes).unwrap(); + let view_bytes = view.encode_to_vec(); + assert_eq!(owned_bytes, view_bytes); +} + +#[test] +fn test_view_encode_construct_from_borrows() { + let view = AddressView { + street: "borrowed st", + city: "ref town", + zip_code: 12345, + ..Default::default() + }; + let bytes = view.encode_to_vec(); + let decoded = Address::decode_from_slice(&bytes).unwrap(); + assert_eq!(decoded.street, "borrowed st"); + assert_eq!(decoded.city, "ref town"); + assert_eq!(decoded.zip_code, 12345); +} + +#[test] +fn test_view_encode_roundtrip_inventory_maps() { + let mut owned = Inventory::default(); + owned.stock.insert("widgets".into(), 7); + owned.stock.insert("gadgets".into(), 3); + owned.locations.insert( + "hq".into(), + Address { + street: "main".into(), + city: "metro".into(), + zip_code: 1, + ..Default::default() + }, + ); + owned.statuses.insert("hq".into(), Status::ACTIVE.into()); + let owned_bytes = owned.encode_to_vec(); + let view = InventoryView::decode_view(&owned_bytes).unwrap(); + let view_bytes = view.encode_to_vec(); + // Map ordering: HashMap iter order is unspecified, but a decoded + // MapView preserves wire order, so re-encoding the view of the + // owned encoding produces the SAME bytes. + assert_eq!(owned_bytes, view_bytes); +} + +#[test] +fn test_view_encode_construct_map_from_iter() { + let pairs = [("a", 1i32), ("b", 2)]; + let view = InventoryView { + stock: pairs.iter().copied().collect(), + ..Default::default() + }; + let bytes = view.encode_to_vec(); + let decoded = Inventory::decode_from_slice(&bytes).unwrap(); + assert_eq!(decoded.stock.get("a"), Some(&1)); + assert_eq!(decoded.stock.get("b"), Some(&2)); +} + +#[test] +fn test_view_encode_oneof_roundtrip() { + // String variant. + let mut owned = Person::default(); + owned.contact = Some(person::ContactOneof::Email("a@b.c".into())); + let owned_bytes = owned.encode_to_vec(); + let view = PersonView::decode_view(&owned_bytes).unwrap(); + assert_eq!(owned_bytes, view.encode_to_vec()); + + // Nested-message variant — exercises ViewEncode dispatch through Box. + let mut owned = Person::default(); + owned.contact = Some(person::ContactOneof::HomeAddress(Box::new(Address { + street: "1 main".into(), + city: "metro".into(), + zip_code: 9, + ..Default::default() + }))); + let owned_bytes = owned.encode_to_vec(); + let view = PersonView::decode_view(&owned_bytes).unwrap(); + assert_eq!(owned_bytes, view.encode_to_vec()); +} + +#[test] +fn test_view_encode_repeated_nested_and_oneof_from_borrows() { + use buffa::{MessageFieldView, RepeatedView}; + let addr = AddressView { + street: "x", + city: "y", + zip_code: 9, + ..Default::default() + }; + let view = PersonView { + tags: RepeatedView::new(vec!["t1", "t2"]), + lucky_numbers: RepeatedView::new(vec![7, 11]), + address: MessageFieldView::set(addr.clone()), + addresses: RepeatedView::new(vec![addr.clone()]), + contact: Some(person::ContactOneofView::HomeAddress(Box::new(addr))), + ..Default::default() + }; + let bytes = view.encode_to_vec(); + let decoded = Person::decode_from_slice(&bytes).unwrap(); + assert_eq!(decoded.tags, ["t1", "t2"]); + assert_eq!(decoded.lucky_numbers, [7, 11]); + assert_eq!(decoded.address.street, "x"); + assert_eq!(decoded.addresses[0].zip_code, 9); + let Some(person::ContactOneof::HomeAddress(a)) = &decoded.contact else { + panic!("expected HomeAddress variant") + }; + assert_eq!(a.city, "y"); +} + +#[test] +fn test_view_encode_compute_size_matches_len() { + let owned = Address { + street: "1 Infinite Loop".into(), + city: "Cupertino".into(), + zip_code: 95014, + ..Default::default() + }; + let bytes = owned.encode_to_vec(); + let view = AddressView::decode_view(&bytes).unwrap(); + assert_eq!(view.compute_size() as usize, view.encode_to_vec().len()); +} + +#[test] +fn test_view_encode_proto2_groups_roundtrip() { + use crate::proto2::{with_groups, WithGroups, WithGroupsView}; + let owned = WithGroups { + mygroup: with_groups::MyGroup { + a: Some(7), + b: Some("inner".into()), + ..Default::default() + } + .into(), + item: vec![ + with_groups::Item { + id: Some(1), + name: Some("first".into()), + ..Default::default() + }, + with_groups::Item { + id: Some(2), + name: Some("second".into()), + ..Default::default() + }, + ], + label: Some("alongside".into()), + ..Default::default() + }; + let owned_bytes = owned.encode_to_vec(); + let view = WithGroupsView::decode_view(&owned_bytes).unwrap(); + let view_bytes = view.encode_to_vec(); + assert_eq!(owned_bytes, view_bytes); + assert_eq!(view.compute_size() as usize, view_bytes.len()); +} + +#[test] +fn test_view_encode_proto2_oneof_group_closed_enum_roundtrip() { + use crate::proto2::{view_coverage, Priority, ViewCoverage, ViewCoverageView}; + let mut owned = ViewCoverage { + level: Priority::HIGH, + choice: Some(view_coverage::ChoiceOneof::Payload(Box::new( + view_coverage::Payload { + x: Some(42), + y: Some("oneof-group".into()), + ..Default::default() + }, + ))), + ..Default::default() + }; + owned.by_id.insert(10, "ten".into()); + owned.priorities.insert("k".into(), Priority::LOW); + let owned_bytes = owned.encode_to_vec(); + let view = ViewCoverageView::decode_view(&owned_bytes).unwrap(); + let view_bytes = view.encode_to_vec(); + assert_eq!(owned_bytes, view_bytes); + // Re-decode through owned to confirm semantic equality, not just byte + // identity (which here happens to hold because each map has 1 entry). + let redecoded = ViewCoverage::decode_from_slice(&view_bytes).unwrap(); + assert_eq!(redecoded, owned); +} From 64f13ed038e53c71ed92198f28f696ec3ab30c77 Mon Sep 17 00:00:00 2001 From: Ryan Brewster Date: Sat, 18 Apr 2026 18:49:16 +0000 Subject: [PATCH 05/13] benchmarks: encode_view + build_encode comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit encode_view: serialize a pre-decoded view (parity with owned encode; wire-compat asserted by decode-and-compare). build_encode vs build_encode_view: construct a string+map LogRecord from borrowed source data and encode. Includes the per-field String allocs that the view path avoids — 1.35 µs → 227 ns (5.96×) on a 15-label fixture. --- benchmarks/buffa/benches/protobuf.rs | 127 ++++++++++++++++++++++++++- benchmarks/buffa/build.rs | 1 + 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/benchmarks/buffa/benches/protobuf.rs b/benchmarks/buffa/benches/protobuf.rs index 617020e..4a683c7 100644 --- a/benchmarks/buffa/benches/protobuf.rs +++ b/benchmarks/buffa/benches/protobuf.rs @@ -1,4 +1,4 @@ -use buffa::{Message, MessageView}; +use buffa::{Message, MessageView, ViewEncode}; use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use serde::{de::DeserializeOwned, Serialize}; @@ -209,6 +209,126 @@ fn bench_media_frame_view(c: &mut Criterion) { group.finish(); } +/// Add `encode_view` to a concrete per-dataset bench group: pre-decode +/// payloads into views, assert wire-compat against owned decode, then bench +/// re-encoding from the views' borrowed fields. The owned `encode` baseline +/// is in [`benchmark_decode`] — same group name, so throughputs sit side by +/// side. +/// +/// Per-dataset functions are concrete (not generic over `V`) because the +/// views borrow from the locally-decoded `dataset.payload`; a `<'a, V>` fn +/// signature can't tie `'a` to a local. Same shape as `decode_view` above. +macro_rules! bench_view_encode { + ($fn_name:ident, $owned:ty, $view:ty, $group:literal, $dataset:literal) => { + fn $fn_name(c: &mut Criterion) { + let dataset = load_dataset(include_bytes!($dataset)); + let bytes = total_payload_bytes(&dataset); + let views: Vec<$view> = dataset + .payload + .iter() + .map(|p| <$view>::decode_view(p).unwrap()) + .collect(); + for (v, p) in views.iter().zip(&dataset.payload) { + let from_view = <$owned>::decode_from_slice(&v.encode_to_vec()).unwrap(); + let from_wire = <$owned>::decode_from_slice(p).unwrap(); + assert!(from_view == from_wire, "view-encode wire mismatch"); + } + let mut group = c.benchmark_group($group); + group.throughput(Throughput::Bytes(bytes)); + group.bench_function("encode_view", |b| { + b.iter(|| { + for v in &views { + criterion::black_box(v.encode_to_vec()); + } + }); + }); + group.finish(); + } + }; +} + +bench_view_encode!( + bench_api_response_view_encode, + ApiResponse, + ApiResponseView, + "buffa/api_response", + "../../datasets/api_response.pb" +); +bench_view_encode!( + bench_log_record_view_encode, + LogRecord, + LogRecordView, + "buffa/log_record", + "../../datasets/log_record.pb" +); +bench_view_encode!( + bench_analytics_event_view_encode, + AnalyticsEvent, + AnalyticsEventView, + "buffa/analytics_event", + "../../datasets/analytics_event.pb" +); +bench_view_encode!( + bench_google_message1_view_encode, + bench_buffa::proto3::GoogleMessage1, + bench_buffa::proto3::GoogleMessage1View, + "buffa/google_message1_proto3", + "../../datasets/google_message1_proto3.pb" +); + +/// Build a `LogRecord` from borrowed source data and encode, vs build a +/// `LogRecordView` from the same borrows and encode. Unlike `encode` / +/// `encode_view` above (which serialize a pre-built struct), this includes +/// the build phase — the per-field `String`/`HashMap` allocs that the view +/// path avoids. +fn bench_log_record_build_encode(c: &mut Criterion) { + let labels: Vec<(&str, &str)> = (0..15) + .map(|i| { + ( + Box::leak(format!("k8s.io/label-key-{i:02}").into_boxed_str()) as &str, + Box::leak(format!("label-value-{i:04}").into_boxed_str()) as &str, + ) + }) + .collect(); + let service = "inventory-service-2a"; + let msg = "GET /api/v1/items?tenant=acme-corp&warehouse=us-west-2a&page=1400 200 17ms"; + let mut group = c.benchmark_group("buffa/log_record"); + let probe = LogRecord { + service_name: service.into(), + message: msg.into(), + labels: labels.iter().map(|(k, v)| ((*k).into(), (*v).into())).collect(), + ..Default::default() + } + .encode_to_vec(); + group.throughput(Throughput::Bytes(probe.len() as u64)); + + group.bench_function("build_encode", |b| { + b.iter(|| { + let owned = LogRecord { + service_name: service.into(), + message: msg.into(), + labels: labels.iter().map(|(k, v)| ((*k).into(), (*v).into())).collect(), + ..Default::default() + }; + criterion::black_box(owned.encode_to_vec()) + }); + }); + + group.bench_function("build_encode_view", |b| { + b.iter(|| { + let view = LogRecordView { + service_name: service, + message: msg, + labels: labels.iter().copied().collect(), + ..Default::default() + }; + criterion::black_box(view.encode_to_vec()) + }); + }); + + group.finish(); +} + fn bench_api_response(c: &mut Criterion) { benchmark_decode::( c, @@ -305,6 +425,11 @@ criterion_group!( bench_analytics_event_view, bench_google_message1_view, bench_media_frame_view, + bench_api_response_view_encode, + bench_log_record_view_encode, + bench_analytics_event_view_encode, + bench_google_message1_view_encode, + bench_log_record_build_encode, ); criterion_group!( diff --git a/benchmarks/buffa/build.rs b/benchmarks/buffa/build.rs index 6198800..844a693 100644 --- a/benchmarks/buffa/build.rs +++ b/benchmarks/buffa/build.rs @@ -7,6 +7,7 @@ fn main() { ]) .includes(&["../proto/"]) .generate_json(true) + .view_encode(true) .compile() .expect("failed to compile benchmark protos"); } From 4c28f277c636500457d9708f2475adf576b1de83 Mon Sep 17 00:00:00 2001 From: Ryan Brewster Date: Sat, 18 Apr 2026 20:22:39 +0000 Subject: [PATCH 06/13] benchmarks: add buffa(view) to encode charts + build-encode chart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit binary-encode.svg gains a "buffa (view)" series (encode_view, all 4 messages). New build-encode.svg shows build_encode vs build_encode_view for LogRecord — the alloc-elimination case (522 -> 3011 MiB/s, 5.77x). generate.py: messages_with_data() so per-chart message lists auto-shrink to those with data (build-encode is LogRecord-only); skip all-None series. Regenerated from native buffa/prost cargo bench + Docker google/Go. --- benchmarks/charts/build-encode.svg | 35 ++++++++++++++++++++++++++++++ benchmarks/charts/generate.py | 14 +++++++++++- benchmarks/charts/tables.md | 6 +++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 benchmarks/charts/build-encode.svg diff --git a/benchmarks/charts/build-encode.svg b/benchmarks/charts/build-encode.svg new file mode 100644 index 0000000..b229eb9 --- /dev/null +++ b/benchmarks/charts/build-encode.svg @@ -0,0 +1,35 @@ + + + + Build + Binary Encode Throughput (from borrowed source data) + + buffa + + buffa (view) + + 0 + + 800 + + 1.6k + + 2.4k + + 3.2k + + 4.0k + MiB/s + LogRecord + + 522 + + 3,011 + diff --git a/benchmarks/charts/generate.py b/benchmarks/charts/generate.py index 6b9da6c..5dbcdc3 100644 --- a/benchmarks/charts/generate.py +++ b/benchmarks/charts/generate.py @@ -188,11 +188,16 @@ def build_tables( ]), ("binary-encode", [ ("buffa", lambda ms, md: _get(buffa, "buffa", ms, "encode")), + ("buffa (view)", lambda ms, md: _get(buffa, "buffa", ms, "encode_view")), ("prost", lambda ms, md: _get(prost, "prost", ms, "encode")), ("prost (bytes)", lambda ms, md: _get(prost_bytes, "prost-bytes", ms, "encode")), ("protobuf-v4", lambda ms, md: _get(google, "google", ms, "encode")), ("Go", lambda ms, md: _get_go(go, "BinaryEncode", md)), ]), + ("build-encode", [ + ("buffa", lambda ms, md: _get(buffa, "buffa", ms, "build_encode")), + ("buffa (view)", lambda ms, md: _get(buffa, "buffa", ms, "build_encode_view")), + ]), ("json-encode", [ ("buffa", lambda ms, md: _get(buffa, "buffa", ms, "json_encode")), ("prost", lambda ms, md: _get(prost, "prost", ms, "json_encode")), @@ -215,6 +220,11 @@ def build_tables( return tables +def messages_with_data(table: dict[str, dict[str, float | None]]) -> list[str]: + """Subset of MESSAGES that have at least one non-None value in this table.""" + return [m for m in MESSAGES if any(table[s].get(m) for s in table)] + + # ── SVG chart generation ─────────────────────────────────────────────── @dataclass @@ -366,6 +376,7 @@ def generate_readme_tables(tables: dict[str, dict[str, dict[str, float | None]]] chart_meta = { "binary-decode": ("Binary decode", "buffa"), "binary-encode": ("Binary encode", "buffa"), + "build-encode": ("Build + binary encode", "buffa"), "json-encode": ("JSON encode", "buffa"), "json-decode": ("JSON decode", "buffa"), } @@ -377,7 +388,7 @@ def generate_readme_tables(tables: dict[str, dict[str, dict[str, float | None]]] sep = "|---------|" + "|".join("------:" for _ in series_names) + "|" rows: list[str] = [] - for msg in MESSAGES: + for msg in messages_with_data(table): baseline = table[baseline_name].get(msg) cells = [_pct(table[s].get(msg), baseline) for s in series_names] rows.append(f"| {msg} | " + " | ".join(cells) + " |") @@ -429,6 +440,7 @@ def load_criterion(name: str) -> dict[str, float]: chart_titles = { "binary-decode": "Binary Decode Throughput", "binary-encode": "Binary Encode Throughput", + "build-encode": "Build + Binary Encode Throughput (from borrowed source data)", "json-encode": "JSON Encode Throughput", "json-decode": "JSON Decode Throughput", } diff --git a/benchmarks/charts/tables.md b/benchmarks/charts/tables.md index 38c110c..3712d8b 100644 --- a/benchmarks/charts/tables.md +++ b/benchmarks/charts/tables.md @@ -18,6 +18,12 @@ | GoogleMessage1 | 2,594 | 1,808 (−30%) | — | 869 (−67%) | 360 (−86%) | | MediaFrame | 45,990 | 38,514 (−16%) | — | 10,463 (−77%) | 1,647 (−96%) | +### Build + binary encode + +| Message | buffa | buffa (view) | +|---------|------:|------:| +| LogRecord | 522 | 3,012 (+477%) | + ### JSON encode | Message | buffa | prost | Go | From a3db906d54dad37605e993740007633f1044a9cd Mon Sep 17 00:00:00 2001 From: Ryan Brewster Date: Sat, 18 Apr 2026 21:25:47 +0000 Subject: [PATCH 07/13] address review feedback: classify_fields, all-4 build_encode, restored owned test, doc notes - impl_message.rs: extract classify_fields() shared between generate_message_impl and build_view_encode_methods so a new field category only needs adding once. Add doc-notes on MessageSet (verbatim spans need no Item-group rewrap) and MapView/HashMap match-ergonomics. - benches/protobuf.rs: bench_build_encode! macro + per-type fixtures for all 4 message types (was log_record only). Spectrum: GoogleMessage1 1.34x to LogRecord 5.80x; view never slower. - buffa-test/src/tests/view.rs: restore owned test_compute_size_matches_encode_len alongside the new _view variant. - charts/: regenerate (build-encode.svg now shows all 4). --- benchmarks/buffa/benches/protobuf.rs | 227 +++++++++++++++++++++------ benchmarks/charts/build-encode.svg | 53 ++++--- benchmarks/charts/tables.md | 5 +- buffa-codegen/src/impl_message.rs | 172 +++++++++----------- buffa-test/src/tests/view.rs | 15 +- 5 files changed, 308 insertions(+), 164 deletions(-) diff --git a/benchmarks/buffa/benches/protobuf.rs b/benchmarks/buffa/benches/protobuf.rs index 4a683c7..b7f9de0 100644 --- a/benchmarks/buffa/benches/protobuf.rs +++ b/benchmarks/buffa/benches/protobuf.rs @@ -276,58 +276,188 @@ bench_view_encode!( "../../datasets/google_message1_proto3.pb" ); -/// Build a `LogRecord` from borrowed source data and encode, vs build a -/// `LogRecordView` from the same borrows and encode. Unlike `encode` / -/// `encode_view` above (which serialize a pre-built struct), this includes -/// the build phase — the per-field `String`/`HashMap` allocs that the view -/// path avoids. -fn bench_log_record_build_encode(c: &mut Criterion) { - let labels: Vec<(&str, &str)> = (0..15) - .map(|i| { - ( - Box::leak(format!("k8s.io/label-key-{i:02}").into_boxed_str()) as &str, - Box::leak(format!("label-value-{i:04}").into_boxed_str()) as &str, - ) - }) - .collect(); - let service = "inventory-service-2a"; - let msg = "GET /api/v1/items?tenant=acme-corp&warehouse=us-west-2a&page=1400 200 17ms"; - let mut group = c.benchmark_group("buffa/log_record"); - let probe = LogRecord { - service_name: service.into(), - message: msg.into(), - labels: labels.iter().map(|(k, v)| ((*k).into(), (*v).into())).collect(), +/// Build-then-encode benches: unlike `encode`/`encode_view` (which serialize +/// a pre-built struct), these include the cost of populating the message from +/// borrowed source — the per-field `String`/`Vec`/`HashMap` allocs that the +/// view path avoids. Each uses a synthetic fixture representative of the +/// message's shape; both paths populate identical fields, throughput is the +/// encoded length. +/// +/// `bench_build_encode!(fn_name, group, OwnedTy, owned_expr, view_expr)` — +/// the two exprs share the source bindings declared above the macro call. +/// Asserts decode-equivalence (not byte-equality, since `HashMap` vs +/// `MapView` iteration order may differ on the wire). +macro_rules! bench_build_encode { + ($fn_name:ident, $group:literal, $owned_ty:ty, $owned:expr, $view:expr $(,)?) => { + fn $fn_name(c: &mut Criterion) { + let probe = ($owned).encode_to_vec(); + let view_bytes = ($view).encode_to_vec(); + assert_eq!(probe.len(), view_bytes.len(), "fixture encode-len mismatch"); + assert_eq!( + <$owned_ty>::decode_from_slice(&probe).unwrap(), + <$owned_ty>::decode_from_slice(&view_bytes).unwrap(), + "owned/view fixtures must decode-equal" + ); + let mut group = c.benchmark_group($group); + group.throughput(Throughput::Bytes(probe.len() as u64)); + group.bench_function("build_encode", |b| { + b.iter(|| criterion::black_box(($owned).encode_to_vec())); + }); + group.bench_function("build_encode_view", |b| { + b.iter(|| criterion::black_box(($view).encode_to_vec())); + }); + group.finish(); + } + }; +} + +const TAGS: [&str; 5] = ["payments", "us-west-2a", "canary", "v3.14.2", "k8s"]; + +bench_build_encode!( + bench_api_response_build_encode, + "buffa/api_response", + ApiResponse, + ApiResponse { + request_id: 9_001_234_567_890, + status_code: 200, + message: "transaction accepted".into(), + latency_ms: 17.42, + cached: true, + trace_id: Some("4bf92f3577b34da6a3ce929d0e0e4736".into()), + retry_after_ms: None, + tags: TAGS.iter().map(|s| (*s).into()).collect(), ..Default::default() - } - .encode_to_vec(); - group.throughput(Throughput::Bytes(probe.len() as u64)); + }, + ApiResponseView { + request_id: 9_001_234_567_890, + status_code: 200, + message: "transaction accepted", + latency_ms: 17.42, + cached: true, + trace_id: Some("4bf92f3577b34da6a3ce929d0e0e4736"), + retry_after_ms: None, + tags: TAGS.iter().copied().collect(), + ..Default::default() + }, +); - group.bench_function("build_encode", |b| { - b.iter(|| { - let owned = LogRecord { - service_name: service.into(), - message: msg.into(), - labels: labels.iter().map(|(k, v)| ((*k).into(), (*v).into())).collect(), - ..Default::default() - }; - criterion::black_box(owned.encode_to_vec()) - }); - }); +const LABELS: [(&str, &str); 15] = [ + ("k8s.io/label-key-00", "label-value-0000"), + ("k8s.io/label-key-01", "label-value-0001"), + ("k8s.io/label-key-02", "label-value-0002"), + ("k8s.io/label-key-03", "label-value-0003"), + ("k8s.io/label-key-04", "label-value-0004"), + ("k8s.io/label-key-05", "label-value-0005"), + ("k8s.io/label-key-06", "label-value-0006"), + ("k8s.io/label-key-07", "label-value-0007"), + ("k8s.io/label-key-08", "label-value-0008"), + ("k8s.io/label-key-09", "label-value-0009"), + ("k8s.io/label-key-10", "label-value-0010"), + ("k8s.io/label-key-11", "label-value-0011"), + ("k8s.io/label-key-12", "label-value-0012"), + ("k8s.io/label-key-13", "label-value-0013"), + ("k8s.io/label-key-14", "label-value-0014"), +]; +const LOG_SVC: &str = "inventory-service-2a"; +const LOG_MSG: &str = "GET /api/v1/items?tenant=acme-corp&warehouse=us-west-2a&page=1400 200 17ms"; + +bench_build_encode!( + bench_log_record_build_encode, + "buffa/log_record", + LogRecord, + LogRecord { + service_name: LOG_SVC.into(), + message: LOG_MSG.into(), + labels: LABELS.iter().map(|(k, v)| ((*k).into(), (*v).into())).collect(), + ..Default::default() + }, + LogRecordView { + service_name: LOG_SVC, + message: LOG_MSG, + labels: LABELS.iter().copied().collect(), + ..Default::default() + }, +); - group.bench_function("build_encode_view", |b| { - b.iter(|| { - let view = LogRecordView { - service_name: service, - message: msg, - labels: labels.iter().copied().collect(), +const PROPS: [(&str, &str); 8] = [ + ("page", "/checkout/confirm"), + ("referrer", "https://example.com/cart"), + ("session", "f0e1d2c3b4a59687"), + ("variant", "control"), + ("locale", "en-US"), + ("device", "desktop"), + ("browser", "firefox-125"), + ("ab_bucket", "treatment-7"), +]; + +// `sections` (recursive Nested) omitted: building nested views means a +// `Box` per child — that conflates the alloc-avoidance signal +// with the documented `MessageFieldView` boxing follow-up. +bench_build_encode!( + bench_analytics_event_build_encode, + "buffa/analytics_event", + AnalyticsEvent, + AnalyticsEvent { + event_id: "evt_01HW3K9QXAMPLE".into(), + timestamp: 1_700_000_000_000, + user_id: "usr_8f7e6d5c4b3a2910".into(), + properties: PROPS + .iter() + .map(|(k, v)| analytics_event::Property { + key: (*k).into(), + value: Some(analytics_event::property::ValueOneof::StringValue((*v).into())), ..Default::default() - }; - criterion::black_box(view.encode_to_vec()) - }); - }); + }) + .collect(), + ..Default::default() + }, + AnalyticsEventView { + event_id: "evt_01HW3K9QXAMPLE", + timestamp: 1_700_000_000_000, + user_id: "usr_8f7e6d5c4b3a2910", + properties: PROPS + .iter() + .map(|(k, v)| analytics_event::PropertyView { + key: k, + value: Some(analytics_event::property::ValueOneofView::StringValue(v)), + ..Default::default() + }) + .collect(), + ..Default::default() + }, +); - group.finish(); -} +bench_build_encode!( + bench_google_message1_build_encode, + "buffa/google_message1_proto3", + bench_buffa::proto3::GoogleMessage1, + bench_buffa::proto3::GoogleMessage1 { + field1: "the quick brown fox".into(), + field9: "jumps over the lazy dog".into(), + field2: 42, + field3: 17, + field6: 9001, + field22: 1_234_567_890_123, + field12: true, + field14: true, + field100: 100, + field101: 101, + ..Default::default() + }, + bench_buffa::proto3::GoogleMessage1View { + field1: "the quick brown fox", + field9: "jumps over the lazy dog", + field2: 42, + field3: 17, + field6: 9001, + field22: 1_234_567_890_123, + field12: true, + field14: true, + field100: 100, + field101: 101, + ..Default::default() + }, +); fn bench_api_response(c: &mut Criterion) { benchmark_decode::( @@ -429,7 +559,10 @@ criterion_group!( bench_log_record_view_encode, bench_analytics_event_view_encode, bench_google_message1_view_encode, + bench_api_response_build_encode, bench_log_record_build_encode, + bench_analytics_event_build_encode, + bench_google_message1_build_encode, ); criterion_group!( diff --git a/benchmarks/charts/build-encode.svg b/benchmarks/charts/build-encode.svg index b229eb9..04a9d9f 100644 --- a/benchmarks/charts/build-encode.svg +++ b/benchmarks/charts/build-encode.svg @@ -1,4 +1,4 @@ - + diff --git a/benchmarks/charts/tables.md b/benchmarks/charts/tables.md index 3712d8b..bc3ed34 100644 --- a/benchmarks/charts/tables.md +++ b/benchmarks/charts/tables.md @@ -22,7 +22,10 @@ | Message | buffa | buffa (view) | |---------|------:|------:| -| LogRecord | 522 | 3,012 (+477%) | +| ApiResponse | 790 | 1,799 (+128%) | +| LogRecord | 531 | 3,080 (+480%) | +| AnalyticsEvent | 408 | 1,178 (+189%) | +| GoogleMessage1 | 935 | 1,251 (+34%) | ### JSON encode diff --git a/buffa-codegen/src/impl_message.rs b/buffa-codegen/src/impl_message.rs index 6e1ffb2..270a9b3 100644 --- a/buffa-codegen/src/impl_message.rs +++ b/buffa-codegen/src/impl_message.rs @@ -214,43 +214,30 @@ pub(crate) fn closed_enum_unknown_route( } } -/// Generate `unsafe impl DefaultInstance` and `impl Message` for a message. -/// -/// `preserve_unknown_fields`: when `true`, the generated merge collects -/// unknown fields into `self.__buffa_unknown_fields` and both `compute_size` and -/// `write_to` include them. -#[allow(clippy::too_many_arguments)] -pub fn generate_message_impl( - ctx: &CodeGenContext, - msg: &DescriptorProto, - preserve_unknown_fields: bool, - rust_name: &str, - current_package: &str, - proto_fqn: &str, - features: &ResolvedFeatures, - oneof_idents: &std::collections::HashMap, - oneof_prefix: &TokenStream, - nesting: usize, -) -> Result { - let name_ident = format_ident!("{}", rust_name); +/// Partition a message's fields by encode-dispatch shape. Shared between +/// [`generate_message_impl`] (owned) and [`build_view_encode_methods`] (view) +/// so a new field category only needs adding here. +struct ClassifiedFields<'a> { + scalar: Vec<&'a FieldDescriptorProto>, + repeated: Vec<&'a FieldDescriptorProto>, + map: Vec<&'a FieldDescriptorProto>, + oneof_groups: Vec<(String, proc_macro2::Ident, Vec<&'a FieldDescriptorProto>)>, +} - let scalar_fields: Vec<_> = msg +fn classify_fields<'a>( + msg: &'a DescriptorProto, + oneof_idents: &std::collections::HashMap, +) -> ClassifiedFields<'a> { + let scalar = msg .field .iter() .filter(|f| { - // Real oneof members are excluded (handled via the oneof enum). - if is_real_oneof_member(f) { - return false; - } - if f.label.unwrap_or_default() == Label::LABEL_REPEATED { - return false; - } - is_supported_field_type(f.r#type.unwrap_or_default()) + !is_real_oneof_member(f) + && f.label.unwrap_or_default() != Label::LABEL_REPEATED + && is_supported_field_type(f.r#type.unwrap_or_default()) }) .collect(); - - // Repeated fields (excluding map entries, which are handled separately). - let repeated_fields: Vec<_> = msg + let repeated = msg .field .iter() .filter(|f| { @@ -259,9 +246,15 @@ pub fn generate_message_impl( && is_supported_field_type(f.r#type.unwrap_or_default()) }) .collect(); - - // Non-synthetic oneof groups: each entry is (oneof_name, enum_ident, fields[]). - let oneof_groups: Vec<(String, proc_macro2::Ident, Vec<&FieldDescriptorProto>)> = msg + let map = msg + .field + .iter() + .filter(|f| { + f.label.unwrap_or_default() == Label::LABEL_REPEATED + && crate::message::is_map_field(msg, f) + }) + .collect(); + let oneof_groups = msg .oneof_decl .iter() .enumerate() @@ -282,6 +275,40 @@ pub fn generate_message_impl( )) }) .collect(); + ClassifiedFields { + scalar, + repeated, + map, + oneof_groups, + } +} + +/// Generate `unsafe impl DefaultInstance` and `impl Message` for a message. +/// +/// `preserve_unknown_fields`: when `true`, the generated merge collects +/// unknown fields into `self.__buffa_unknown_fields` and both `compute_size` and +/// `write_to` include them. +#[allow(clippy::too_many_arguments)] +pub fn generate_message_impl( + ctx: &CodeGenContext, + msg: &DescriptorProto, + preserve_unknown_fields: bool, + rust_name: &str, + current_package: &str, + proto_fqn: &str, + features: &ResolvedFeatures, + oneof_idents: &std::collections::HashMap, + oneof_prefix: &TokenStream, + nesting: usize, +) -> Result { + let name_ident = format_ident!("{}", rust_name); + + let ClassifiedFields { + scalar: scalar_fields, + repeated: repeated_fields, + map: map_fields, + oneof_groups, + } = classify_fields(msg, oneof_idents); let compute_stmts = scalar_fields .iter() @@ -336,16 +363,6 @@ pub fn generate_message_impl( oneof_merge_arms.extend(mas); } - // Map fields. - let map_fields: Vec<_> = msg - .field - .iter() - .filter(|f| { - f.label.unwrap_or_default() == Label::LABEL_REPEATED - && crate::message::is_map_field(msg, f) - }) - .collect(); - let mut map_compute_stmts: Vec = Vec::new(); let mut map_write_stmts: Vec = Vec::new(); let mut map_merge_arms: Vec = Vec::new(); @@ -617,57 +634,12 @@ pub(crate) fn build_view_encode_methods( oneof_idents: &std::collections::HashMap, view_oneof_prefix: &TokenStream, ) -> Result { - let scalar_fields: Vec<_> = msg - .field - .iter() - .filter(|f| { - if is_real_oneof_member(f) { - return false; - } - if f.label.unwrap_or_default() == Label::LABEL_REPEATED { - return false; - } - is_supported_field_type(f.r#type.unwrap_or_default()) - }) - .collect(); - let repeated_fields: Vec<_> = msg - .field - .iter() - .filter(|f| { - f.label.unwrap_or_default() == Label::LABEL_REPEATED - && !crate::message::is_map_field(msg, f) - && is_supported_field_type(f.r#type.unwrap_or_default()) - }) - .collect(); - let oneof_groups: Vec<(String, proc_macro2::Ident, Vec<&FieldDescriptorProto>)> = msg - .oneof_decl - .iter() - .enumerate() - .filter_map(|(idx, oneof)| { - let enum_ident = oneof_idents.get(&idx)?; - let fields: Vec<_> = msg - .field - .iter() - .filter(|f| is_real_oneof_member(f) && f.oneof_index == Some(idx as i32)) - .collect(); - if fields.is_empty() { - return None; - } - Some(( - oneof.name.as_deref()?.to_string(), - enum_ident.clone(), - fields, - )) - }) - .collect(); - let map_fields: Vec<_> = msg - .field - .iter() - .filter(|f| { - f.label.unwrap_or_default() == Label::LABEL_REPEATED - && crate::message::is_map_field(msg, f) - }) - .collect(); + let ClassifiedFields { + scalar: scalar_fields, + repeated: repeated_fields, + map: map_fields, + oneof_groups, + } = classify_fields(msg, oneof_idents); let compute_stmts = scalar_fields .iter() @@ -735,6 +707,11 @@ pub(crate) fn build_view_encode_methods( }); } + // map_{compute_size,write_to}_stmt emit `for (k, v) in &self.field { ... }`. + // For owned `&HashMap` that yields `(&K, &V)` directly. For + // `&MapView<'_,K,V>` it yields `&(K,V)`, but match-ergonomics binds the + // pattern `(k, v)` to `(&K, &V)` either way — so the same generated body + // works on both without modification. let mut map_compute_stmts: Vec = Vec::new(); let mut map_write_stmts: Vec = Vec::new(); for f in &map_fields { @@ -747,6 +724,11 @@ pub(crate) fn build_view_encode_methods( } else { quote! {} }; + // MessageSet (option message_set_wire_format = true) needs no special + // handling here: `UnknownFieldsView` stores raw verbatim wire spans, so the + // Item-group framing is already in the bytes and `write_to` is a passthrough. + // The owned path (see `generate_message_impl`) re-wraps because owned + // `UnknownFields` stores parsed `(number, data)` pairs. let unknown_fields_write_stmt = if preserve_unknown_fields { quote! { self.__buffa_unknown_fields.write_to(buf); } } else { diff --git a/buffa-test/src/tests/view.rs b/buffa-test/src/tests/view.rs index ef5e40e..eea1d75 100644 --- a/buffa-test/src/tests/view.rs +++ b/buffa-test/src/tests/view.rs @@ -260,8 +260,19 @@ fn test_view_map_empty() { #[test] fn test_compute_size_matches_encode_len() { - // View type, not owned — covers nested-message, repeated-message, - // and oneof-message cached-size dispatch through MessageFieldView/Box. + let mut msg = Person::default(); + msg.id = 99; + msg.name = "Bob".into(); + msg.tags = vec!["a".into(), "b".into()]; + let size = msg.compute_size() as usize; + let bytes = msg.encode_to_vec(); + assert_eq!(size, bytes.len()); +} + +#[test] +fn test_compute_size_matches_encode_len_view() { + // Covers nested-message, repeated-message, and oneof-message cached-size + // dispatch through MessageFieldView/Box. let addr = AddressView { street: "1 Main St", city: "Springfield", From 50fdefc359527f997a70d9ff16a78adecb92d75a Mon Sep 17 00:00:00 2001 From: Ryan Brewster Date: Sat, 18 Apr 2026 21:45:00 +0000 Subject: [PATCH 08/13] view_encode: drop opt-in flag, tie to generate_views (option B) ViewEncode is now generated whenever views are generated (generate_views, on by default). The separate view_encode opt-in flag is removed: the uncalled .text cost measured ~12 B/message in release without LTO (linker dedups identical cached_size bodies; write_to is generic so monomorphizes only at call sites), and the WKT struct-shape break landed in 0.4.0 regardless. generate_views(false) remains the escape hatch for targets that want neither. Removes Config::view_encode field/builder/plugin-param and the 9+1 build.rs callsites. WKT regen byte-identical. --- CHANGELOG.md | 25 ++++++++--------- benchmarks/buffa/build.rs | 1 - buffa-build/src/lib.rs | 12 -------- buffa-codegen/src/bin/gen_wkt_types.rs | 7 ----- buffa-codegen/src/lib.rs | 15 ---------- buffa-codegen/src/view.rs | 38 +++++++++++--------------- buffa-test/build.rs | 9 ------ buffa/src/view.rs | 14 +++++----- protoc-gen-buffa/src/main.rs | 1 - 9 files changed, 35 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 961c94d..a89ef4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Breaking changes -- **WKT view structs gain a `__buffa_cached_size` field** when `buffa-types` - is built with the new `view_encode` capability enabled (which it is, so - nested-WKT views are encodable out of the box). Code that constructs - `TimestampView { seconds, nanos, __buffa_unknown_fields }` without - `..Default::default()` will fail to compile; use the trailing - `..Default::default()` per the documented convention. +- **All generated `*View<'a>` structs gain a `__buffa_cached_size` field** + for the new `ViewEncode` impl. Code that constructs a view literal + without `..Default::default()` will fail to compile; use the trailing + `..Default::default()` per the documented convention. Applies to WKT + view structs in `buffa-types` and to consumer-generated views. - **`google.protobuf.Any.value` is now `::bytes::Bytes` instead of `Vec`.** Makes `Any::clone()` a cheap refcount bump (up to ~170x faster for large payloads) instead of a full memcpy. Call sites constructing an `Any` by hand @@ -23,13 +22,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added -- **`ViewEncode<'a>` — opt-in serialization from borrowed view types.** - `Config::view_encode(true)` makes generated `*View<'a>` types implement - `ViewEncode` with the same two-pass `compute_size`/`write_to` model as - `Message`. Views can be constructed from borrowed `&'a str` / `&'a [u8]` - and encoded without intermediate `String`/`Vec` allocation. Benchmarks: - parity on serialize-only; ~6× on build+encode for a 15-label string-map - message. +- **`ViewEncode<'a>` — serialization from borrowed view types.** Generated + `*View<'a>` types implement `ViewEncode` (whenever views are generated, + i.e. `generate_views(true)`, the default) with the same two-pass + `compute_size`/`write_to` model as `Message`. Views can be constructed + from borrowed `&'a str` / `&'a [u8]` and encoded without intermediate + `String`/`Vec` allocation. Benchmarks: parity on serialize-only; ~6× on + build+encode for a 15-label string-map message. - **`MapView::new(Vec)` / `From` / `FromIterator`** for constructing map views directly (for `ViewEncode`). diff --git a/benchmarks/buffa/build.rs b/benchmarks/buffa/build.rs index 844a693..6198800 100644 --- a/benchmarks/buffa/build.rs +++ b/benchmarks/buffa/build.rs @@ -7,7 +7,6 @@ fn main() { ]) .includes(&["../proto/"]) .generate_json(true) - .view_encode(true) .compile() .expect("failed to compile benchmark protos"); } diff --git a/buffa-build/src/lib.rs b/buffa-build/src/lib.rs index 581c87a..5b8c1e4 100644 --- a/buffa-build/src/lib.rs +++ b/buffa-build/src/lib.rs @@ -125,18 +125,6 @@ impl Config { self } - /// Enable or disable `impl buffa::ViewEncode` on generated `*View<'a>` - /// types (default: false). - /// - /// When enabled, view types can be serialized directly from their - /// borrowed `&'a str` / `&'a [u8]` fields without building an owned - /// message. Each view struct gains a `__buffa_cached_size` field. - #[must_use] - pub fn view_encode(mut self, enabled: bool) -> Self { - self.codegen_config.view_encode = enabled; - self - } - /// Enable or disable `#[derive(arbitrary::Arbitrary)]` on generated /// types (default: false). /// diff --git a/buffa-codegen/src/bin/gen_wkt_types.rs b/buffa-codegen/src/bin/gen_wkt_types.rs index 3780275..cc5ca0a 100644 --- a/buffa-codegen/src/bin/gen_wkt_types.rs +++ b/buffa-codegen/src/bin/gen_wkt_types.rs @@ -92,13 +92,6 @@ fn main() { config.generate_arbitrary = true; config.generate_json = false; config.generate_text = true; - // view_encode = true WKTs are commonly nested inside - // application messages (Timestamp, Any, Struct/Value); a - // consumer that opts into ViewEncode needs the WKT view - // types to implement it for the nested-message dispatch to - // compile. The extra `__buffa_cached_size` field is 4 bytes - // per WKT view — negligible. - config.view_encode = true; config.emit_register_fn = false; // `Any.value` carries arbitrary encoded payloads that callers commonly // cache and clone into `repeated google.protobuf.Any` response fields. diff --git a/buffa-codegen/src/lib.rs b/buffa-codegen/src/lib.rs index 9b6fb4e..d290e63 100644 --- a/buffa-codegen/src/lib.rs +++ b/buffa-codegen/src/lib.rs @@ -196,20 +196,6 @@ pub struct CodeGenConfig { /// When this is `true`, the downstream crate must enable the `buffa/text` /// feature for the runtime encoder/decoder. pub generate_text: bool, - /// Whether to emit `impl buffa::ViewEncode` on generated `*View<'a>` types - /// so views can be serialized directly from borrowed fields. - /// - /// When `true`, each view struct gains a `__buffa_cached_size` field and - /// an `impl ViewEncode<'a>` block whose `compute_size` / `write_to` - /// reuse the same per-field encode logic as the owned `Message` impl — - /// the field encoders take `&str` / `&[u8]` so they apply to view field - /// types unchanged. - /// - /// Default `false`: View remains the zero-copy *read* path per - /// [DESIGN.md §2](../DESIGN.md). Enable for high-throughput servers - /// that build messages from borrowed data and want to skip the owned - /// `String` allocation per field on the encode path. - pub view_encode: bool, /// Whether the per-package `.mod.rs` stitcher emits /// `__buffa::register_types(&mut TypeRegistry)`. /// @@ -262,7 +248,6 @@ impl Default for CodeGenConfig { strict_utf8_mapping: false, allow_message_set: false, generate_text: false, - view_encode: false, emit_register_fn: true, type_attributes: Vec::new(), field_attributes: Vec::new(), diff --git a/buffa-codegen/src/view.rs b/buffa-codegen/src/view.rs index ab759e6..014a8ee 100644 --- a/buffa-codegen/src/view.rs +++ b/buffa-codegen/src/view.rs @@ -149,28 +149,22 @@ pub(crate) fn generate_view_with_nesting( } else { quote! {} }; - let (cached_size_field, view_encode_impl) = if ctx.config.view_encode { - let methods = crate::impl_message::build_view_encode_methods( - ctx, - msg, - ctx.config.preserve_unknown_fields, - features, - &oneof_idents, - &view_oneof_prefix, - )?; - ( - quote! { - #[doc(hidden)] - pub __buffa_cached_size: ::buffa::__private::CachedSize, - }, - quote! { - impl<'a> ::buffa::ViewEncode<'a> for #view_ident<'a> { - #methods - } - }, - ) - } else { - (quote! {}, quote! {}) + let view_encode_methods = crate::impl_message::build_view_encode_methods( + ctx, + msg, + ctx.config.preserve_unknown_fields, + features, + &oneof_idents, + &view_oneof_prefix, + )?; + let cached_size_field = quote! { + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, + }; + let view_encode_impl = quote! { + impl<'a> ::buffa::ViewEncode<'a> for #view_ident<'a> { + #view_encode_methods + } }; // When preserving unknowns we capture `before_tag` so we can compute the diff --git a/buffa-test/build.rs b/buffa-test/build.rs index 09f40fe..1cc1a50 100644 --- a/buffa-test/build.rs +++ b/buffa-test/build.rs @@ -4,7 +4,6 @@ fn main() { .files(&["protos/basic.proto"]) .includes(&["protos/"]) .generate_text(true) - .view_encode(true) .compile() .expect("buffa_build failed for basic.proto"); @@ -13,7 +12,6 @@ fn main() { buffa_build::Config::new() .files(&["protos/proto3_semantics.proto"]) .includes(&["protos/"]) - .view_encode(true) .compile() .expect("buffa_build failed for proto3_semantics.proto"); @@ -31,7 +29,6 @@ fn main() { buffa_build::Config::new() .files(&["protos/nested_deep.proto"]) .includes(&["protos/"]) - .view_encode(true) .compile() .expect("buffa_build failed for nested_deep.proto"); @@ -39,7 +36,6 @@ fn main() { buffa_build::Config::new() .files(&["protos/wkt_usage.proto"]) .includes(&["protos/"]) - .view_encode(true) .compile() .expect("buffa_build failed for wkt_usage.proto"); @@ -67,7 +63,6 @@ fn main() { .files(&["protos/proto2_defaults.proto"]) .includes(&["protos/"]) .generate_text(true) - .view_encode(true) .compile() .expect("buffa_build failed for proto2_defaults.proto"); @@ -119,7 +114,6 @@ fn main() { .includes(&["protos/"]) .strict_utf8_mapping(true) .generate_json(true) - .view_encode(true) .compile() .expect("buffa_build failed for utf8_validation.proto"); @@ -130,7 +124,6 @@ fn main() { .includes(&["protos/"]) .generate_json(true) .generate_views(true) - .view_encode(true) .compile() .expect("buffa_build failed for edge_cases.proto"); @@ -151,7 +144,6 @@ fn main() { .includes(&["protos/"]) .use_bytes_type() .generate_json(true) - .view_encode(true) .out_dir(bytes_out) .compile() .expect("buffa_build failed for basic.proto with use_bytes_type"); @@ -167,7 +159,6 @@ fn main() { .files(&["protos/basic.proto"]) .includes(&["protos/"]) .preserve_unknown_fields(false) - .view_encode(true) .out_dir(no_uf_out) .compile() .expect("buffa_build failed for basic.proto with preserve_unknown_fields=false"); diff --git a/buffa/src/view.rs b/buffa/src/view.rs index 8c8d458..4285691 100644 --- a/buffa/src/view.rs +++ b/buffa/src/view.rs @@ -119,11 +119,11 @@ pub trait MessageView<'a>: Sized { /// [`MapView`] / [`RepeatedView`] fields are written by borrow — no /// owned-struct intermediary, no per-field `String`/`Vec` allocations. /// -/// Generated `*View<'a>` types implement this trait when codegen is -/// configured with `view_encode(true)`; off by default. Each implementing -/// view struct gains a `__buffa_cached_size` field for the cached-size -/// pass — same `AtomicU32`-backed [`CachedSize`](crate::__private::CachedSize) -/// as owned messages, so views remain `Send + Sync`. +/// Generated `*View<'a>` types implement this trait whenever views are +/// generated (`generate_views(true)`, the default). Each view struct +/// carries a `__buffa_cached_size` field for the cached-size pass — same +/// `AtomicU32`-backed [`CachedSize`](crate::__private::CachedSize) as +/// owned messages, so views remain `Send + Sync`. /// /// ## When to use /// @@ -142,8 +142,8 @@ pub trait MessageView<'a>: Sized { /// let bytes = view.encode_to_vec(); /// ``` #[diagnostic::on_unimplemented( - message = "`{Self}` does not implement `ViewEncode` — view encoding was not generated for this type", - note = "enable `view_encode(true)` in your buffa-build / buffa-codegen config to generate encode methods on view types" + message = "`{Self}` does not implement `ViewEncode` — view types were not generated for this message", + note = "ViewEncode is implemented on every generated `*View<'a>` type; enable `generate_views(true)` (on by default) in your buffa-build / buffa-codegen config" )] pub trait ViewEncode<'a>: MessageView<'a> { /// Compute and cache the encoded byte size of this view. diff --git a/protoc-gen-buffa/src/main.rs b/protoc-gen-buffa/src/main.rs index 0fc7f1c..1398900 100644 --- a/protoc-gen-buffa/src/main.rs +++ b/protoc-gen-buffa/src/main.rs @@ -127,7 +127,6 @@ fn parse_config(params: &str) -> Result { if let Some((key, value)) = param.split_once('=') { match key.trim() { "views" => codegen.generate_views = value.trim() == "true", - "view_encode" => codegen.view_encode = value.trim() == "true", "unknown_fields" => codegen.preserve_unknown_fields = value.trim() != "false", "json" => codegen.generate_json = value.trim() == "true", "text" => codegen.generate_text = value.trim() == "true", From c370e449d3ffd672412ba547a2399629fa3856d9 Mon Sep 17 00:00:00 2001 From: Ryan Brewster Date: Thu, 23 Apr 2026 14:56:31 +0000 Subject: [PATCH 09/13] benchmarks: wire MediaFrame into view-encode benches post-rebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the 5th `bench_view_encode!` and `bench_build_encode!` invocations for the MediaFrame dataset (introduced in #61) so the encode-side spectrum covers all five message shapes. Drops the orphaned single-file `build-encode.svg` — superseded by per-dataset SVGs from #61's chart layout. Measured on this branch: MediaFrame build_encode_view = 2.66× (mid-spectrum; win scales with alloc count + map inserts, not bytes copied). LogRecord remains the best case at 5.68×. --- benchmarks/buffa/benches/protobuf.rs | 39 ++++++++++++++++++++++ benchmarks/charts/build-encode.svg | 50 ---------------------------- 2 files changed, 39 insertions(+), 50 deletions(-) delete mode 100644 benchmarks/charts/build-encode.svg diff --git a/benchmarks/buffa/benches/protobuf.rs b/benchmarks/buffa/benches/protobuf.rs index b7f9de0..27fec87 100644 --- a/benchmarks/buffa/benches/protobuf.rs +++ b/benchmarks/buffa/benches/protobuf.rs @@ -275,6 +275,13 @@ bench_view_encode!( "buffa/google_message1_proto3", "../../datasets/google_message1_proto3.pb" ); +bench_view_encode!( + bench_media_frame_view_encode, + MediaFrame, + MediaFrameView, + "buffa/media_frame", + "../../datasets/media_frame.pb" +); /// Build-then-encode benches: unlike `encode`/`encode_view` (which serialize /// a pre-built struct), these include the cost of populating the message from @@ -459,6 +466,36 @@ bench_build_encode!( }, ); +static MF_BODY: [u8; 4096] = [0xAB; 4096]; +static MF_CHUNKS: [[u8; 1024]; 4] = [[0xC0; 1024], [0xC1; 1024], [0xC2; 1024], [0xC3; 1024]]; +static MF_ATT_A: [u8; 512] = [0xA0; 512]; +static MF_ATT_B: [u8; 768] = [0xB0; 768]; +const MF_ATTACH: [(&str, &[u8]); 2] = [("thumbnail", &MF_ATT_A), ("metadata", &MF_ATT_B)]; + +bench_build_encode!( + bench_media_frame_build_encode, + "buffa/media_frame", + MediaFrame, + MediaFrame { + frame_id: "frame-001a2b3c".into(), + timestamp_nanos: 1_700_000_000_000_000_000, + content_type: "video/h264".into(), + body: MF_BODY.to_vec(), + chunks: MF_CHUNKS.iter().map(|c| c.to_vec()).collect(), + attachments: MF_ATTACH.iter().map(|(k, v)| ((*k).into(), v.to_vec())).collect(), + ..Default::default() + }, + MediaFrameView { + frame_id: "frame-001a2b3c", + timestamp_nanos: 1_700_000_000_000_000_000, + content_type: "video/h264", + body: &MF_BODY, + chunks: MF_CHUNKS.iter().map(|c| &c[..]).collect(), + attachments: MF_ATTACH.iter().copied().collect(), + ..Default::default() + }, +); + fn bench_api_response(c: &mut Criterion) { benchmark_decode::( c, @@ -559,10 +596,12 @@ criterion_group!( bench_log_record_view_encode, bench_analytics_event_view_encode, bench_google_message1_view_encode, + bench_media_frame_view_encode, bench_api_response_build_encode, bench_log_record_build_encode, bench_analytics_event_build_encode, bench_google_message1_build_encode, + bench_media_frame_build_encode, ); criterion_group!( diff --git a/benchmarks/charts/build-encode.svg b/benchmarks/charts/build-encode.svg deleted file mode 100644 index 04a9d9f..0000000 --- a/benchmarks/charts/build-encode.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - - Build + Binary Encode Throughput (from borrowed source data) - - buffa - - buffa (view) - - 0 - - 800 - - 1.6k - - 2.4k - - 3.2k - - 4.0k - MiB/s - ApiResponse - - 790 - - 1,798 - LogRecord - - 531 - - 3,080 - AnalyticsEvent - - 408 - - 1,178 - GoogleMessage1 - - 934 - - 1,251 - From 3a02634fef710d4dff6875a3727407e7629f5527 Mon Sep 17 00:00:00 2001 From: Ryan Brewster Date: Thu, 23 Apr 2026 23:31:29 +0000 Subject: [PATCH 10/13] post-rebase: adapt tests to __buffa:: sentinel paths from #62 Auto-merge during rebase left some `use` lines out of rustfmt order and PR #55's view-encode tests referenced oneof types via the pre-#62 paths (`person::ContactOneof`, `view_coverage::ChoiceOneof`). Updated to the new `__buffa::oneof::` / `__buffa::view::oneof::` scheme; rustfmt for the rest. --- buffa-test/src/tests/bytes_type.rs | 2 +- buffa-test/src/tests/closed_enum.rs | 6 +++--- buffa-test/src/tests/json.rs | 10 +++++----- buffa-test/src/tests/nesting.rs | 4 ++-- buffa-test/src/tests/view.rs | 20 ++++++++++++-------- buffa-test/src/tests/wkt.rs | 2 +- buffa-types/src/timestamp_ext.rs | 2 +- 7 files changed, 25 insertions(+), 21 deletions(-) diff --git a/buffa-test/src/tests/bytes_type.rs b/buffa-test/src/tests/bytes_type.rs index 2f7831c..35d59da 100644 --- a/buffa-test/src/tests/bytes_type.rs +++ b/buffa-test/src/tests/bytes_type.rs @@ -82,9 +82,9 @@ fn test_bytes_type_view_to_owned() { // The bytes_variant build block compiles BytesContexts with use_bytes_type() // + generate_views=true; compilation alone is the primary assertion. +use crate::basic_bytes::BytesContexts; use crate::basic_bytes::__buffa::oneof::bytes_contexts::Choice as ChoiceOneof; use crate::basic_bytes::__buffa::view::BytesContextsView; -use crate::basic_bytes::BytesContexts; #[test] fn test_bytes_type_repeated_view_to_owned() { diff --git a/buffa-test/src/tests/closed_enum.rs b/buffa-test/src/tests/closed_enum.rs index a4fcbc1..5480f25 100644 --- a/buffa-test/src/tests/closed_enum.rs +++ b/buffa-test/src/tests/closed_enum.rs @@ -195,8 +195,8 @@ fn test_view_closed_enum_optional_unknown_to_unknown_fields() { #[test] fn test_view_closed_enum_repeated_unpacked_unknown_preserved() { - use crate::proto2::__buffa::view::ClosedEnumContextsView; use crate::proto2::Priority; + use crate::proto2::__buffa::view::ClosedEnumContextsView; use buffa::MessageView; // Field 2 (unpacked): [LOW=0, 99, HIGH=2] let mut wire = Vec::new(); @@ -232,8 +232,8 @@ fn test_view_closed_enum_oneof_unknown_to_unknown_fields() { #[test] fn test_view_closed_enum_known_not_routed() { - use crate::proto2::__buffa::view::ClosedEnumContextsView; use crate::proto2::Priority; + use crate::proto2::__buffa::view::ClosedEnumContextsView; use buffa::MessageView; let wire = varint_field(1, 2); // HIGH = 2 let view = ClosedEnumContextsView::decode_view(&wire).unwrap(); @@ -245,8 +245,8 @@ fn test_view_closed_enum_known_not_routed() { fn test_view_owned_parity_for_closed_enum_unknowns() { // Whatever the owned decoder produces, the view path must produce // byte-identical output after to_owned_message().encode_to_vec(). - use crate::proto2::__buffa::view::ClosedEnumContextsView; use crate::proto2::ClosedEnumContexts; + use crate::proto2::__buffa::view::ClosedEnumContextsView; use buffa::{Message, MessageView}; let mut wire = Vec::new(); wire.extend(varint_field(1, 99)); // optional unknown diff --git a/buffa-test/src/tests/json.rs b/buffa-test/src/tests/json.rs index 3c26956..61faff6 100644 --- a/buffa-test/src/tests/json.rs +++ b/buffa-test/src/tests/json.rs @@ -66,8 +66,8 @@ fn test_json_oneof_all_scalar_types_round_trip() { // Exercises serde_helper_path dispatch for all proto3-JSON-special // scalar types in oneof position, and the corresponding runtime // json_helpers::{int64, uint32, uint64, float, double, bytes} paths. - use crate::json_types::__buffa::oneof::with_oneof_types::Kind as KindOneof; use crate::json_types::WithOneofTypes; + use crate::json_types::__buffa::oneof::with_oneof_types::Kind as KindOneof; #[rustfmt::skip] let cases: &[(KindOneof, &str)] = &[ @@ -106,8 +106,8 @@ fn test_json_oneof_all_scalar_types_round_trip() { #[test] fn test_json_oneof_float_special_values() { // NaN/Infinity/-Infinity serialize as string tokens per proto3-JSON spec. - use crate::json_types::__buffa::oneof::with_oneof_types::Kind as KindOneof; use crate::json_types::WithOneofTypes; + use crate::json_types::__buffa::oneof::with_oneof_types::Kind as KindOneof; #[rustfmt::skip] let cases: &[(KindOneof, &str)] = &[ @@ -151,8 +151,8 @@ fn test_json_oneof_float_special_values() { fn test_json_oneof_null_value() { // google.protobuf.NullValue in a oneof serializes as JSON null. // On deserialize, JSON null populates the NullValue variant (not unset). - use crate::json_types::__buffa::oneof::with_oneof_types::Kind as KindOneof; use crate::json_types::WithOneofTypes; + use crate::json_types::__buffa::oneof::with_oneof_types::Kind as KindOneof; use buffa_types::google::protobuf::NullValue; let msg = WithOneofTypes { @@ -174,8 +174,8 @@ fn test_json_oneof_null_value() { fn test_json_oneof_float_deserialize_from_integer() { // proto3-JSON: float/double fields accept integer JSON values. // Exercises json_helpers::float::visit_i64/visit_u64. - use crate::json_types::__buffa::oneof::with_oneof_types::Kind as KindOneof; use crate::json_types::WithOneofTypes; + use crate::json_types::__buffa::oneof::with_oneof_types::Kind as KindOneof; let decoded: WithOneofTypes = serde_json::from_str(r#"{"f32": 42}"#).unwrap(); assert_eq!(decoded.kind, Some(KindOneof::F32(42.0))); @@ -490,8 +490,8 @@ fn test_json_mixed_value_field_null_forwarding() { // not "field absent". The custom Deserialize must forward null to // Value's own Deserialize rather than skipping the field. use crate::json_types::MixedOneofAndFields; - use buffa_types::google::protobuf::__buffa::oneof::value::Kind as KindOneof; use buffa_types::google::protobuf::NullValue; + use buffa_types::google::protobuf::__buffa::oneof::value::Kind as KindOneof; let decoded: MixedOneofAndFields = serde_json::from_str(r#"{"dynamic": null}"#).unwrap(); assert!(decoded.dynamic.is_set(), "null should set the Value field"); diff --git a/buffa-test/src/tests/nesting.rs b/buffa-test/src/tests/nesting.rs index 2239d61..a79044c 100644 --- a/buffa-test/src/tests/nesting.rs +++ b/buffa-test/src/tests/nesting.rs @@ -122,8 +122,8 @@ fn test_recursive_oneof_direct() { // Expr { kind { Expr negated = 3; } } is directly self-recursive // through the oneof. Message/group variants are always boxed to // break the infinite-size cycle. - use crate::nested::__buffa::oneof::expr; use crate::nested::Expr; + use crate::nested::__buffa::oneof::expr; let inner = Expr { kind: Some(expr::Kind::IntLiteral(42)), ..Default::default() @@ -256,9 +256,9 @@ fn test_recursive_oneof_merge_semantics() { fn test_view_oneof_boxed_message_variant() { // View oneof enums box message/group variants for the same reason // as owned enums. The Box holds a lifetime-bound view struct. + use crate::nested::Expr; use crate::nested::__buffa::oneof::expr; use crate::nested::__buffa::view::ExprView; - use crate::nested::Expr; use buffa::MessageView; let inner = Expr { kind: Some(expr::Kind::IntLiteral(42)), diff --git a/buffa-test/src/tests/view.rs b/buffa-test/src/tests/view.rs index eea1d75..a9b3bfe 100644 --- a/buffa-test/src/tests/view.rs +++ b/buffa-test/src/tests/view.rs @@ -2,6 +2,7 @@ //! MapView iteration, oneof views, unknown-field preservation, recursion limit. use crate::basic::__buffa::oneof; +use crate::basic::__buffa::view::oneof as view_oneof; use crate::basic::__buffa::view::*; use crate::basic::*; use buffa::{Message, MessageView, ViewEncode}; @@ -285,7 +286,7 @@ fn test_compute_size_matches_encode_len_view() { tags: ["a", "b"].iter().copied().collect(), address: addr.clone().into(), addresses: [addr.clone()].into_iter().collect(), - contact: Some(person::ContactOneofView::HomeAddress(Box::new(addr))), + contact: Some(view_oneof::person::Contact::HomeAddress(Box::new(addr))), ..Default::default() }; let size = view.compute_size() as usize; @@ -439,14 +440,14 @@ fn test_view_encode_construct_map_from_iter() { fn test_view_encode_oneof_roundtrip() { // String variant. let mut owned = Person::default(); - owned.contact = Some(person::ContactOneof::Email("a@b.c".into())); + owned.contact = Some(oneof::person::Contact::Email("a@b.c".into())); let owned_bytes = owned.encode_to_vec(); let view = PersonView::decode_view(&owned_bytes).unwrap(); assert_eq!(owned_bytes, view.encode_to_vec()); // Nested-message variant — exercises ViewEncode dispatch through Box. let mut owned = Person::default(); - owned.contact = Some(person::ContactOneof::HomeAddress(Box::new(Address { + owned.contact = Some(oneof::person::Contact::HomeAddress(Box::new(Address { street: "1 main".into(), city: "metro".into(), zip_code: 9, @@ -471,7 +472,7 @@ fn test_view_encode_repeated_nested_and_oneof_from_borrows() { lucky_numbers: RepeatedView::new(vec![7, 11]), address: MessageFieldView::set(addr.clone()), addresses: RepeatedView::new(vec![addr.clone()]), - contact: Some(person::ContactOneofView::HomeAddress(Box::new(addr))), + contact: Some(view_oneof::person::Contact::HomeAddress(Box::new(addr))), ..Default::default() }; let bytes = view.encode_to_vec(); @@ -480,7 +481,7 @@ fn test_view_encode_repeated_nested_and_oneof_from_borrows() { assert_eq!(decoded.lucky_numbers, [7, 11]); assert_eq!(decoded.address.street, "x"); assert_eq!(decoded.addresses[0].zip_code, 9); - let Some(person::ContactOneof::HomeAddress(a)) = &decoded.contact else { + let Some(oneof::person::Contact::HomeAddress(a)) = &decoded.contact else { panic!("expected HomeAddress variant") }; assert_eq!(a.city, "y"); @@ -501,7 +502,8 @@ fn test_view_encode_compute_size_matches_len() { #[test] fn test_view_encode_proto2_groups_roundtrip() { - use crate::proto2::{with_groups, WithGroups, WithGroupsView}; + use crate::proto2::__buffa::view::WithGroupsView; + use crate::proto2::{with_groups, WithGroups}; let owned = WithGroups { mygroup: with_groups::MyGroup { a: Some(7), @@ -533,10 +535,12 @@ fn test_view_encode_proto2_groups_roundtrip() { #[test] fn test_view_encode_proto2_oneof_group_closed_enum_roundtrip() { - use crate::proto2::{view_coverage, Priority, ViewCoverage, ViewCoverageView}; + use crate::proto2::__buffa::oneof::view_coverage as vc_oneof; + use crate::proto2::__buffa::view::ViewCoverageView; + use crate::proto2::{view_coverage, Priority, ViewCoverage}; let mut owned = ViewCoverage { level: Priority::HIGH, - choice: Some(view_coverage::ChoiceOneof::Payload(Box::new( + choice: Some(vc_oneof::Choice::Payload(Box::new( view_coverage::Payload { x: Some(42), y: Some("oneof-group".into()), diff --git a/buffa-test/src/tests/wkt.rs b/buffa-test/src/tests/wkt.rs index cb39fdc..d3e404a 100644 --- a/buffa-test/src/tests/wkt.rs +++ b/buffa-test/src/tests/wkt.rs @@ -86,8 +86,8 @@ fn test_wkt_view_with_extern_path() { // appends "View" to the last path segment: crate::wkt::Timestamp // → crate::wkt::TimestampView (which is re-exported from buffa-types' // generated code). - use crate::wkt::__buffa::view::EventView; use crate::wkt::Event; + use crate::wkt::__buffa::view::EventView; use buffa::MessageView; let msg = Event { created_at: buffa::MessageField::some(buffa_types::google::protobuf::Timestamp { diff --git a/buffa-types/src/timestamp_ext.rs b/buffa-types/src/timestamp_ext.rs index d945252..b27557d 100644 --- a/buffa-types/src/timestamp_ext.rs +++ b/buffa-types/src/timestamp_ext.rs @@ -540,8 +540,8 @@ mod tests { #[test] fn timestamp_view_round_trip() { - use crate::google::protobuf::__buffa::view::TimestampView; use crate::google::protobuf::Timestamp; + use crate::google::protobuf::__buffa::view::TimestampView; use buffa::{Message, MessageView}; let ts = Timestamp { From 184f0470ec802457d141c30da8f44bc3875b0934 Mon Sep 17 00:00:00 2001 From: Ryan Brewster Date: Thu, 23 Apr 2026 23:34:00 +0000 Subject: [PATCH 11/13] post-rebase: fix bench imports for __buffa:: paths from #62 The `bench::__buffa::view::*` glob from #62 exported a nested `analytics_event` module that shadowed the owned-side one from `bench::*`, breaking `analytics_event::Property` resolution in the build_encode benches. Replaced the view glob with explicit imports and updated oneof references to the new `__buffa::oneof::` / `__buffa::view::oneof::` paths. --- benchmarks/buffa/benches/protobuf.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/benchmarks/buffa/benches/protobuf.rs b/benchmarks/buffa/benches/protobuf.rs index 27fec87..d791ce7 100644 --- a/benchmarks/buffa/benches/protobuf.rs +++ b/benchmarks/buffa/benches/protobuf.rs @@ -2,7 +2,11 @@ use buffa::{Message, MessageView, ViewEncode}; use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use serde::{de::DeserializeOwned, Serialize}; -use bench_buffa::bench::__buffa::view::*; +use bench_buffa::bench::__buffa::view::{ + analytics_event::PropertyView, AnalyticsEventView, ApiResponseView, LogRecordView, + MediaFrameView, +}; +use bench_buffa::bench::__buffa::{oneof, view::oneof as view_oneof}; use bench_buffa::bench::*; use bench_buffa::benchmarks::BenchmarkDataset; use bench_buffa::proto3::__buffa::view::GoogleMessage1View; @@ -271,7 +275,7 @@ bench_view_encode!( bench_view_encode!( bench_google_message1_view_encode, bench_buffa::proto3::GoogleMessage1, - bench_buffa::proto3::GoogleMessage1View, + GoogleMessage1View, "buffa/google_message1_proto3", "../../datasets/google_message1_proto3.pb" ); @@ -412,7 +416,9 @@ bench_build_encode!( .iter() .map(|(k, v)| analytics_event::Property { key: (*k).into(), - value: Some(analytics_event::property::ValueOneof::StringValue((*v).into())), + value: Some(oneof::analytics_event::property::Value::StringValue( + (*v).into(), + )), ..Default::default() }) .collect(), @@ -424,9 +430,9 @@ bench_build_encode!( user_id: "usr_8f7e6d5c4b3a2910", properties: PROPS .iter() - .map(|(k, v)| analytics_event::PropertyView { + .map(|(k, v)| PropertyView { key: k, - value: Some(analytics_event::property::ValueOneofView::StringValue(v)), + value: Some(view_oneof::analytics_event::property::Value::StringValue(v)), ..Default::default() }) .collect(), @@ -451,7 +457,7 @@ bench_build_encode!( field101: 101, ..Default::default() }, - bench_buffa::proto3::GoogleMessage1View { + GoogleMessage1View { field1: "the quick brown fox", field9: "jumps over the lazy dog", field2: 42, From 4586655eb11cb1568cca02ed2f85e2b52acc8268 Mon Sep 17 00:00:00 2001 From: Iain McGinniss <309153+iainmcgin@users.noreply.github.com> Date: Fri, 24 Apr 2026 01:00:54 +0000 Subject: [PATCH 12/13] fixup: rustfmt 1.95 import ordering + update view-oneof doc comment CI rustfmt (1.95) sorts __buffa:: paths before sibling PascalCase imports; local default toolchain is older and does not reorder. Also updates the build_view_encode_methods doc comment to reference the post-#62 __buffa::view::oneof:: layout. --- buffa-codegen/src/impl_message.rs | 13 +++++++------ buffa-test/src/tests/bytes_type.rs | 2 +- buffa-test/src/tests/closed_enum.rs | 6 +++--- buffa-test/src/tests/json.rs | 10 +++++----- buffa-test/src/tests/nesting.rs | 4 ++-- buffa-test/src/tests/wkt.rs | 2 +- buffa-types/src/timestamp_ext.rs | 2 +- 7 files changed, 20 insertions(+), 19 deletions(-) diff --git a/buffa-codegen/src/impl_message.rs b/buffa-codegen/src/impl_message.rs index 270a9b3..0ff2c12 100644 --- a/buffa-codegen/src/impl_message.rs +++ b/buffa-codegen/src/impl_message.rs @@ -662,12 +662,13 @@ pub(crate) fn build_view_encode_methods( .map(|f| repeated_write_to_stmt(ctx, f, features)) .collect::, _>>()?; - // The view-side oneof enum (`mod::FooOneofView<'a>`) has the same variant - // *names* as the owned `mod::FooOneof` but borrowed payload types - // (`&'a str` / `Box>` vs `String` / `Box`). The arm - // builders only emit the enum path + variant name and call duck-typed - // primitives (`string_encoded_len(x)`, `x.compute_size()`), so they work - // unchanged once pointed at the view enum. + // The view-side oneof enum (in the parallel `__buffa::view::oneof::` tree) + // has the same variant *names* as the owned `__buffa::oneof::::Kind` + // but borrowed payload types (`&'a str` / `Box>` vs `String` / + // `Box`). The arm builders only emit the enum path + variant name and + // call duck-typed primitives (`string_encoded_len(x)`, `x.compute_size()`), + // so they work unchanged once pointed at the view enum via + // `view_oneof_prefix` (no `View` suffix — the tree disambiguates). let mut oneof_compute_stmts: Vec = Vec::new(); let mut oneof_write_stmts: Vec = Vec::new(); for (oneof_name, enum_ident, fields) in &oneof_groups { diff --git a/buffa-test/src/tests/bytes_type.rs b/buffa-test/src/tests/bytes_type.rs index 35d59da..2f7831c 100644 --- a/buffa-test/src/tests/bytes_type.rs +++ b/buffa-test/src/tests/bytes_type.rs @@ -82,9 +82,9 @@ fn test_bytes_type_view_to_owned() { // The bytes_variant build block compiles BytesContexts with use_bytes_type() // + generate_views=true; compilation alone is the primary assertion. -use crate::basic_bytes::BytesContexts; use crate::basic_bytes::__buffa::oneof::bytes_contexts::Choice as ChoiceOneof; use crate::basic_bytes::__buffa::view::BytesContextsView; +use crate::basic_bytes::BytesContexts; #[test] fn test_bytes_type_repeated_view_to_owned() { diff --git a/buffa-test/src/tests/closed_enum.rs b/buffa-test/src/tests/closed_enum.rs index 5480f25..a4fcbc1 100644 --- a/buffa-test/src/tests/closed_enum.rs +++ b/buffa-test/src/tests/closed_enum.rs @@ -195,8 +195,8 @@ fn test_view_closed_enum_optional_unknown_to_unknown_fields() { #[test] fn test_view_closed_enum_repeated_unpacked_unknown_preserved() { - use crate::proto2::Priority; use crate::proto2::__buffa::view::ClosedEnumContextsView; + use crate::proto2::Priority; use buffa::MessageView; // Field 2 (unpacked): [LOW=0, 99, HIGH=2] let mut wire = Vec::new(); @@ -232,8 +232,8 @@ fn test_view_closed_enum_oneof_unknown_to_unknown_fields() { #[test] fn test_view_closed_enum_known_not_routed() { - use crate::proto2::Priority; use crate::proto2::__buffa::view::ClosedEnumContextsView; + use crate::proto2::Priority; use buffa::MessageView; let wire = varint_field(1, 2); // HIGH = 2 let view = ClosedEnumContextsView::decode_view(&wire).unwrap(); @@ -245,8 +245,8 @@ fn test_view_closed_enum_known_not_routed() { fn test_view_owned_parity_for_closed_enum_unknowns() { // Whatever the owned decoder produces, the view path must produce // byte-identical output after to_owned_message().encode_to_vec(). - use crate::proto2::ClosedEnumContexts; use crate::proto2::__buffa::view::ClosedEnumContextsView; + use crate::proto2::ClosedEnumContexts; use buffa::{Message, MessageView}; let mut wire = Vec::new(); wire.extend(varint_field(1, 99)); // optional unknown diff --git a/buffa-test/src/tests/json.rs b/buffa-test/src/tests/json.rs index 61faff6..3c26956 100644 --- a/buffa-test/src/tests/json.rs +++ b/buffa-test/src/tests/json.rs @@ -66,8 +66,8 @@ fn test_json_oneof_all_scalar_types_round_trip() { // Exercises serde_helper_path dispatch for all proto3-JSON-special // scalar types in oneof position, and the corresponding runtime // json_helpers::{int64, uint32, uint64, float, double, bytes} paths. - use crate::json_types::WithOneofTypes; use crate::json_types::__buffa::oneof::with_oneof_types::Kind as KindOneof; + use crate::json_types::WithOneofTypes; #[rustfmt::skip] let cases: &[(KindOneof, &str)] = &[ @@ -106,8 +106,8 @@ fn test_json_oneof_all_scalar_types_round_trip() { #[test] fn test_json_oneof_float_special_values() { // NaN/Infinity/-Infinity serialize as string tokens per proto3-JSON spec. - use crate::json_types::WithOneofTypes; use crate::json_types::__buffa::oneof::with_oneof_types::Kind as KindOneof; + use crate::json_types::WithOneofTypes; #[rustfmt::skip] let cases: &[(KindOneof, &str)] = &[ @@ -151,8 +151,8 @@ fn test_json_oneof_float_special_values() { fn test_json_oneof_null_value() { // google.protobuf.NullValue in a oneof serializes as JSON null. // On deserialize, JSON null populates the NullValue variant (not unset). - use crate::json_types::WithOneofTypes; use crate::json_types::__buffa::oneof::with_oneof_types::Kind as KindOneof; + use crate::json_types::WithOneofTypes; use buffa_types::google::protobuf::NullValue; let msg = WithOneofTypes { @@ -174,8 +174,8 @@ fn test_json_oneof_null_value() { fn test_json_oneof_float_deserialize_from_integer() { // proto3-JSON: float/double fields accept integer JSON values. // Exercises json_helpers::float::visit_i64/visit_u64. - use crate::json_types::WithOneofTypes; use crate::json_types::__buffa::oneof::with_oneof_types::Kind as KindOneof; + use crate::json_types::WithOneofTypes; let decoded: WithOneofTypes = serde_json::from_str(r#"{"f32": 42}"#).unwrap(); assert_eq!(decoded.kind, Some(KindOneof::F32(42.0))); @@ -490,8 +490,8 @@ fn test_json_mixed_value_field_null_forwarding() { // not "field absent". The custom Deserialize must forward null to // Value's own Deserialize rather than skipping the field. use crate::json_types::MixedOneofAndFields; - use buffa_types::google::protobuf::NullValue; use buffa_types::google::protobuf::__buffa::oneof::value::Kind as KindOneof; + use buffa_types::google::protobuf::NullValue; let decoded: MixedOneofAndFields = serde_json::from_str(r#"{"dynamic": null}"#).unwrap(); assert!(decoded.dynamic.is_set(), "null should set the Value field"); diff --git a/buffa-test/src/tests/nesting.rs b/buffa-test/src/tests/nesting.rs index a79044c..2239d61 100644 --- a/buffa-test/src/tests/nesting.rs +++ b/buffa-test/src/tests/nesting.rs @@ -122,8 +122,8 @@ fn test_recursive_oneof_direct() { // Expr { kind { Expr negated = 3; } } is directly self-recursive // through the oneof. Message/group variants are always boxed to // break the infinite-size cycle. - use crate::nested::Expr; use crate::nested::__buffa::oneof::expr; + use crate::nested::Expr; let inner = Expr { kind: Some(expr::Kind::IntLiteral(42)), ..Default::default() @@ -256,9 +256,9 @@ fn test_recursive_oneof_merge_semantics() { fn test_view_oneof_boxed_message_variant() { // View oneof enums box message/group variants for the same reason // as owned enums. The Box holds a lifetime-bound view struct. - use crate::nested::Expr; use crate::nested::__buffa::oneof::expr; use crate::nested::__buffa::view::ExprView; + use crate::nested::Expr; use buffa::MessageView; let inner = Expr { kind: Some(expr::Kind::IntLiteral(42)), diff --git a/buffa-test/src/tests/wkt.rs b/buffa-test/src/tests/wkt.rs index d3e404a..cb39fdc 100644 --- a/buffa-test/src/tests/wkt.rs +++ b/buffa-test/src/tests/wkt.rs @@ -86,8 +86,8 @@ fn test_wkt_view_with_extern_path() { // appends "View" to the last path segment: crate::wkt::Timestamp // → crate::wkt::TimestampView (which is re-exported from buffa-types' // generated code). - use crate::wkt::Event; use crate::wkt::__buffa::view::EventView; + use crate::wkt::Event; use buffa::MessageView; let msg = Event { created_at: buffa::MessageField::some(buffa_types::google::protobuf::Timestamp { diff --git a/buffa-types/src/timestamp_ext.rs b/buffa-types/src/timestamp_ext.rs index b27557d..d945252 100644 --- a/buffa-types/src/timestamp_ext.rs +++ b/buffa-types/src/timestamp_ext.rs @@ -540,8 +540,8 @@ mod tests { #[test] fn timestamp_view_round_trip() { - use crate::google::protobuf::Timestamp; use crate::google::protobuf::__buffa::view::TimestampView; + use crate::google::protobuf::Timestamp; use buffa::{Message, MessageView}; let ts = Timestamp { From 2fa6cb1c8dd995105a75e48f7fec921e1812c82b Mon Sep 17 00:00:00 2001 From: Iain McGinniss <309153+iainmcgin@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:55:16 +0000 Subject: [PATCH 13/13] review followups: must_use + use_bytes_type/closed-enum-unknown view-encode tests - Add #[must_use] to compute_size/cached_size/encode_to_vec/encode_to_bytes on both Message and ViewEncode (let _ = in default encode() bodies where the caching side-effect is the point). - test_bytes_type_view_encode_roundtrip: ViewEncode x use_bytes_type across repeated/optional/oneof/map/singular bytes shapes. - test_view_encode_closed_enum_unknown_value_preserved: unknown closed-enum value on the wire goes to UnknownFieldsView; ViewEncode must re-emit so a downstream owned decode sees the same result as direct decode. --- buffa-test/src/tests/bytes_type.rs | 36 ++++++++++++++++++++++++++++++ buffa-test/src/tests/view.rs | 23 +++++++++++++++++++ buffa/src/message.rs | 7 +++++- buffa/src/view.rs | 7 +++++- 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/buffa-test/src/tests/bytes_type.rs b/buffa-test/src/tests/bytes_type.rs index 2f7831c..ca2cd45 100644 --- a/buffa-test/src/tests/bytes_type.rs +++ b/buffa-test/src/tests/bytes_type.rs @@ -269,3 +269,39 @@ fn test_bytes_type_map_value_stays_vec() { let owned: BytesContexts = view.to_owned_message(); assert_eq!(owned.by_key.get("k").map(Vec::as_slice), Some(&b"v"[..])); } + +#[test] +fn test_bytes_type_view_encode_roundtrip() { + // ViewEncode × use_bytes_type: view-side bytes fields are `&[u8]` while + // owned-side are `bytes::Bytes`. The encode-stmt builders duck-type + // through `encode_bytes(&self.#ident, buf)` (AsRef<[u8]>) so this should + // be wire-identical to the owned encode across all bytes-field shapes. + use buffa::{MessageView, ViewEncode}; + let msg = BytesContexts { + many: vec![ + bytes::Bytes::from_static(b"a"), + bytes::Bytes::from_static(b"bc"), + ], + maybe: Some(bytes::Bytes::from_static(&[0x00, 0xFF])), + choice: Some(ChoiceOneof::Raw(bytes::Bytes::from_static(b"o"))), + by_key: [("k".to_string(), b"v".to_vec())].into_iter().collect(), + ..Default::default() + }; + let wire = msg.encode_to_vec(); + let view = BytesContextsView::decode_view(&wire).expect("decode_view"); + let view_wire = view.encode_to_vec(); + // Decode-then-compare rather than byte-equality (map iteration order is + // hash-seed dependent on the owned side). + let back = BytesContexts::decode_from_slice(&view_wire).expect("decode"); + assert_eq!(back, msg); + // Singular bytes field via Person.avatar (no map → byte-exact). + use crate::basic_bytes::__buffa::view::PersonView; + let p = Person { + id: 1, + avatar: bytes::Bytes::from_static(&[0xCA, 0xFE, 0xBE, 0xEF]), + ..Default::default() + }; + let p_wire = p.encode_to_vec(); + let p_view = PersonView::decode_view(&p_wire).expect("decode_view"); + assert_eq!(p_view.encode_to_vec(), p_wire); +} diff --git a/buffa-test/src/tests/view.rs b/buffa-test/src/tests/view.rs index a9b3bfe..8703845 100644 --- a/buffa-test/src/tests/view.rs +++ b/buffa-test/src/tests/view.rs @@ -560,3 +560,26 @@ fn test_view_encode_proto2_oneof_group_closed_enum_roundtrip() { let redecoded = ViewCoverage::decode_from_slice(&view_bytes).unwrap(); assert_eq!(redecoded, owned); } + +#[test] +fn test_view_encode_closed_enum_unknown_value_preserved() { + // Unknown closed-enum value on the wire: view decode cannot represent + // 99 as a `Priority` so the typed `level` field stays at default and the + // raw bytes go to UnknownFieldsView. ViewEncode must re-emit those + // unknown bytes so a downstream owned decoder sees the same result as + // decoding the original wire directly. + use crate::proto2::__buffa::view::ViewCoverageView; + use crate::proto2::{Priority, ViewCoverage}; + // field 1 (level) varint = tag 0x08, value 99 (not a Priority variant). + let wire: &[u8] = &[0x08, 99]; + let owned_direct = ViewCoverage::decode_from_slice(wire).unwrap(); + let view = ViewCoverageView::decode_view(wire).unwrap(); + // The typed field is NOT 99 — it's the proto2 first-variant default. + assert_eq!(view.level, Priority::LOW); + let view_bytes = view.encode_to_vec(); + let owned_via_view = ViewCoverage::decode_from_slice(&view_bytes).unwrap(); + assert_eq!( + owned_via_view, owned_direct, + "view-encode must preserve unknown closed-enum semantics" + ); +} diff --git a/buffa/src/message.rs b/buffa/src/message.rs index 0a08304..e8fb5be 100644 --- a/buffa/src/message.rs +++ b/buffa/src/message.rs @@ -83,6 +83,8 @@ pub trait Message: DefaultInstance + Clone + PartialEq + Send + Sync { /// is `u32`, so messages whose encoded size exceeds `u32::MAX` (4 GiB) /// will produce a wrapped (undefined) size and a truncated encoding. /// Stay well within the 2 GiB spec limit. + #[must_use = "compute_size has the side-effect of populating cached sizes; \ + if you only need that, call encode() instead"] fn compute_size(&self) -> u32; /// Write this message's encoded bytes to a buffer. @@ -93,7 +95,7 @@ pub trait Message: DefaultInstance + Clone + PartialEq + Send + Sync { /// Convenience: compute size, then write. This is the primary encoding API. fn encode(&self, buf: &mut impl BufMut) { - self.compute_size(); + let _ = self.compute_size(); self.write_to(buf); } @@ -105,6 +107,7 @@ pub trait Message: DefaultInstance + Clone + PartialEq + Send + Sync { } /// Encode this message to a new `Vec`. + #[must_use] fn encode_to_vec(&self) -> alloc::vec::Vec { let size = self.compute_size() as usize; let mut buf = alloc::vec::Vec::with_capacity(size); @@ -120,6 +123,7 @@ pub trait Message: DefaultInstance + Clone + PartialEq + Send + Sync { /// This is equivalent to `Bytes::from(self.encode_to_vec())` — both /// are zero-copy with respect to the encoded bytes — but saves readers /// from having to know that `From> for Bytes` is zero-copy. + #[must_use] fn encode_to_bytes(&self) -> bytes::Bytes { let size = self.compute_size() as usize; let mut buf = bytes::BytesMut::with_capacity(size); @@ -377,6 +381,7 @@ pub trait Message: DefaultInstance + Clone + PartialEq + Send + Sync { /// The cached encoded size from the last `compute_size()` call. /// /// Returns 0 if `compute_size()` has never been called. + #[must_use] fn cached_size(&self) -> u32; /// Clear all fields to their default values. diff --git a/buffa/src/view.rs b/buffa/src/view.rs index 4285691..00d7613 100644 --- a/buffa/src/view.rs +++ b/buffa/src/view.rs @@ -151,6 +151,8 @@ pub trait ViewEncode<'a>: MessageView<'a> { /// Recursively computes sizes for sub-message views and stores them in /// each view's `CachedSize` field. Must be called before /// [`write_to`](Self::write_to). + #[must_use = "compute_size has the side-effect of populating cached sizes; \ + if you only need that, call encode() instead"] fn compute_size(&self) -> u32; /// Write this view's encoded bytes to a buffer. @@ -160,11 +162,12 @@ pub trait ViewEncode<'a>: MessageView<'a> { fn write_to(&self, buf: &mut impl BufMut); /// Return the size cached by the most recent [`compute_size`](Self::compute_size). + #[must_use] fn cached_size(&self) -> u32; /// Convenience: compute size, then write. Primary view-encode entry point. fn encode(&self, buf: &mut impl BufMut) { - self.compute_size(); + let _ = self.compute_size(); self.write_to(buf); } @@ -176,6 +179,7 @@ pub trait ViewEncode<'a>: MessageView<'a> { } /// Encode this view to a new `Vec`. + #[must_use] fn encode_to_vec(&self) -> alloc::vec::Vec { let size = self.compute_size() as usize; let mut buf = alloc::vec::Vec::with_capacity(size); @@ -184,6 +188,7 @@ pub trait ViewEncode<'a>: MessageView<'a> { } /// Encode this view to a new [`bytes::Bytes`]. + #[must_use] fn encode_to_bytes(&self) -> Bytes { let size = self.compute_size() as usize; let mut buf = bytes::BytesMut::with_capacity(size);