From 8b36958229be4c9c51e14974b38d16859c67c332 Mon Sep 17 00:00:00 2001 From: Iain McGinniss <309153+iainmcgin@users.noreply.github.com> Date: Wed, 8 Jul 2026 18:13:54 -0700 Subject: [PATCH 1/2] remote-derive: optional as_shared override on derive(ProtoBytes) The generated ProtoBytes impl previously always inherited the trait's as_shared default of None, so a remote bytes newtype - even one wrapping bytes::Bytes - silently took the copy path when encoding into a segmented (Rope) sink, with no way to override from within the derive (the impl is owned by the derive, so a manual one is a coherence error). #[buffa(remote = ..., as_shared = path)] now generates the hook, calling the named path as a free function on the wrapped field (fn(&Remote) -> Option). Absent the key, nothing is generated and the trait default still applies, so existing derives produce identical output. Unknown keys are still rejected, with as_shared added to the error message's key list. Tests cover the default (None), the override returning the wrapped handle (pointer equality), an end-to-end Rope splice through put_shared_bytes_field with parity against a contiguous sink, and the named-field newtype shape. :house: Remote-Dev: homespace --- .../unreleased/added-20260708-180602.yaml | 9 +++ buffa-remote-derive/src/bytes.rs | 16 ++++- buffa-remote-derive/src/lib.rs | 27 +++++++- buffa-remote-derive/src/remote_field.rs | 11 +-- buffa-remote-derive/tests/proto_bytes.rs | 69 +++++++++++++++++++ 5 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 .changes/unreleased/added-20260708-180602.yaml diff --git a/.changes/unreleased/added-20260708-180602.yaml b/.changes/unreleased/added-20260708-180602.yaml new file mode 100644 index 00000000..41c96b13 --- /dev/null +++ b/.changes/unreleased/added-20260708-180602.yaml @@ -0,0 +1,9 @@ +kind: Added +body: |- + **`buffa-remote-derive`: optional `as_shared` override on `derive(ProtoBytes)`**. + `#[buffa(remote = ..., as_shared = path)]` generates the encode-side + `ProtoBytes::as_shared` hook, letting a remote bytes newtype that stores a + `bytes::Bytes` splice into segmented (`Rope`) sinks by reference count + instead of being copied. Without the key the trait default (`None`, copy) + still applies. +time: 2026-07-08T18:06:02.186522554-07:00 diff --git a/buffa-remote-derive/src/bytes.rs b/buffa-remote-derive/src/bytes.rs index e0656a71..3c2a3c73 100644 --- a/buffa-remote-derive/src/bytes.rs +++ b/buffa-remote-derive/src/bytes.rs @@ -5,7 +5,7 @@ use syn::DeriveInput; use crate::remote_field::{self, RemoteField}; pub fn derive(input: DeriveInput) -> syn::Result { - let remote = remote_field::parse(&input)?; + let (remote, overrides) = remote_field::parse_with_overrides(&input, &["as_shared"])?; let RemoteField { ident, generics, @@ -28,6 +28,18 @@ pub fn derive(input: DeriveInput) -> syn::Result { let ctor_from_vec = remote.construct(quote! { #from_vec(v) }); let ctor_from_wire = remote.construct(quote! { #from_vec(payload.as_slice().to_vec()) }); + // Unlike the `ProtoBox`/`MapStorage` overrides there is no conventional + // method name to default to: absent the key, nothing is generated and + // the trait's own `None` default applies. + let as_shared_impl = overrides.get("as_shared").map(|path| { + quote! { + #[inline] + fn as_shared(&self) -> ::core::option::Option<::buffa::bytes::Bytes> { + #path(&#accessor) + } + } + }); + Ok(quote! { impl #impl_generics ::core::ops::Deref for #ident #ty_generics #where_clause { type Target = [u8]; @@ -58,6 +70,8 @@ pub fn derive(input: DeriveInput) -> syn::Result { ) -> ::core::result::Result { ::core::result::Result::Ok(#ctor_from_wire) } + + #as_shared_impl } }) } diff --git a/buffa-remote-derive/src/lib.rs b/buffa-remote-derive/src/lib.rs index 1c93fd43..33e8ff42 100644 --- a/buffa-remote-derive/src/lib.rs +++ b/buffa-remote-derive/src/lib.rs @@ -87,6 +87,21 @@ //! payload, and the common single-chunk source arrives borrowed and is //! copied there too. When that copy matters, use the built-in `bytes::Bytes` //! representation for the field rather than a custom type. +//! +//! The encode side has the mirror-image limitation with an escape hatch: by +//! default the generated `ProtoBytes` impl inherits the trait's `as_shared` +//! default of `None`, so encoding into a segmented sink (`buffa::Rope`) +//! copies the payload instead of splicing it by reference count. A remote +//! type that stores (or can cheaply produce) a `bytes::Bytes` handle can name +//! the callable via `#[buffa(remote = ..., as_shared = path)]`; it is called +//! as a free function on the wrapped field — `path(&self.0)` or +//! `path(&self.field)` — and must have the shape `fn(&Remote) -> +//! Option`. A signature mismatch is a type error at the +//! generated call site, not a special diagnostic from this macro. The +//! returned handle must satisfy `buffa::ProtoBytes::as_shared`'s correctness +//! contract, and only a segmented sink ever calls it — test against a +//! `Rope` explicitly. +//! //! `ProtoList` additionally requires the //! remote collection to implement `Extend` (used //! to implement `push`); its generated `clear` reinitializes the field via @@ -107,6 +122,9 @@ //! `MapStorage`) don't give a generic derive enough to call through to //! `new`/`into_inner`/`insert`/`clear`/`iter`/`len` the way `From`/ //! `FromIterator`/`Extend` did for `ProtoString`/`ProtoBytes`/`ProtoList`. +//! (`ProtoBytes`'s `as_shared` key is a different kind of override — an +//! opt-in over a working trait default, not a renamed inherent method — +//! and is documented above.) //! //! So these two derives default to the near-universal naming convention //! (`Type::new`/`Type::into_inner` for pointers — `Rc`, `Arc`, @@ -153,7 +171,9 @@ //! //! To override a default, name the method explicitly: //! `#[buffa(remote = ..., into_inner = MyType::unwrap)]` for `ProtoBox`, or -//! any of `len`/`insert`/`clear`/`iter` for `MapStorage`. The override path is +//! any of `len`/`insert`/`clear`/`iter` for `MapStorage`. (The full key +//! catalog is these plus `ProtoBytes`'s `as_shared`, covered earlier; the +//! other derives accept no extra keys.) The override path is //! called the same way the default is — as a free function taking the //! receiver as its first argument (`Type::method(&self.0, ...)`) — so it //! **must** accept the same receiver as the method it replaces: `new` takes @@ -195,7 +215,10 @@ pub fn derive_proto_string(input: TokenStream) -> TokenStream { /// See the [crate-level docs](crate). Generates `Deref`, /// `AsRef<[u8]>`, `From>`, and `buffa::ProtoBytes` for a single-field -/// newtype wrapping the type named by `#[buffa(remote = ...)]`. +/// newtype wrapping the type named by `#[buffa(remote = ...)]`. An optional +/// `as_shared = path` key generates the encode-side +/// `buffa::ProtoBytes::as_shared` override — see the crate docs for the +/// callable's contract. #[proc_macro_derive(ProtoBytes, attributes(buffa))] pub fn derive_proto_bytes(input: TokenStream) -> TokenStream { expand(input, bytes::derive) diff --git a/buffa-remote-derive/src/remote_field.rs b/buffa-remote-derive/src/remote_field.rs index 217d36b6..c529199f 100644 --- a/buffa-remote-derive/src/remote_field.rs +++ b/buffa-remote-derive/src/remote_field.rs @@ -40,11 +40,12 @@ pub fn parse(input: &DeriveInput) -> syn::Result { /// Like [`parse`], but also collects any of `allowed_overrides` present in /// `#[buffa(remote = ..., key = path, ...)]` as `syn::Path`s — used by derives -/// (`ProtoBox`, `MapStorage`) whose reference implementations call **inherent** -/// methods on the remote type (e.g. `into_inner()`, `insert()`) rather than -/// trait methods, so the method path can't be synthesized generically and -/// instead defaults to the common naming convention with an escape hatch to -/// override it. +/// whose generated impl needs a caller-supplied method path. Two modes exist: +/// replacing a conventional inherent-method default (`ProtoBox`'s +/// `new`/`into_inner`, `MapStorage`'s `len`/`insert`/`clear`/`iter`, resolved +/// through [`overridable_call`]), and enabling an optional hook with no +/// default at all (`ProtoBytes`'s `as_shared`, where an absent key means the +/// method is not generated and the trait default applies). pub fn parse_with_overrides( input: &DeriveInput, allowed_overrides: &[&str], diff --git a/buffa-remote-derive/tests/proto_bytes.rs b/buffa-remote-derive/tests/proto_bytes.rs index 136b7b7b..fb065fe7 100644 --- a/buffa-remote-derive/tests/proto_bytes.rs +++ b/buffa-remote-derive/tests/proto_bytes.rs @@ -25,3 +25,72 @@ fn from_vec_round_trips() { let b = MyBytes::from(v.clone()); assert_eq!(b.as_ref(), v.as_slice()); } + +#[test] +fn as_shared_defaults_to_none() { + let b = MyBytes::from(vec![1u8, 2, 3]); + assert!(b.as_shared().is_none()); +} + +mod share { + pub fn handle(b: &buffa::bytes::Bytes) -> Option { + Some(b.clone()) + } +} + +#[derive(Clone, PartialEq, Default, Debug, DeriveProtoBytes)] +#[buffa(remote = buffa::bytes::Bytes, as_shared = share::handle)] +struct SharedBytes(pub buffa::bytes::Bytes); + +#[test] +fn as_shared_override_returns_the_wrapped_handle() { + let b = SharedBytes(buffa::bytes::Bytes::from(vec![7u8; 32])); + let shared = b.as_shared().expect("override returns Some"); + // Same allocation, not a copy. + assert_eq!(shared.as_ptr(), b.0.as_ptr()); + assert_eq!(shared.as_ref(), b.as_ref()); +} + +/// The override must reach a segmented sink end-to-end: encoding through +/// `put_shared_bytes_field` into a `Rope` splices the wrapped `Bytes` by +/// reference count instead of copying. Contiguous sinks never call +/// `as_shared`, so only this path proves the generated hook is wired up. +#[test] +fn as_shared_override_splices_into_rope() { + // Ropes copy payloads below their min-segment threshold, so the payload + // must exceed it for the splice assertion to be meaningful. + let payload = buffa::bytes::Bytes::from(vec![0xAB; 2 * buffa::DEFAULT_MIN_SEGMENT]); + let b = SharedBytes(payload.clone()); + + let mut rope = buffa::Rope::new(); + buffa::types::put_shared_bytes_field(1, &b, &mut rope); + let rope_bytes = rope.to_contiguous_bytes(); + let spliced = rope + .into_segments() + .into_iter() + .find(|seg| seg.len() == payload.len()) + .expect("payload segment present"); + assert_eq!(spliced.as_ptr(), payload.as_ptr(), "spliced, not copied"); + + // Byte-for-byte parity with a contiguous sink. + let mut contiguous = Vec::new(); + buffa::types::put_shared_bytes_field(1, &b, &mut contiguous); + assert_eq!(rope_bytes.as_ref(), contiguous.as_slice()); +} + +/// The override call shape also covers a named-field newtype +/// (`path(&self.field)` rather than `path(&self.0)`). +#[derive(Clone, PartialEq, Default, Debug, DeriveProtoBytes)] +#[buffa(remote = buffa::bytes::Bytes, as_shared = share::handle)] +struct NamedShared { + inner: buffa::bytes::Bytes, +} + +#[test] +fn as_shared_override_works_on_named_field_newtype() { + let b = NamedShared { + inner: buffa::bytes::Bytes::from(vec![3u8; 32]), + }; + let shared = b.as_shared().expect("override returns Some"); + assert_eq!(shared.as_ptr(), b.inner.as_ptr()); +} From 2687ba38c2ed050a3c79c8ebf5e70bfed0c831f5 Mon Sep 17 00:00:00 2001 From: Iain McGinniss <309153+iainmcgin@users.noreply.github.com> Date: Wed, 8 Jul 2026 18:14:24 -0700 Subject: [PATCH 2/2] changelog: reference PR number in as_shared fragment :house: Remote-Dev: homespace --- .changes/unreleased/added-20260708-180602.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changes/unreleased/added-20260708-180602.yaml b/.changes/unreleased/added-20260708-180602.yaml index 41c96b13..394faa4f 100644 --- a/.changes/unreleased/added-20260708-180602.yaml +++ b/.changes/unreleased/added-20260708-180602.yaml @@ -1,6 +1,6 @@ kind: Added body: |- - **`buffa-remote-derive`: optional `as_shared` override on `derive(ProtoBytes)`**. + **`buffa-remote-derive`: optional `as_shared` override on `derive(ProtoBytes)`** (#294). `#[buffa(remote = ..., as_shared = path)]` generates the encode-side `ProtoBytes::as_shared` hook, letting a remote bytes newtype that stores a `bytes::Bytes` splice into segmented (`Rope`) sinks by reference count