Microsoft Async Case Study
Background
Microsoft uses async Rust in several projects both internally and externally. In this case study, we will focus on how async is used in one project in particular.
This project manages and interacts with low level hardware resources. Performance and resource efficiency is key. Async Rust has proven useful not just because of it enables scalability and efficient use of resources, but also because features such as cancel-on-drop semantics simplify the interaction between components.
Due to our constrainted, low-level environment, we use a custom executor but rely heavily on ecosystem crates to reduce the amount of custom code needed in our executor.
Async Trait Usage
The project makes regular use of async traits. Since these are not yet supported
by the language, we have instead used the async-trait
crate.
For the most part this works well, but sometimes the overhead introduced by
boxing the returned fututures is unacceptable. For those cases, we use
StackFuture, which allows us to emulate a dyn Future
while storing it in
space provided by the caller.
Now that there is built in support for async in traits in the nightly compiler,
we have tried porting some of our async traits away from the async-trait
crate.
For many of these the transformation was simple. We merely had to remove the
#[async_trait]
attribute on the trait and all of its implementations. For
example, we had one trait that looks similar to this:
#![allow(unused)] fn main() { #[async_trait] pub trait BusControl { async fn offer(&self) -> Result<()>; async fn revoke(&self) -> Result<()>; } }
There were several implementations of this trait as well. In this case, all we
needed to do was remove the #[async_trait]
annotation.
Send Bounds
In about half the cases, we needed methods to return a future that was Send
.
This happens by default with #[async_trait]
, but not when using the built-in
feature.
In these cases, we instead manually desugared the async fn
definition so we
could add additional bounds. Although these bounds applied at the trait
definition site, and therefore to all implementors, we have not found this to be
a deal breaker in practice.
As an example, one trait that required a manual desugaring looked like this:
#![allow(unused)] fn main() { pub trait Component: 'static + Send + Sync { async fn save<'a>( &'a mut self, writer: StateWriter<'a>, ) -> Result<(), SaveError>; async fn restore<'a>( &'a mut self, reader: StateReader<'a>, ) -> Result<(), RestoreError>; } }
The desugared version looked like this:
#![allow(unused)] fn main() { pub trait Component: 'static + Send + Sync { fn save<'a>( &'a mut self, writer: StateWriter<'a>, ) -> impl Future<Output = Result<(), SaveError>> + Send + 'a; fn restore<'a>( &'a mut self, reader: StateReader<'a>, ) -> impl Future<Output = Result<(), RestoreError>> + Send + 'a; } }
This also required a change to all the implementation sites since we were
migrating from async_trait
. This was slightly tedious but basically a
mechanical change.
Dyn Trait Workaround
We use trait objects in several places to support heterogenous collections of data that implements a certain trait. Rust nightly does not currently have built-in support for this, so we needed to find a workaround.
The workaround that we have used so far is to create a DynTrait
version of
each Trait
that we need to use as a trait object. One example is the
Component
trait shown above. For the Dyn
version, we basically just
duplicate the definition but apply #[async_trait]
to this one. Then we add a
blanket implementation so that we can triviall get a DynTrait
for any trait
that has a Trait
implementation. As an example:
#![allow(unused)] fn main() { #[async_trait] pub trait DynComponent: 'static + Send + Sync { async fn save<'a>( &'a mut self, writer: StateWriter<'a>, ) -> Result<(), SaveError>; async fn restore<'a>( &'a mut self, reader: StateReader<'a>, ) -> Result<(), RestoreError>; } #[async_trait] impl<T: Component> DynComponent for T { async fn save(&mut self, writer: StateWriter<'_>) -> Result<(), SaveError> { <Self as Component>::save(self, writer).await } async fn restore( &mut self, reader: StateReader<'_>, ) -> Result<(), RestoreError> { <Self as Component>::restore(self, reader).await } } }
It is a little annoying to have to duplicate the trait definition and write a
blanket implementation, but there are some mitigating factors. First of all,
this only needs to be done in once per trait and can conveniently be done next
to the non-Dyn
version of the trait. Outside of that crate or module, the user
just has to remember to use dyn DynTrait
instead of just DynTrait
.
The second mitigating factor is that this is a mechanical change that could easily be automated using a proc macro (although we have not done so).
Return Type Notation
Since there is an active PR implementing Return Type Notation (RTN), we
gave that a try as well. The most obvious place this was applicable was on the
Component
trait we have already looked at. This turned out to be a little
tricky. Because async_trait
did not know how to parse RTN bounds, we had to
forego the use of #[async_trait]
and use a manually expanded version where
each async function returned Pin<Box<dyn Future<Output = ...> + Send + '_>>
.
Once we did this, we needed to add save(..): Send
and restore(..): Send
bounds to the places where the Component
trait was used as a DynComponent
.
There were only two methods we needed to bound, but this was still mildly
annoying.
The #[async_trait]
limitation will likely go away almost immediately once the
RTN PR merges, since it can be updated to support the new syntax. Still, this
experience does highlight one thing, which is that the DynTrait
workaround
requires us to commit up front to whether that trait will guarantee Send
futures or not. There does not seem to be an obvious way to push this decision
to the use site like there is with Return Type Notation.
This is something we will want to keep in mind with any macros we create to help automate these transformations as well as with built in language support for async functions in trait objects.
Conclusion
We have not seen any insurmountable problems with async functions in traits as they are currently implemented in the compiler. That said, it would be significantly more ergonomic with a few more improvements, such as:
- A way to specify
Send
bounds without manually desugaring a function. Return type notation looks like it would work, but even in our limited experience with it we have run into ergonomics issues. - A way to simplify
dyn Trait
support. Language-provided support would be ideal, but a macro that automates something like ourDynTrait
workaround would be acceptable too.
That said, we are eager to be able to use this feature in production!