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
9 changes: 9 additions & 0 deletions .changes/unreleased/added-20260708-180602.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
kind: Added
body: |-
**`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
instead of being copied. Without the key the trait default (`None`, copy)
still applies.
time: 2026-07-08T18:06:02.186522554-07:00
16 changes: 15 additions & 1 deletion buffa-remote-derive/src/bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use syn::DeriveInput;
use crate::remote_field::{self, RemoteField};

pub fn derive(input: DeriveInput) -> syn::Result<TokenStream> {
let remote = remote_field::parse(&input)?;
let (remote, overrides) = remote_field::parse_with_overrides(&input, &["as_shared"])?;
let RemoteField {
ident,
generics,
Expand All @@ -28,6 +28,18 @@ pub fn derive(input: DeriveInput) -> syn::Result<TokenStream> {
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];
Expand Down Expand Up @@ -58,6 +70,8 @@ pub fn derive(input: DeriveInput) -> syn::Result<TokenStream> {
) -> ::core::result::Result<Self, ::buffa::DecodeError> {
::core::result::Result::Ok(#ctor_from_wire)
}

#as_shared_impl
}
})
}
27 changes: 25 additions & 2 deletions buffa-remote-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bytes::Bytes>`. 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<T>` (used
//! to implement `push`); its generated `clear` reinitializes the field via
Expand All @@ -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`,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -195,7 +215,10 @@ pub fn derive_proto_string(input: TokenStream) -> TokenStream {

/// See the [crate-level docs](crate). Generates `Deref<Target = [u8]>`,
/// `AsRef<[u8]>`, `From<Vec<u8>>`, 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)
Expand Down
11 changes: 6 additions & 5 deletions buffa-remote-derive/src/remote_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ pub fn parse(input: &DeriveInput) -> syn::Result<RemoteField> {

/// 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],
Expand Down
69 changes: 69 additions & 0 deletions buffa-remote-derive/tests/proto_bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<buffa::bytes::Bytes> {
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());
}
Loading