Skip to content
Open
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
26 changes: 26 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"buffa-descriptor",
"buffa-codegen",
"buffa-build",
"buffa-remote-derive",
"buffa-test",
"buffa-yaml",
"protoc-gen-buffa",
Expand Down
25 changes: 25 additions & 0 deletions buffa-remote-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "buffa-remote-derive"
description = "Derive macros that implement buffa's pluggable owned-type traits (ProtoString, ProtoBytes, ProtoList, ProtoBox, MapStorage) for a newtype wrapping a foreign remote type"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
keywords = ["protobuf", "protocol-buffers", "derive"]
categories = ["encoding"]

[lib]
proc-macro = true

[dependencies]
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true }

[dev-dependencies]
buffa = { workspace = true, features = ["std"] }
ecow = { workspace = true }
smallvec = { version = "1", default-features = false }
smallbox = { version = "0.8", default-features = false }
indexmap = { version = "2", default-features = false, features = ["std"] }
62 changes: 62 additions & 0 deletions buffa-remote-derive/src/box_ptr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::DeriveInput;

use crate::remote_field::{self, RemoteField};

pub fn derive(input: DeriveInput) -> syn::Result<TokenStream> {
let (remote, overrides) = remote_field::parse_with_overrides(&input, &["new", "into_inner"])?;
let RemoteField {
ident,
generics,
field_ty,
accessor,
..
} = &remote;

let element_ty = remote_field::single_type_param(generics)?;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

// Defaults assume the remote pointer follows the `Type::new(value) ->
// Self` / `Type::into_inner(self) -> T` convention (e.g.
// `smallbox::SmallBox`); override with `#[buffa(remote = ..., new = path,
// into_inner = path)]` for a remote type that names them differently —
// notably plain `std::boxed::Box`, whose `into_inner` is nightly-only
// (`box_into_inner`), so wrapping `Box<T>` with this derive needs an
// `into_inner` override (or just use buffa's built-in `Box<T>` impl,
// which never needs this derive).
let new_call = remote_field::overridable_call(&overrides, "new", field_ty, "new");
let into_inner_call =
remote_field::overridable_call(&overrides, "into_inner", field_ty, "into_inner");

let ctor_new = remote.construct(quote! { #new_call(value) });

Ok(quote! {
impl #impl_generics ::core::ops::Deref for #ident #ty_generics #where_clause {
type Target = #element_ty;
#[inline]
fn deref(&self) -> &#element_ty {
&#accessor
}
}

impl #impl_generics ::core::ops::DerefMut for #ident #ty_generics #where_clause {
#[inline]
fn deref_mut(&mut self) -> &mut #element_ty {
&mut #accessor
}
}

impl #impl_generics ::buffa::ProtoBox<#element_ty> for #ident #ty_generics #where_clause {
#[inline]
fn new(value: #element_ty) -> Self {
#ctor_new
}

#[inline]
fn into_inner(self) -> #element_ty {
#into_inner_call(#accessor)
}
}
})
}
59 changes: 59 additions & 0 deletions buffa-remote-derive/src/bytes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::DeriveInput;

use crate::remote_field::{self, RemoteField};

pub fn derive(input: DeriveInput) -> syn::Result<TokenStream> {
let remote = remote_field::parse(&input)?;
let RemoteField {
ident,
generics,
field_ty,
accessor,
..
} = &remote;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

let from_vec = remote_field::qualified_call(
field_ty,
quote! { ::core::convert::From<::buffa::alloc::vec::Vec<u8>> },
"from",
);

let ctor_from_vec = remote.construct(quote! { #from_vec(v) });
let ctor_from_wire = remote.construct(quote! { #from_vec(payload.as_slice().to_vec()) });

Ok(quote! {
impl #impl_generics ::core::ops::Deref for #ident #ty_generics #where_clause {
type Target = [u8];
#[inline]
fn deref(&self) -> &[u8] {
#accessor.as_ref()
}
}

impl #impl_generics ::core::convert::AsRef<[u8]> for #ident #ty_generics #where_clause {
#[inline]
fn as_ref(&self) -> &[u8] {
#accessor.as_ref()
}
}

impl #impl_generics ::core::convert::From<::buffa::alloc::vec::Vec<u8>> for #ident #ty_generics #where_clause {
#[inline]
fn from(v: ::buffa::alloc::vec::Vec<u8>) -> Self {
#ctor_from_vec
}
}

impl #impl_generics ::buffa::ProtoBytes for #ident #ty_generics #where_clause {
#[inline]
fn from_wire(
payload: ::buffa::WirePayload<'_>,
) -> ::core::result::Result<Self, ::buffa::DecodeError> {
::core::result::Result::Ok(#ctor_from_wire)
}
}
})
}
193 changes: 193 additions & 0 deletions buffa-remote-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
//! Derive macros that implement buffa's pluggable owned-type traits for a
//! newtype wrapping a **foreign** ("remote") type.
//!
//! The owned Rust representation backing a proto `string`/`bytes`/`repeated`
//! field is pluggable (see [`buffa::ProtoString`], [`buffa::ProtoBytes`],
//! [`buffa::ProtoList`]). A custom representation implements one of those
//! traits. The friction is the orphan rule: a type from another crate (e.g.
//! `ecow::EcoString`) cannot implement a buffa-owned trait directly, so it
//! must be wrapped in a crate-local newtype with the trait impl — plus
//! `Deref`, `AsRef`, and the `From` conversions the trait requires —
//! hand-written on the wrapper. That boilerplate is mechanical and identical
//! in shape every time; these derives generate it from one annotation,
//! mirroring `serde`'s `remote` attribute pattern.
//!
//! ```rust
//! #[derive(Clone, PartialEq, Default, Debug, buffa_remote_derive::ProtoString)]
//! #[buffa(remote = ecow::EcoString)]
//! pub struct MyEcoString(pub ecow::EcoString);
//! ```
//!
//! expands the `Deref<Target = str>`, `AsRef<str>`, `From<String>`,
//! `From<&str>`, and `buffa::ProtoString` impls that would otherwise be
//! hand-written (compare to the worked example in `buffa-smolstr` or
//! `examples/custom-types`). The remote type must already satisfy
//! `ProtoString`'s non-buffa-owned supertraits (`Clone`, `PartialEq`,
//! `Default`, `Debug`, `Send`, `Sync`, `AsRef<str>`, `From<String>`,
//! `From<&str>`) — true of essentially every inline/shared-string crate, since
//! that's the API surface they're built to offer as a `String` substitute.
//! Derive those on the newtype yourself (they forward to the inner field
//! automatically via `#[derive(..)]`); this crate only generates the
//! buffa-specific pieces that the orphan rule blocks. If the remote type is
//! missing one of those supertraits, the compiler error names the missing
//! trait bound against the newtype's field — there is no need to expand the
//! macro to diagnose it.
//!
//! [`ProtoBytes`](macro@ProtoBytes) and [`ProtoList`](macro@ProtoList) follow
//! the same shape for `bytes` and `repeated` fields respectively. `ProtoList`
//! additionally requires the remote collection to implement `Extend<T>` (used
//! to implement `push`); its generated `clear` reinitializes the field via
//! `Default::default()`, which drops the existing allocation rather than
//! retaining capacity — acceptable per `ProtoList`'s contract ("retaining
//! capacity *where the underlying type allows*"), but worth knowing if a
//! decoder reuses long-lived buffers and capacity retention matters for that
//! workload. Hand-write `clear` to forward to the remote's own clearing
//! method instead, in that case.
//!
//! # `ProtoBox` and `MapStorage`: inherent methods, not trait methods
//!
//! [`ProtoBox`](macro@ProtoBox) and [`MapStorage`](macro@MapStorage) follow a
//! different shape from the three above. Their reference newtypes
//! (`smallbox::SmallBox::into_inner()`, `indexmap::IndexMap::insert()`) call
//! **inherent** methods on the remote type, not trait methods — `ProtoBox`'s
//! and `MapStorage`'s own supertraits (`Deref`/`DerefMut`; none, for
//! `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`.
//!
//! So these two derives default to the near-universal naming convention
//! (`Type::new`/`Type::into_inner` for pointers — `Rc`, `Arc`,
//! `smallbox::SmallBox` all use these names, though plain `std::boxed::Box`
//! does not, since its `into_inner` is nightly-only; `len`/`insert`/`clear`/
//! `iter` for maps — `HashMap`, `BTreeMap`, `indexmap::IndexMap`,
//! `dashmap::DashMap` all use these names), with an attribute escape hatch
//! when a remote type names them differently. If a default doesn't match —
//! the remote names its insert method `put`, say — the compiler reports
//! `no function or associated item named 'insert' found for struct '...'`;
//! that's the signal to add the matching override, named below.
//!
//! ```rust
//! #[derive(buffa_remote_derive::ProtoBox)]
//! #[buffa(remote = smallbox::SmallBox<T, smallbox::space::S4>)]
//! pub struct SmallBox<T>(pub smallbox::SmallBox<T, smallbox::space::S4>);
//! ```
//!
//! ```rust
//! #[derive(Clone, PartialEq, Debug, buffa_remote_derive::MapStorage)]
//! #[buffa(remote = indexmap::IndexMap<K, V>)]
//! pub struct MyIndexMap<K: core::hash::Hash + Eq, V>(pub indexmap::IndexMap<K, V>);
//!
//! impl<K: core::hash::Hash + Eq, V> Default for MyIndexMap<K, V> {
//! fn default() -> Self {
//! Self(indexmap::IndexMap::new())
//! }
//! }
//! impl<K: core::hash::Hash + Eq, V> FromIterator<(K, V)> for MyIndexMap<K, V> {
//! fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
//! Self(indexmap::IndexMap::from_iter(iter))
//! }
//! }
//! ```
//!
//! `Default` and `FromIterator<(Key, Value)>` are required by the message
//! codec that drives every `MapStorage` field, not by this derive — `cargo`
//! won't suggest them, since the derive itself compiles fine without them; the
//! failure shows up later, in generated message code, as `the trait bound
//! '...: Default' is not satisfied`. They aren't generated here for the same
//! reason `ProtoList`'s `Default` isn't (see above): a derived impl would
//! force `K: Default`/`V: Default`, which `MapStorage` does not require.
//!
//! 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
//! 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
//! the value by ownership and returns `Self`; `into_inner` takes `self` by
//! ownership; `insert`/`clear` take `&mut self`; `len`/`iter` take `&self`.
//! `iter` additionally must yield `(&Key, &Value)` pairs, matching
//! `storage_iter`'s contract. A receiver or item-type mismatch is a type error
//! at the generated call site, not a special diagnostic from this macro.
//!
//! # Why a `remote` attribute that just repeats the field's type?
//!
//! It doesn't change what's generated — the macro always reads the wrapped
//! field's actual type, never the type written in the attribute — and its
//! content is not checked against the field (comparing two type *spellings*
//! for equality isn't possible from within a derive macro without resolving
//! `use` imports). It exists so the newtype's purpose is legible without
//! reading the field declaration, the same role `serde`'s `remote` attribute
//! plays. The value still has to parse as a type, so a typo is caught even
//! though its content isn't otherwise used.

use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};

mod box_ptr;
mod bytes;
mod list;
mod map;
mod remote_field;
mod string;

/// See the [crate-level docs](crate) for the full pattern. Generates
/// `Deref<Target = str>`, `AsRef<str>`, `From<String>`, `From<&str>`, and
/// `buffa::ProtoString` for a single-field newtype wrapping the type named by
/// `#[buffa(remote = ...)]`.
#[proc_macro_derive(ProtoString, attributes(buffa))]
pub fn derive_proto_string(input: TokenStream) -> TokenStream {
expand(input, string::derive)
}

/// 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 = ...)]`.
#[proc_macro_derive(ProtoBytes, attributes(buffa))]
pub fn derive_proto_bytes(input: TokenStream) -> TokenStream {
expand(input, bytes::derive)
}

/// See the [crate-level docs](crate). Generates `Deref<Target = [T]>`,
/// `FromIterator<T>`, `From<Vec<T>>`, and `buffa::ProtoList<T>` for a
/// single-field, single-type-parameter newtype wrapping the type named by
/// `#[buffa(remote = ...)]`. Requires the remote type to implement
/// `Extend<T>`, and the newtype itself to implement `Default` by hand (not
/// `#[derive(Default)]`, which would wrongly force `T: Default`).
#[proc_macro_derive(ProtoList, attributes(buffa))]
pub fn derive_proto_list(input: TokenStream) -> TokenStream {
expand(input, list::derive)
}

/// See the [crate-level docs](crate). Generates `Deref<Target = T>`,
/// `DerefMut`, and `buffa::ProtoBox<T>` for a single-field,
/// single-type-parameter newtype wrapping the type named by
/// `#[buffa(remote = ...)]`. Calls the remote type's `new`/`into_inner`
/// methods by the conventional names unless overridden with
/// `#[buffa(remote = ..., new = path, into_inner = path)]`.
#[proc_macro_derive(ProtoBox, attributes(buffa))]
pub fn derive_proto_box(input: TokenStream) -> TokenStream {
expand(input, box_ptr::derive)
}

/// See the [crate-level docs](crate). Generates `buffa::MapStorage` for a
/// single-field, two-type-parameter (`Key`, `Value`) newtype wrapping the
/// type named by `#[buffa(remote = ...)]`. Calls the remote map's
/// `len`/`insert`/`clear`/`iter` methods by their conventional names unless
/// overridden with `#[buffa(remote = ..., insert = path, ...)]`. The newtype
/// itself must implement `Default` and `FromIterator<(Key, Value)>` by hand —
/// see the crate docs' example.
#[proc_macro_derive(MapStorage, attributes(buffa))]
pub fn derive_map_storage(input: TokenStream) -> TokenStream {
expand(input, map::derive)
}

fn expand(
input: TokenStream,
f: impl FnOnce(DeriveInput) -> syn::Result<proc_macro2::TokenStream>,
) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match f(input) {
Ok(tokens) => tokens.into(),
Err(err) => err.to_compile_error().into(),
}
}
Loading
Loading