Crate dynify

Source
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 and Vec.
  • smallvec: Enable container implementations for SmallVec, a drop-in replacement for [u8; N] + Vec<u8>.

Macros§

Fn
Determines the constructor type of a function.
from_fn
Creates a constructor for static functions.

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.
OutOfCapacity
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.