Add dyn compatible variant to your async trait with 🦕 dynify!
## The problem
Currently, dynamic dispatch on AFIT (Async Fn In Trait) is not possible in Rust.
For the following code:
```rust,compile_fail
trait AsyncRead {
async fn read_to_string(&mut self) -> String;
}
async fn dynamic_dispatch(reader: &dyn AsyncRead) {
// ...
}
```
compiler will give you errors like this:
```text
error[E0038]: the trait `AsyncRead` cannot be made into an object
--> src/lib.rs:12:36
|
|
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically
```
## The solution
dynify implements partial features of the experimental
[in-place initialization proposal](https://github.com/rust-lang/lang-team/issues/336),
which makes it possible to create a dyn compatible variant for `AsyncRead`:
```rust
# trait AsyncRead {
# async fn read_to_string(&mut self) -> String;
# }
use dynify::{from_fn, Dynify, Fn};
use std::future::Future;
use std::mem::MaybeUninit;
trait DynAsyncRead {
// `Fn!()` returns a dyn compatible type for the original async function.
fn read_to_string(&mut self) -> Fn!(&mut Self => dyn '_ + Future<Output = String>);
}
impl<T: AsyncRead> DynAsyncRead for T {
fn read_to_string(&mut self) -> Fn!(&mut Self => dyn '_ + Future<Output = String>) {
// While `from_fn!()` lets you create a constructor of such type.
from_fn!(T::read_to_string, self)
}
}
// Now we can use dynamic dispatched `AsyncRead`!
async fn dynamic_dispatch(reader: &mut dyn DynAsyncRead) {
// Prepare containers, we will see how they are used soon.
let mut stack = [MaybeUninit::<u8>::uninit(); 16];
let mut heap = Vec::<MaybeUninit<u8>>::new();
// `read_to_string` returns a constructor, which can be considered as a
// function pointer to `AsyncRead::read_to_string` along with all necessary
// arguments to invoke it.
let init = reader.read_to_string();
// Therefore, we need to initialize the constructor to obtain the actual
// `Future` before going on. `Dynify` offers a set of convenient methods to
// do this. Since the size of the `Future` object is determined at runtime,
// we can't know in advance what size containers can fit it. Here we use
// `init2` to select a appropriate container for it. It accepts two
// containers:
let fut = init.init2(
// the first one is allocated on the stack, allowing us to put the
// object there to avoid relatively costly heap allocations.
&mut stack,
// the second one is allocated on the heap, serving as a fallback if the
// size of the object exceeds the capacity of our stack.
&mut heap,
);
// Finally, we get the `Future`. Now poll it to obtain the output!
let content = fut.await;
// ...
}
```
## Why not async-trait?
[async-trait](https://crates.io/crates/async-trait) is the most popular crate
for the aforementioned problem. However, it may not play well with limited
environments such as kernels or embedded systems, as it transforms every
`async fn()` into `fn() -> Box<dyn Future>`, requiring heap allocation. dynify
doesn't have such limitation, since you can decide where to place trait objects.
Additionally, you can opt out of the `alloc` feature to completely turn off heap
allocation.
Furthermore, dynify offers some unique features compared to async-trait. One of
them, as shown in the example below, is the ability to reuse buffers across
different trait objects:
```rust
# use dynify::{from_fn, Dynify, Fn, PinDynify};
# use std::future::Future;
# use std::mem::MaybeUninit;
# use std::pin::Pin;
trait Stream {
type Item;
async fn next(&mut self) -> Option<Self::Item>;
}
trait DynStream {
type Item;
fn next(&mut self) -> Fn!(&mut Self => dyn '_ + Future<Output = Option<Self::Item>>);
fn next_boxed(&mut self) -> Pin<Box<dyn '_ + Future<Output = Option<Self::Item>>>>;
}
async fn process_stream(stream: &mut dyn DynStream<Item = char>) {
let mut stack = [MaybeUninit::<u8>::uninit(); 16];
let mut heap = Vec::<MaybeUninit<u8>>::new();
// With dynify, all items are stored in the same buffer.
while let Some(item) = stream.next().init2(&mut stack, &mut heap).await {
// ...
}
// While with async-trait, every item is stored in a unique `Box`.
while let Some(item) = stream.next_boxed().await {
// ...
}
}
```
Nevertheless, the differences can be rather trivial in many cases. If you don't
have these concerns, it's better to go with the battle tested async-trait.
## Features
- **alloc**: Enable container implementations for types that require heap
allocation such as `Box` and `Vec`.
- **smallvec**: Enable container implementations for [`SmallVec`], a drop-in
replacement for `[u8; N] + Vec<u8>`.
- **macros**: Enable helpful procedural macros.
[`SmallVec`]: smallvec::SmallVec