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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Shape + Send>` and `&(dyn Shape + Send + Sync)`
benefit from devirtualization without any extra annotation:

```rust
fn total_area_send(shapes: &[Box<dyn Shape + Send>]) -> 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
Expand Down
70 changes: 69 additions & 1 deletion crates/core/benches/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Shape +
// Send>` 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<Box<dyn Shape + Send>> {
let mut v: Vec<Box<dyn Shape + Send>> = 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<Box<dyn PlainShape + Send>> {
let mut v: Vec<Box<dyn PlainShape + Send>> = 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);
49 changes: 49 additions & 0 deletions crates/core/tests/equivalence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn T + Send> ---
let boxed: Box<dyn attr::T + Send> = Box::new(Hot { val: 7 });
assert_eq!(boxed.get(), 7);
}

// ── Extended proc-macro tests: supertraits, method lifetimes, #[must_use] ──

#[cfg(feature = "macros")]
Expand Down
1 change: 1 addition & 0 deletions crates/core/tests/ui_attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
53 changes: 53 additions & 0 deletions crates/core/tests/ui_attr/attr_dyn_send.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Shape + Send>) -> 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<dyn Trait + Send>
let boxed: Box<dyn Shape + Send> = Box::new(Hot { val: 5 });
assert_eq!(use_boxed_send(boxed), 5.0);
}
Loading