Asynchronous Rust

Async Traits and Dynamic Dispatch

Stabilized in Rust 1.75, native async fn in traits (AFIT) and Return-Position impl Trait in Traits (RPITIT) allow defining and implementing async fn methods directly inside standard trait declarations without external procedural macro crates (#[async_trait]) for static dispatch.

Static Dispatch with Native async fn in Traits

When a trait declares async fn fetch(&self, id: u64) -> String, each implementing type generates its own concrete state-machine future with zero heap allocation (Box) and full compiler inlining when used with generic static dispatch (T: DataStore or impl DataStore):

use std::future::Future;
use std::pin::pin;
use std::task::{Context, Poll, Waker};

pub trait KeyValueStore {
    async fn get(&self, key: &str) -> Option<String>;
}

struct MemoryStore;

impl KeyValueStore for MemoryStore {
    async fn get(&self, key: &str) -> Option<String> {
        std::future::ready(Some(format!("value-for-{key}"))).await
    }
}

async fn lookup_user(store: &impl KeyValueStore, username: &str) -> String {
    store.get(username).await.unwrap_or_else(|| String::from("missing"))
}

fn main() {
    let store = MemoryStore;
    let mut fut = pin!(lookup_user(&store, "prime"));
    let mut cx = Context::from_waker(Waker::noop());

    println!("static async trait lookup: {:?}", fut.as_mut().poll(&mut cx));
}
static async trait lookup: Ready("value-for-prime")

Requiring Send Bounds on Returned Trait Futures (Return-Type Notation & RPITIT)

In a public library trait used with multi-threaded executors, a generic caller fn spawn_lookup<S: KeyValueStore + Send + Sync>(s: S) needs to know that the future returned by s.get(key) implements Send.

In stable Rust, write the trait method using explicit -> impl Future<Output = ...> + Send so every implementor's returned future is guaranteed to be Send:

use std::future::Future;

pub trait SendableStore: Send + Sync {
    // Explicitly constrains the returned opaque Future to implement `Send`:
    fn get(&self, key: &str) -> impl Future<Output = Option<String>> + Send;
}

Why async fn Traits Are Not Directly Dyn-Compatible (dyn Trait) and How to Bridge Them

A trait containing async fn (or -> impl Future) is not directly dyn-compatible (Box<dyn KeyValueStore> fails with E0038) because each concrete implementor's async fn state machine has a different byte size on the stack, whereas a dyn Trait vtable requires a single fixed return layout!

To use an async trait as a trait object (&dyn DynKeyValueStore or Box<dyn DynKeyValueStore>), return a pinned boxed future Pin<Box<dyn Future<Output = T> + Send + '_>>—or provide an automatic blanket-implemented object-safe bridge trait over your zero-cost native async fn trait:

Zero-Cost Native Trait + Automatic `dyn` Bridge Pattern:

  1. `trait Store { async fn get(&self, k: &str) -> Option<String>; }`
     ---> Zero heap allocation for generic callers (`impl Store`)!

  2. `trait DynStore { fn get_boxed<'a>(&'a self, k: &'a str) -> Pin<Box<dyn Future<...> + 'a>>; }`
     + Blanket `impl<T: Store> DynStore for T`
     ---> Enables `Box<dyn DynStore>` for heterogeneous dynamic collections!
use std::future::Future;
use std::pin::{Pin, pin};
use std::task::{Context, Poll, Waker};

pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;

// Dyn-compatible async trait using `BoxFuture<'_, T>`:
pub trait DynService: Send + Sync {
    fn call<'a>(&'a self, request: &'a str) -> BoxFuture<'a, String>;
}

struct EchoService;

impl DynService for EchoService {
    fn call<'a>(&'a self, request: &'a str) -> BoxFuture<'a, String> {
        Box::pin(async move { format!("echo: {request}") })
    }
}

fn main() {
    let services: Vec<Box<dyn DynService>> = vec![Box::new(EchoService)];

    let mut fut = pin!(services[0].call("hello dyn async"));
    let mut cx = Context::from_waker(Waker::noop());
    println!("dyn service result: {:?}", fut.as_mut().poll(&mut cx));
}
dyn service result: Ready("echo: hello dyn async")