Skip to content

Commit

Permalink
Add basic example and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
tmandry committed Sep 15, 2024
1 parent 9035733 commit d75cd5b
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 84 deletions.
24 changes: 24 additions & 0 deletions Cargo.lock

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

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
members = [
"dynosaur",
"dynosaur_derive",
]
members = ["dynosaur", "dynosaur_derive"]
resolver = "2"

[workspace.dependencies]
tokio = { version = "1.40.0", features = ["macros", "rt"] }
84 changes: 18 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,91 +1,43 @@
[![Latest Version]][crates.io] [![Documentation]][docs.rs] [![GHA Status]][GitHub Actions] ![License]

The core goal is to make `async fn` in trait and other RPITIT usable
with dynamic dispatch via a proc macro, in a way similar to how it works
with async_trait, but while continuing to allow static dispatch.

The core idea of how it works is described here:
https://rust-lang.github.io/async-fundamentals-initiative/explainer/async_fn_in_dyn_trait/hardcoding_box.html

Given a trait like:
dynosaur lets you use dynamic dispatch on traits with with `async fn` and
methods returning `impl Trait`.

```rust,ignore
use dynosaur::dynosaur;
#[dynosaur(DynMyTrait)]
trait MyTrait {
#[dynosaur::dynosaur(DynNext)]
trait Next {
type Item;
async fn foo(&self) -> Self::Item;
async fn next(&self) -> Self::Item;
}
```

Instead of using a type like `Box<dyn MyTrait>` which wouldn't compile
today, we would have the proc macro create a type `DynMyTrait` with this
interface:
The macro above generates a type called `DynNext` which can be used like this:

```rust,ignore
impl<'a, Item> DynMyTrait<'a, Item> {
fn new<T>(value: T) -> DynMyTrait<'a, Item>
where
T: MyTrait<Item = Item> + 'a,
Item: 'a,
{
unimplemented!()
async fn dyn_dispatch(iter: &mut DynNext<'_, i32>) {
while let Some(item) = iter.next().await {
println!("- {item}");
}
}
impl<'a, Item> MyTrait for DynMyTrait<'a, Item> {
type Item = Item;
fn foo(&self) -> impl std::future::Future<Output = Self::Item> { /* return Pin<Box<dyn Future<Output = Item> here */ }
}
```

The struct itself would look something like this. A full explanation is
in the links and in the exposition below.

```rust
struct DynMyTrait<'a, Item> {
ptr: *mut (dyn ErasedMyTrait<Item = Item> + 'a),
}

trait ErasedMyTrait {
type Item;
fn foo<'a>(&'a self) -> ::core::pin::Pin<Box<dyn ::core::future::Future<Output = Self::Item> + 'a>>;
}
let a = [1, 2, 3];
dyn_dispatch(DynNext::from_mut(&mut a.into_iter())).await;
```

A full example including macro output is here:
https://github.com/nikomatsakis/dyner/blob/main/src/async_iter.rs.
Notes:
The general rule is that anywhere you would write `dyn Trait` (which would
result in a compiler error), you instead write `DynTrait`.

* The `ErasedAsyncIter` trait is used to have the compiler generate a
vtable for each concrete type that is used to create a `DynAsyncIter`.
It is not part of the public interface. `*mut dyn ErasedAsyncIter` is
a fat pointer.
* Note the use of `Ref` and `RefMut` wrapper types (which would go in
some support library) so that we can also have
`DynAsyncIter::from_ref` and `DynAsyncIter::from_ref_mut`. These
wrappers are slightly verbose, but due to their deref impls, can be
reborrowed to create `&DynMyTrait` and `&mut DynMyTrait` respectively.
* This code uses GATs instead of RPITIT in the original trait, since
those weren't stable yet, but the same ideas should apply.
* This code makes use of a union to do bit-twiddling on the data
pointer, as a way of marking whether the underlying value is owned.
This is not well-defined ~and should _not_ be necessary, because we
can instead have `Ref<DynAsyncIter>`'s field be
`ManuallyDrop<DynAsyncIter>`~ EDIT: Actually we probably do need
something like this if we have `RefMut` which gives out `&mut
DynAsyncIter`; we just need to make it well-defined by using std
pointer APIs.
Methods returning `impl Trait` box their return types when dispatched
dynamically, but not when dispatched statically.

#### License and usage notes

Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or
[MIT license](LICENSE-MIT) at your option.

[GitHub Actions]: https://github.com/spastorino/impl-trait-utils/actions
[GHA Status]: https://github.com/spastorino/impl-trait-utils/actions/workflows/rust.yml/badge.svg
[crates.io]: https://crates.io/crates/trait-variant
[GitHub Actions]: https://github.com/spastorino/dynosaur/actions
[GHA Status]: https://github.com/spastorino/dynosaur/actions/workflows/rust.yml/badge.svg
[crates.io]: https://crates.io/crates/dynosaur
[Latest Version]: https://img.shields.io/crates/v/dynosaur.svg
[Documentation]: https://img.shields.io/docsrs/dynosaur
[docs.rs]: https://docs.rs/dynosaur
Expand Down
10 changes: 9 additions & 1 deletion dynosaur/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
[package]
name = "dynosaur"
version = "0.1.0"
authors = ["Santiago Pastorino <spastorino@gmail.com>", "Tyler Mandry <tmandry@google.com>", "Niko Matsakis <rust@nikomatsakis.com>"]
authors = [
"Santiago Pastorino <spastorino@gmail.com>",
"Tyler Mandry <tmandry@google.com>",
"Niko Matsakis <rust@nikomatsakis.com>",
]
categories = ["asynchronous", "no-std"]
keywords = ["async", "trait", "impl"]
description = "Dynamic dispatch for return position impl traits and async in Rust"
Expand All @@ -15,8 +19,12 @@ rust-version = "1.75"
dynosaur_derive = { version = "0.1", path = "../dynosaur_derive" }

[dev-dependencies]
tokio = { workspace = true }
ui_test = "0.24"

[[example]]
name = "next"

[[test]]
name = "ui"
harness = false
70 changes: 70 additions & 0 deletions dynosaur/examples/next.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#[dynosaur::dynosaur(DynNext)]
trait Next {
type Item;
async fn next(&mut self) -> Option<Self::Item>;
}

async fn dyn_dispatch(iter: &mut DynNext<'_, i32>) {
print!("dyn_dispatch: ");
while let Some(item) = iter.next().await {
print!("{item} ");
}
println!();
}

async fn static_dispatch(mut iter: impl Next<Item = i32>) {
print!("static_dispatch: ");
while let Some(item) = iter.next().await {
print!("{item} ");
}
println!();
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
let v = [1, 2, 3];
dyn_dispatch(&mut DynNext::boxed(from_iter(v))).await;
dyn_dispatch(DynNext::from_mut(&mut from_iter(v))).await;
static_dispatch(from_iter(v)).await;
static_dispatch(DynNext::boxed(from_iter(v))).await;
static_dispatch(DynNext::from_mut(&mut from_iter(v))).await;
}

impl<S: Next + ?Sized> Next for Box<S> {
type Item = S::Item;
fn next(&mut self) -> impl core::future::Future<Output = Option<Self::Item>> {
S::next(&mut *self)
}
}

impl<S: Next + ?Sized> Next for &mut S {
type Item = S::Item;
fn next(&mut self) -> impl core::future::Future<Output = Option<Self::Item>> {
S::next(&mut *self)
}
}

struct ForArray<T: Copy, const N: usize> {
v: [T; N],
i: usize,
}

fn from_iter<T: Copy, const N: usize>(v: [T; N]) -> ForArray<T, N> {
ForArray { v, i: 0 }
}

impl<T: Copy, const N: usize> Next for ForArray<T, N> {
type Item = T;

async fn next(&mut self) -> Option<Self::Item> {
if self.i >= self.v.len() {
return None;
}
stuff().await;
let item = self.v[self.i];
self.i += 1;
Some(item)
}
}

async fn stuff() {}
9 changes: 8 additions & 1 deletion dynosaur_derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
[package]
name = "dynosaur_derive"
version = "0.1.0"
authors = ["Santiago Pastorino <spastorino@gmail.com>", "Tyler Mandry <tmandry@google.com>", "Niko Matsakis <rust@nikomatsakis.com>"]
authors = [
"Santiago Pastorino <spastorino@gmail.com>",
"Tyler Mandry <tmandry@google.com>",
"Niko Matsakis <rust@nikomatsakis.com>",
]
categories = ["asynchronous", "no-std"]
keywords = ["async", "trait", "impl"]
description = "Dynamic dispatch for return position impl traits and async in Rust"
Expand All @@ -18,3 +22,6 @@ proc-macro = true
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full", "visit-mut"] }

[dev-dependencies]
tokio = { workspace = true }
76 changes: 64 additions & 12 deletions dynosaur_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,78 @@ impl Parse for Attrs {
}
}

/// Given a trait like
/// Create a struct that takes the place of `dyn Trait` for a trait, supporting
/// both `async` and `-> impl Trait` methods.
///
/// ```rust,ignore
/// use dynosaur::dynosaur;
///
/// #[dynosaur(DynMyTrait)]
/// trait MyTrait {
/// ```
/// # mod dynosaur { pub use dynosaur_derive::dynosaur; }
/// #[dynosaur::dynosaur(DynNext)]
/// trait Next {
/// type Item;
/// async fn foo(&self) -> Self::Item;
/// async fn next(&self) -> Option<Self::Item>;
/// }
/// # // This is necessary to prevent weird scoping errors in the doctets:
/// # fn main() {}
/// ```
///
/// The above example causes the trait to be rewritten as:
/// Here, the struct is named `DynNext`. It can be used like this:
///
/// ```rust
/// trait DynMyTrait {
/// type Item;
/// fn foo(&self) -> ::core::pin::Pin<Box<dyn ::core::future::Future<Output = Self::Item>>>;
/// ```
/// # mod dynosaur { pub use dynosaur_derive::dynosaur; }
/// # #[dynosaur::dynosaur(DynNext)]
/// # trait Next {
/// # type Item;
/// # async fn next(&self) -> Option<Self::Item>;
/// # }
/// #
/// # fn from_iter<T: IntoIterator>(v: T) -> impl Next<Item = i32> {
/// # Never
/// # }
/// # struct Never;
/// # impl Next for Never {
/// # type Item = i32;
/// # async fn next(&self) -> Option<Self::Item> { None }
/// # }
/// #
/// # #[tokio::main(flavor = "current_thread")]
/// # async fn main() {
/// #
/// async fn dyn_dispatch(iter: &mut DynNext<'_, i32>) {
/// while let Some(item) = iter.next().await {
/// println!("- {item}");
/// }
/// }
///
/// let a = [1, 2, 3];
/// dyn_dispatch(DynNext::from_mut(&mut from_iter(a))).await;
/// # }
/// ```
///
/// ## Interface
///
/// The `Dyn` struct produced by this macro has the following constructors:
///
/// ```ignore
/// impl<'a> DynTrait<'a> {
/// fn new(from: Box<impl Trait>) -> Box<Self> { ... }
/// fn boxed(from: impl Trait) -> Box<Self> { ... }
/// fn from_ref(from: &'a impl Trait) -> &'a Self { ... }
/// fn from_mut(from: &'a mut impl Trait) -> &'a mut Self { ... }
/// }
/// ```
///
/// Normally a concrete type behind a pointer coerces to `dyn Trait` implicitly.
/// When using the `Dyn` struct created by this macro, such conversions must be
/// done explicitly with the provided constructors.
///
/// ## Performance
///
/// In addition to the normal overhead of dynamic dispatch, calling `async` and
/// `-> impl Trait` methods on a `Dyn` struct will box the returned value so it
/// has a known size.
///
/// There is no performance cost to using this macro when the trait is used with
/// static dispatch.
#[proc_macro_attribute]
pub fn dynosaur(
attr: proc_macro::TokenStream,
Expand Down

0 comments on commit d75cd5b

Please sign in to comment.