Rust 1.75 Async Fn in Traits vs async-trait Crate
0 reputation · 25 Oct 2023, 04:59 UTC
0 reputation · 25 Oct 2023, 04:59 UTC
The stabilization of async fn in traits (AFIT) in Rust 1.75 allows developers to define asynchronous methods directly within traits without relying on the #[async_trait] macro. This change moves the implementation from a boxing-based approach to a compiler-generated state machine.
While native support reduces dependency overhead, the behavior regarding return-type erasure and the Send bound on the resulting Future differs from the macro-based approach. Specifically, when using trait objects (dyn Trait), the compiler requires the future to be Send if the trait is used across thread boundaries, but native AFIT does not automatically provide the boxing required for dynamic dispatch.
Send bound for native async trait methods when the trait must be used as a trait object?async-trait while using the stable 1.75+ feature set?31775 reputation · 25 Oct 2023, 10:14 UTC
In Rust 1.75 an async fn in a trait desugars to a method that returns an opaque type implementing Future. The compiler does **not** automatically add a Send bound to that future, nor does it box the return type. When the trait is used through a trait object (dyn Trait) the caller must know the concrete future type unless you explicitly erase it.
async-traitTo get the erase‑and‑box behaviour that the async-trait macro provided, you need to make the trait method return a boxed, send‑able future yourself. The pattern is:
Pin + Send>> (or the same without + Send if the future never crosses threads).Box::pin..awaited.use std::pin::Pin;
use std::future::Future;
trait MyTrait {
fn method(&self) -> Pin> + Send>>;
}
impl MyTrait for MyStruct {
fn method(&self) -> Pin> + Send>> {
Box::pin(async move {
// async work here
Ok(())
})
}
}
fn use_it(obj: &dyn MyTrait) {
// obj.method() returns a Pin>
// .await works directly
let _ = obj.method().await;
}
If you are certain the trait objects will never be sent between threads, you can drop the + Send from the return type and the trait object itself, which avoids the unnecessary boxing overhead.
The only diagnostic detail that changes the recommendation is whether the trait objects must be Send (i.e., used across thread boundaries). If they do not need to be Send, you can omit the + Send bound and the associated boxing, keeping the zero‑cost abstraction. Please confirm if your use case requires Send on the trait objects.
Use comments to ask for clarification. Post a solution as an answer.
31,775 reputation · 25 Oct 2023, 08:14 UTC
When you define async fn in a trait (AFIT) the compiler desugars it to a method returning an opaque impl Future. That opaque type captures the &self lifetime, so the resulting future is only valid for as long as the reference lives. If you later try to use the trait through a dyn Trait + Send object, the compiler needs the erased future type to be Send + 'static unless you introduce a higher‑ranked lifetime. Two stable ways to satisfy this are:
fn method(&self) -> Pin<Box<dyn Future + Send>> and implement it with Box::pin(async { … }).&self: trait AsyncTrait { type Fut<'a>: Future + Send where Self: 'a; async fn method(&self) -> Self::Fut<'_>; }. Implementations then return the concrete future directly, and dyn AsyncTrait + Send works without extra boxing.Both approaches give the same erasure that async_trait provided; choose the GAT version when you want to avoid heap allocation, and the boxed version when you need the simplest syntax.