diff --git a/README.md b/README.md index cc54cc6..c891d41 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,26 @@ devirt::devirt! { Both APIs produce identical expanded code. +### `dyn Trait + Send` / `Sync` + +The proc-macro attribute automatically emits dispatch shims for +`dyn Trait + Send`, `dyn Trait + Sync`, and `dyn Trait + Send + Sync`. +This means `Box` and `&(dyn Shape + Send + Sync)` +benefit from devirtualization without any extra annotation: + +```rust +fn total_area_send(shapes: &[Box]) -> f64 { + shapes.iter().map(|s| s.area()).sum() +} +``` + +Auto traits do not change the vtable layout, so the delegation is +zero-cost — LLVM eliminates it entirely with `#[inline(always)]`. + +> **Note:** The declarative macro (`default-features = false`) does not +> emit auto-trait inherent impls. Use the proc-macro attribute for +> `dyn Trait + Send` support. + ## When to use Best when a small number of hot types dominate the population (80%+ of trait diff --git a/crates/core/benches/dispatch.rs b/crates/core/benches/dispatch.rs index 4a6e98d..fdcf0a7 100644 --- a/crates/core/benches/dispatch.rs +++ b/crates/core/benches/dispatch.rs @@ -586,11 +586,79 @@ fn bench_shuffled_mixed(c: &mut Criterion) { group.finish(); } +// ───────────────────────────────────────────────────────────────────── +// dyn Trait + Send benchmark. Exercises the same shuffled 80/20 +// workload as `bench_shuffled_mixed` but through `Box` to verify that the delegating inherent impls are inlined +// and devirt still wins over plain vtable dispatch. +// ───────────────────────────────────────────────────────────────────── + +fn make_shuffled_devirt_send(n: usize) -> Vec> { + let mut v: Vec> = Vec::with_capacity(n); + for i in 0..n { + let bucket = (i * 7 + 3) % 10; + v.push(match bucket { + 0..=3 => Box::new(Circle { radius: 5.0 }), + 4..=7 => Box::new(Rect { w: 3.0, h: 4.0 }), + 8 => Box::new(Triangle { a: 3.0, b: 4.0, c: 5.0 }), + _ => Box::new(Hexagon { side: 1.5 }), + }); + } + v +} + +fn make_shuffled_plain_send(n: usize) -> Vec> { + let mut v: Vec> = Vec::with_capacity(n); + for i in 0..n { + let bucket = (i * 7 + 3) % 10; + v.push(match bucket { + 0..=3 => Box::new(Circle { radius: 5.0 }), + 4..=7 => Box::new(Rect { w: 3.0, h: 4.0 }), + 8 => Box::new(Triangle { a: 3.0, b: 4.0, c: 5.0 }), + _ => Box::new(Hexagon { side: 1.5 }), + }); + } + v +} + +fn bench_shuffled_send(c: &mut Criterion) { + let mut group = c.benchmark_group("shuffled_send"); + + for &n in &[10_usize, 100, 1000] { + let label_devirt = format!("devirt_n{n}"); + group.bench_function(&label_devirt, |b| { + let shapes = make_shuffled_devirt_send(n); + b.iter(|| { + let mut total = 0.0_f64; + for s in &shapes { + total += black_box(s.as_ref()).area(); + } + total + }); + }); + + let label_plain = format!("plain_n{n}"); + group.bench_function(&label_plain, |b| { + let shapes = make_shuffled_plain_send(n); + b.iter(|| { + let mut total = 0.0_f64; + for s in &shapes { + total += black_box(s.as_ref()).area(); + } + total + }); + }); + } + + group.finish(); +} + criterion_group!( benches, bench_area, bench_scale, bench_mixed_vec, - bench_shuffled_mixed + bench_shuffled_mixed, + bench_shuffled_send ); criterion_main!(benches); diff --git a/crates/core/tests/equivalence.rs b/crates/core/tests/equivalence.rs index 3cc3c3e..d274437 100644 --- a/crates/core/tests/equivalence.rs +++ b/crates/core/tests/equivalence.rs @@ -95,6 +95,55 @@ fn decl_dispatch() { assert_eq!(c.val, 100); } +// ── Auto-trait dispatch: dyn Trait + Send / Sync / Send + Sync ────────────── + +#[cfg(feature = "macros")] +#[test] +fn attr_auto_trait_dispatch() { + // Verify that dispatch through &(dyn T + Send), &(dyn T + Sync), + // and &(dyn T + Send + Sync) produces the same results as &dyn T + // for both hot and cold types. + + // --- &self, non-void (hot) --- + let h = Hot { val: 42 }; + let base = (&h as &dyn attr::T).get(); + assert_eq!((&h as &(dyn attr::T + Send)).get(), base); + assert_eq!((&h as &(dyn attr::T + Sync)).get(), base); + assert_eq!((&h as &(dyn attr::T + Send + Sync)).get(), base); + + // --- &self, non-void (cold) --- + let c = Cold { val: 42 }; + let base = (&c as &dyn attr::T).get(); + assert_eq!((&c as &(dyn attr::T + Send)).get(), base); + assert_eq!((&c as &(dyn attr::T + Sync)).get(), base); + assert_eq!((&c as &(dyn attr::T + Send + Sync)).get(), base); + + // --- &self, void --- + (&h as &(dyn attr::T + Send)).notify(1); + (&h as &(dyn attr::T + Sync)).notify(1); + (&h as &(dyn attr::T + Send + Sync)).notify(1); + + // --- &mut self, non-void (hot) --- + let mut h1 = Hot { val: 10 }; + let mut h2 = Hot { val: 10 }; + let expected = (&mut h1 as &mut dyn attr::T).transform(5); + assert_eq!( + (&mut h2 as &mut (dyn attr::T + Send)).transform(5), + expected, + ); + + // --- &mut self, void (hot) --- + let mut h3 = Hot { val: 10 }; + let mut h4 = Hot { val: 10 }; + (&mut h3 as &mut dyn attr::T).reset(99); + (&mut h4 as &mut (dyn attr::T + Send)).reset(99); + assert_eq!(h3.val, h4.val); + + // --- Box --- + let boxed: Box = Box::new(Hot { val: 7 }); + assert_eq!(boxed.get(), 7); +} + // ── Extended proc-macro tests: supertraits, method lifetimes, #[must_use] ── #[cfg(feature = "macros")] diff --git a/crates/core/tests/ui_attr.rs b/crates/core/tests/ui_attr.rs index cc97806..5301ac6 100644 --- a/crates/core/tests/ui_attr.rs +++ b/crates/core/tests/ui_attr.rs @@ -12,6 +12,7 @@ fn ui_attr() { t.pass("tests/ui_attr/attr_method_lifetimes.rs"); t.pass("tests/ui_attr/attr_supertraits.rs"); t.pass("tests/ui_attr/attr_must_use.rs"); + t.pass("tests/ui_attr/attr_dyn_send.rs"); t.compile_fail("tests/ui_attr/attr_must_use_unused.rs"); t.compile_fail("tests/ui_attr/attr_missing_args.rs"); t.compile_fail("tests/ui_attr/attr_unsafe_missing_on_impl.rs"); diff --git a/crates/core/tests/ui_attr/attr_dyn_send.rs b/crates/core/tests/ui_attr/attr_dyn_send.rs new file mode 100644 index 0000000..7c551f4 --- /dev/null +++ b/crates/core/tests/ui_attr/attr_dyn_send.rs @@ -0,0 +1,53 @@ +struct Hot { val: u64 } +struct Cold { val: u64 } + +#[devirt::devirt(Hot)] +pub trait Shape { + fn area(&self) -> f64; + fn scale(&mut self, factor: f64); +} + +#[devirt::devirt] +impl Shape for Hot { + fn area(&self) -> f64 { self.val as f64 } + fn scale(&mut self, factor: f64) { self.val = (self.val as f64 * factor) as u64; } +} + +#[devirt::devirt] +impl Shape for Cold { + fn area(&self) -> f64 { self.val as f64 + 1.0 } + fn scale(&mut self, factor: f64) { self.val = (self.val as f64 * factor) as u64 + 1; } +} + +fn use_ref(s: &dyn Shape) -> f64 { s.area() } +fn use_send_ref(s: &(dyn Shape + Send)) -> f64 { s.area() } +fn use_sync_ref(s: &(dyn Shape + Sync)) -> f64 { s.area() } +fn use_send_sync_ref(s: &(dyn Shape + Send + Sync)) -> f64 { s.area() } + +fn use_send_mut(s: &mut (dyn Shape + Send), f: f64) { s.scale(f); } + +fn use_boxed_send(s: Box) -> f64 { s.area() } + +fn main() { + let h = Hot { val: 42 }; + + assert_eq!(use_ref(&h), 42.0); + assert_eq!(use_send_ref(&h), 42.0); + assert_eq!(use_sync_ref(&h), 42.0); + assert_eq!(use_send_sync_ref(&h), 42.0); + + // Cold type — falls through to vtable + let c = Cold { val: 42 }; + assert_eq!(use_send_ref(&c), 43.0); + assert_eq!(use_sync_ref(&c), 43.0); + assert_eq!(use_send_sync_ref(&c), 43.0); + + // &mut self through dyn Trait + Send + let mut h2 = Hot { val: 10 }; + use_send_mut(&mut h2, 2.0); + assert_eq!(h2.val, 20); + + // Box + let boxed: Box = Box::new(Hot { val: 5 }); + assert_eq!(use_boxed_send(boxed), 5.0); +} diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 7831058..92c5e19 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -191,6 +191,20 @@ fn emit_trait_expansion( }) .collect(); + // Delegating methods for auto-trait inherent impls (Send, Sync, + // Send + Sync). Each method coerces `self` to the base `dyn Trait` + // and calls the dispatch method. + let delegating_methods: Vec<_> = trait_item + .items + .iter() + .filter_map(|item| { + let syn::TraitItem::Fn(m) = item else { + return None; + }; + Some(generate_delegating_method(m, name)) + }) + .collect(); + // Inner trait supertraits: `__FooImpl: Debug + Clone` let inner_supers = if supertraits.is_empty() { quote! {} @@ -257,6 +271,21 @@ fn emit_trait_expansion( #(#dispatch_methods)* } + // (4a) dyn Trait + Send — delegate to base dispatch. + impl<'__devirt> dyn #name + ::core::marker::Send + '__devirt { + #(#delegating_methods)* + } + + // (4b) dyn Trait + Sync — delegate to base dispatch. + impl<'__devirt> dyn #name + ::core::marker::Sync + '__devirt { + #(#delegating_methods)* + } + + // (4c) dyn Trait + Send + Sync — delegate to base dispatch. + impl<'__devirt> dyn #name + ::core::marker::Send + ::core::marker::Sync + '__devirt { + #(#delegating_methods)* + } + // (5) Public marker trait. #(#outer_attrs)* #vis #unsafety trait #name: #public_supers {} @@ -268,6 +297,53 @@ fn emit_trait_expansion( .into() } +// ── Shared helpers ────────────────────────────────────────────────────────── + +/// Clone a method signature, replacing wildcard `_` patterns with +/// generated names so arguments can be forwarded. Returns the +/// rewritten signature and a list of argument identifiers (excluding +/// `self`). +fn rewrite_sig_with_named_args( + sig: &syn::Signature, +) -> (syn::Signature, Vec) { + let mut sig = sig.clone(); + let mut arg_names = Vec::new(); + for (idx, arg) in sig.inputs.iter_mut().enumerate() { + if let syn::FnArg::Typed(pat_type) = arg { + match &*pat_type.pat { + syn::Pat::Ident(pat_ident) => { + arg_names.push(pat_ident.ident.clone()); + } + syn::Pat::Wild(_) => { + let generated = format_ident!("__devirt_arg{idx}"); + *pat_type.pat = syn::Pat::Ident(syn::PatIdent { + attrs: vec![], + by_ref: None, + mutability: None, + ident: generated.clone(), + subpat: None, + }); + arg_names.push(generated); + } + _ => { + // Validation already rejects this case, but + // generate a name defensively. + let generated = format_ident!("__devirt_arg{idx}"); + *pat_type.pat = syn::Pat::Ident(syn::PatIdent { + attrs: vec![], + by_ref: None, + mutability: None, + ident: generated.clone(), + subpat: None, + }); + arg_names.push(generated); + } + } + } + } + (sig, arg_names) +} + // ── Dispatch method generation ────────────────────────────────────────────── fn generate_dispatch_method( @@ -295,35 +371,7 @@ fn generate_dispatch_method( let is_mut = receiver.mutability.is_some(); let is_unsafe = sig.unsafety.is_some(); - // Build the dispatch signature, replacing wildcard `_` patterns - // with generated names so we can forward arguments. - let mut dispatch_sig = sig.clone(); - let mut arg_names: Vec = Vec::new(); - for (idx, arg) in dispatch_sig.inputs.iter_mut().enumerate() { - if let syn::FnArg::Typed(pat_type) = arg { - match &*pat_type.pat { - syn::Pat::Ident(pat_ident) => { - arg_names.push(pat_ident.ident.clone()); - } - syn::Pat::Wild(_) => { - let generated = format_ident!("__arg{idx}"); - *pat_type.pat = syn::Pat::Ident(syn::PatIdent { - attrs: vec![], - by_ref: None, - mutability: None, - ident: generated.clone(), - subpat: None, - }); - arg_names.push(generated); - } - _ => { - // Validation already rejects this case, but - // generate a name defensively. - arg_names.push(format_ident!("__arg{idx}")); - } - } - } - } + let (dispatch_sig, arg_names) = rewrite_sig_with_named_args(sig); let raw_parts = if is_mut { quote! { let __raw = ::__devirt_raw_parts(&*self); } @@ -397,6 +445,68 @@ fn gen_hot_checks( .collect() } +// ── Delegating method generation (auto-trait impls) ───────────────────────── + +/// Generate a delegating shim that coerces `&(dyn Trait + Send)` (or +/// `Sync`, `Send + Sync`) to `&dyn Trait` and calls the base dispatch +/// method. Auto traits do not change the vtable layout, so the +/// coercion is zero-cost and LLVM eliminates the delegation entirely +/// with `#[inline(always)]`. +fn generate_delegating_method( + method: &syn::TraitItemFn, + trait_name: &syn::Ident, +) -> proc_macro2::TokenStream { + let sig = &method.sig; + let method_name = &sig.ident; + let attrs = &method.attrs; + + let receiver = sig + .inputs + .first() + .and_then(|a| { + if let syn::FnArg::Receiver(r) = a { + Some(r) + } else { + None + } + }) + .expect("validated: method has receiver"); + + let is_mut = receiver.mutability.is_some(); + let is_unsafe = sig.unsafety.is_some(); + let (dispatch_sig, arg_names) = rewrite_sig_with_named_args(sig); + + // Coerce to the base `dyn Trait` and call the dispatch method. + let coerce_and_call = if is_mut { + quote! { + let __devirt_base: &mut (dyn #trait_name + '__devirt) = self; + __devirt_base.#method_name(#(#arg_names),*) + } + } else { + quote! { + let __devirt_base: &(dyn #trait_name + '__devirt) = self; + __devirt_base.#method_name(#(#arg_names),*) + } + }; + + // When the method is `unsafe fn`, the base dispatch method is also + // `unsafe fn`, so the call must be inside an `unsafe` block. + let delegation = if is_unsafe { + quote! { unsafe { #coerce_and_call } } + } else { + coerce_and_call + }; + + quote! { + #(#attrs)* + #[doc(hidden)] + #[inline(always)] + pub #dispatch_sig { + #delegation + } + } +} + // ── Impl expansion ────────────────────────────────────────────────────────── fn expand_impl(attr: &TokenStream, impl_item: &syn::ItemImpl) -> TokenStream {