trustee icon indicating copy to clipboard operation
trustee copied to clipboard

Use of RPITIT vs async-trait macro in the project

Open mkulke opened this issue 1 year ago • 2 comments

In Rust 1.75 Return-Position-Impl-Trait-In-Traits (RPITIT) has been stabilized. In principle (w/ a few restrictions still) it'll enable "native" async traits, since this is allowed now:

trait Foo {
   fn bar() -> impl Future<Output=()>;
   // sugered:
   // async fn bar() -> ();
}

We are currently using the async-trait crate, which is, under the hood, turning implementations of async traits into boxed trait objects (-> Pin<Box<dyn Future<Output=()>>> + Send + 'async) and dynamically dispatching function calls.

Our use case is mostly to statically choose implementations of certain interfaces (policy engine, storage backend,...) that happen to be async. We don't change those at runtime, so dynamic dispatch wouldn't be required, but it is accidentally, due to the limitations of async in Rust. I don't think the performance overhead is a concern for us, but async-awaits mandates a degree of unfortunate type and lifetime gymnastics that leak into the codebase.

With RPITIT, in theory, we could drop the crate and gain back some ergonomics w/ simplified code. I spiked something here: https://github.com/mkulke/async-trait-playground that demonstrates picking an async implementation at compile-time and using it as shared state in a route handler.

In practice, usage of the async-trait macro has been very prevalent in the ecosystem, so it's likely that today libraries like actix and tonic are somewhat entangled w/ it and have to be fixed to support native async-traits eventually (if at all). fwiw I didn't hit any issues with axum, but actix-web shared state doesn't seem to work ootb.

mkulke avatar Jan 08 '24 09:01 mkulke