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")