Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/unreleased/added-20260706-162526.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Added
body: 'Vectored ("rope") encode: the new `EncodeSink` trait abstracts the encode output, and the `Rope` sink captures large `bytes::Bytes` fields (and, via `Rope::with_backing`, large borrowed view fields) as reference-counted segments instead of copying them — encoding a message dominated by one large payload costs O(header) instead of O(payload). Contiguous callers are unaffected: every `BufMut` is an `EncodeSink` through a blanket impl. `ProtoBytes` gains a provided `as_shared` method so custom `Bytes`-backed representations can opt in, and `RopeBuf` adapts a finished rope to `bytes::Buf` with vectored-I/O support.'
time: 2026-07-06T16:25:26.000000000Z
3 changes: 3 additions & 0 deletions .changes/unreleased/changed-20260706-162527.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Changed
body: 'Breaking: `Message::write_to`/`encode` (and `ViewEncode`, the `types::put_*`/`encode_*` helpers, and generated code) now take `&mut impl EncodeSink` instead of `&mut impl BufMut`. Callers passing `Vec<u8>`, `BytesMut`, or any other `BufMut` are source-compatible via the blanket impl; manual `Message`/`ViewEncode` implementations must update their method signatures, and generated code must be regenerated with the matching codegen version. Note that `EncodeSink` deliberately exposes only the `BufMut` subset the encoders use (`put_u8`, `put_slice`, and the little-endian fixed-width writers) — a manual `write_to` that used other `BufMut` methods must assemble into a concrete buffer first. Generated `write_to` bodies now emit `put_shared_bytes_field` for `bytes` fields — copy-equivalent for `Vec<u8>`, segment-aware for `bytes::Bytes`.'
time: 2026-07-06T16:25:27.000000000Z
6 changes: 3 additions & 3 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The runtime library that generated code depends on. Contains:
- **`MessageField<T>`**: Ergonomic wrapper for optional message fields that dereferences to a default instance when unset.
- **`SizeCache`**: External pre-order size cache threaded through `compute_size` / `write_to` for linear-time serialization.
- **`EnumValue<T>`**: Type-safe wrapper for open enum fields that preserves unknown values.
- **Wire format codec**: Varint, fixed-width, length-delimited, and group encoding/decoding using `bytes::{Buf, BufMut}`.
- **Wire format codec**: Varint, fixed-width, length-delimited, and group encoding/decoding; decoding reads any `bytes::Buf`, encoding writes to any `EncodeSink` (every `bytes::BufMut` qualifies via a blanket impl, and `Rope` provides segmented zero-copy output).
- **Unknown field storage**: Preserves unknown fields for round-trip fidelity.
- **Edition feature types**: Rust types representing edition features (`FieldPresence`, `EnumType`, `RepeatedFieldEncoding`, etc.) used by generated code and runtime logic.

Expand Down Expand Up @@ -393,13 +393,13 @@ The `Message` trait reflects this two-pass model:
pub trait Message: DefaultInstance + Clone + PartialEq + Send + Sync {
// Required methods (implemented by codegen per message type):
fn compute_size(&self, cache: &mut SizeCache) -> u32; // Pass 1
fn write_to(&self, cache: &mut SizeCache, buf: &mut impl BufMut); // Pass 2
fn write_to(&self, cache: &mut SizeCache, buf: &mut impl EncodeSink); // Pass 2
fn merge_field(&mut self, tag: Tag, buf: &mut impl Buf, ctx: DecodeContext<'_>)
-> Result<(), DecodeError>; // Per-field decode dispatch
fn clear(&mut self);

// Provided methods (default impls):
fn encode(&self, buf: &mut impl BufMut);
fn encode(&self, buf: &mut impl EncodeSink);
fn encode_to_vec(&self) -> Vec<u8>;
fn encode_to_bytes(&self) -> Bytes;
fn decode_from_slice(data: &[u8]) -> Result<Self, DecodeError>;
Expand Down
18 changes: 9 additions & 9 deletions buffa-codegen/src/impl_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -584,9 +584,9 @@ pub fn generate_message_impl(
quote! { let size = 0u32; }
};
let buf_param = if has_body {
quote! { buf: &mut impl ::buffa::bytes::BufMut }
quote! { buf: &mut impl ::buffa::EncodeSink }
} else {
quote! { _buf: &mut impl ::buffa::bytes::BufMut }
quote! { _buf: &mut impl ::buffa::EncodeSink }
};

let extension_set_impl = if preserve_unknown_fields {
Expand Down Expand Up @@ -905,9 +905,9 @@ pub(crate) fn build_view_encode_methods(
quote! { let size = 0u32; }
};
let buf_param = if has_body {
quote! { buf: &mut impl ::buffa::bytes::BufMut }
quote! { buf: &mut impl ::buffa::EncodeSink }
} else {
quote! { _buf: &mut impl ::buffa::bytes::BufMut }
quote! { _buf: &mut impl ::buffa::EncodeSink }
};

// On the lazy family these are inherent `pub fn`s, so they need doc
Expand Down Expand Up @@ -1688,7 +1688,7 @@ fn scalar_write_to_stmt(
}),
Type::TYPE_BYTES => Ok(quote! {
if let Some(ref v) = self.#ident {
::buffa::types::put_bytes_field(#field_number, v, buf);
::buffa::types::put_shared_bytes_field(#field_number, v, buf);
}
}),
Type::TYPE_ENUM => Ok(quote! {
Expand Down Expand Up @@ -1725,12 +1725,12 @@ fn scalar_write_to_stmt(
Type::TYPE_BYTES => {
return Ok(if is_proto2_required {
quote! {
::buffa::types::put_bytes_field(#field_number, &self.#ident, buf);
::buffa::types::put_shared_bytes_field(#field_number, &self.#ident, buf);
}
} else {
quote! {
if !self.#ident.is_empty() {
::buffa::types::put_bytes_field(#field_number, &self.#ident, buf);
::buffa::types::put_shared_bytes_field(#field_number, &self.#ident, buf);
}
}
});
Expand Down Expand Up @@ -2270,7 +2270,7 @@ fn repeated_write_to_stmt(
quote! { ::buffa::types::put_string_field(#field_number, v, buf); }
}
Type::TYPE_BYTES => {
quote! { ::buffa::types::put_bytes_field(#field_number, v, buf); }
quote! { ::buffa::types::put_shared_bytes_field(#field_number, v, buf); }
}
Type::TYPE_ENUM => {
quote! { ::buffa::types::put_int32_field(#field_number, v.to_i32(), buf); }
Expand Down Expand Up @@ -2594,7 +2594,7 @@ fn oneof_write_arm(
},
Type::TYPE_BYTES => quote! {
#enum_ident::#variant_ident(x) => {
::buffa::types::put_bytes_field(#field_number, x, buf);
::buffa::types::put_shared_bytes_field(#field_number, x, buf);
}
},
Type::TYPE_ENUM => quote! {
Expand Down
2 changes: 1 addition & 1 deletion buffa-codegen/src/lazy_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ pub(crate) fn generate_lazy_view_with_nesting(
#view_encode_methods

/// Compute size, then write. Primary encode entry point.
pub fn encode(&self, buf: &mut impl ::buffa::bytes::BufMut) {
pub fn encode(&self, buf: &mut impl ::buffa::EncodeSink) {
let mut __cache = ::buffa::SizeCache::new();
self.compute_size(&mut __cache);
self.write_to(&mut __cache, buf);
Expand Down
8 changes: 4 additions & 4 deletions buffa-codegen/src/tests/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1726,8 +1726,8 @@ fn test_message_proto3_optional_bytes_and_bool() {
);
// Bytes uses encode_bytes
assert!(
content.contains("put_bytes_field"),
"missing put_bytes_field for optional bytes: {content}"
content.contains("put_shared_bytes_field"),
"missing put_shared_bytes_field for optional bytes: {content}"
);
}

Expand Down Expand Up @@ -1772,8 +1772,8 @@ fn test_message_string_and_bytes_fields() {
"missing string_encoded_len: {content}"
);
assert!(
content.contains("put_bytes_field"),
"missing put_bytes_field: {content}"
content.contains("put_shared_bytes_field"),
"missing put_shared_bytes_field: {content}"
);
assert!(
content.contains("merge_bytes"),
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading