Expand description
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:
trait AsyncRead {
async fn read_to_string(&mut self) -> String;
}
async fn dynamic_dispatch(reader: &dyn AsyncRead) {
// ...
}
compiler will give you errors like this:
error[E0038]: the trait `AsyncRead` cannot be made into an object
--> src/lib.rs:12:36
|
7 | async fn dynamic_dispatch(reader: &dyn AsyncRead) {
| ^^^^^^^^^^^^^ `AsyncRead` cannot be made into an object
|
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, which makes
it possible to create a dyn compatible variant for AsyncRead
:
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 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:
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
andVec
. - smallvec: Enable container implementations for
SmallVec
, a drop-in replacement for[u8; N] + Vec<u8>
.
Macros§
Structs§
- Boxed
- A unit type to perform constructions in
Box
. - Buffered
- A pointer to objects stored in buffers.
- Opaque
- A opaque wrapper of initialized objects.
- OutOf
Capacity - An error thrown by buffers with fixed capacity.
- Slot
- A memory block used to store arbitrary objects.
Traits§
- Construct
- A marker for constructors that do not require pinned containers.
- Dynify
- The main interface used to perform in-place object constructions.
- Emplace
- A one-time container used for in-place constructions.
- PinConstruct
- The core trait to package necessary information for object constructions.
- PinDynify
- A variant of
Dynify
that requires pinned containers. - PinEmplace
- A variant of
Emplace
used for pinned constructions.
Functions§
- from_
closure - Creates a new closure constructor.