|
| 1 | +# Microsoft Async Case Study |
| 2 | + |
| 3 | +## Background |
| 4 | + |
| 5 | +Microsoft uses async Rust in several projects both internally and externally. In |
| 6 | +this case study, we will focus on how async is used in one project in |
| 7 | +particular. |
| 8 | + |
| 9 | +This project manages and interacts with low level hardware resources. |
| 10 | +Performance and resource efficiency is key. Async Rust has proven useful not |
| 11 | +just because of it enables scalability and efficient use of resources, but also |
| 12 | +because features such as cancel-on-drop semantics simplify the interaction |
| 13 | +between components. |
| 14 | + |
| 15 | +Due to our constrainted, low-level environment, we use a custom executor but |
| 16 | +rely heavily on ecosystem crates to reduce the amount of custom code needed in |
| 17 | +our executor. |
| 18 | + |
| 19 | +## Async Trait Usage |
| 20 | + |
| 21 | +The project makes regular use of async traits. Since these are not yet supported |
| 22 | +by the language, we have instead used the [`async-trait`] crate. |
| 23 | + |
| 24 | +For the most part this works well, but sometimes the overhead introduced by |
| 25 | +boxing the returned fututures is unacceptable. For those cases, we use |
| 26 | +[StackFuture], which allows us to emulate a `dyn Future` while storing it in |
| 27 | +space provided by the caller. |
| 28 | + |
| 29 | +Now that there is built in support for async in traits in the nightly compiler, |
| 30 | +we have tried porting some of our async traits away from the [`async-trait`] |
| 31 | +crate. |
| 32 | + |
| 33 | +For many of these the transformation was simple. We merely had to remove the |
| 34 | +`#[async_trait]` attribute on the trait and all of its implementations. For |
| 35 | +example, we had one trait that looks similar to this: |
| 36 | + |
| 37 | +```rust |
| 38 | +#[async_trait] |
| 39 | +pub trait BusControl { |
| 40 | + async fn offer(&self) -> Result<()>; |
| 41 | + async fn revoke(&self) -> Result<()>; |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +There were several implementations of this trait as well. In this case, all we |
| 46 | +needed to do was remove the `#[async_trait]` annotation. |
| 47 | + |
| 48 | +### Send Bounds |
| 49 | + |
| 50 | +In about half the cases, we needed methods to return a future that was `Send`. |
| 51 | +This happens by default with `#[async_trait]`, but not when using the built-in |
| 52 | +feature. |
| 53 | + |
| 54 | +In these cases, we instead manually desugared the `async fn` definition so we |
| 55 | +could add additional bounds. Although these bounds applied at the trait |
| 56 | +definition site, and therefore to all implementors, we have not found this to be |
| 57 | +a deal breaker in practice. |
| 58 | + |
| 59 | +As an example, one trait that required a manual desugaring looked like this: |
| 60 | + |
| 61 | +```rust |
| 62 | +pub trait Component: 'static + Send + Sync { |
| 63 | + async fn save<'a>( |
| 64 | + &'a mut self, |
| 65 | + writer: StateWriter<'a>, |
| 66 | + ) -> Result<(), SaveError>; |
| 67 | + |
| 68 | + async fn restore<'a>( |
| 69 | + &'a mut self, |
| 70 | + reader: StateReader<'a>, |
| 71 | + ) -> Result<(), RestoreError>; |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +The desugared version looked like this: |
| 76 | + |
| 77 | +```rust |
| 78 | +pub trait Component: 'static + Send + Sync { |
| 79 | + fn save<'a>( |
| 80 | + &'a mut self, |
| 81 | + writer: StateWriter<'a>, |
| 82 | + ) -> impl Future<Output = Result<(), SaveError>> + Send + 'a; |
| 83 | + |
| 84 | + fn restore<'a>( |
| 85 | + &'a mut self, |
| 86 | + reader: StateReader<'a>, |
| 87 | + ) -> impl Future<Output = Result<(), RestoreError>> + Send + 'a; |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +This also required a change to all the implementation sites since we were |
| 92 | +migrating from `async_trait`. This was slightly tedious but basically a |
| 93 | +mechanical change. |
| 94 | + |
| 95 | +### Dyn Trait Workaround |
| 96 | + |
| 97 | +We use trait objects in several places to support heterogenous collections of |
| 98 | +data that implements a certain trait. Rust nightly does not currently have |
| 99 | +built-in support for this, so we needed to find a workaround. |
| 100 | + |
| 101 | +The workaround that we have used so far is to create a `DynTrait` version of |
| 102 | +each `Trait` that we need to use as a trait object. One example is the |
| 103 | +`Component` trait shown above. For the `Dyn` version, we basically just |
| 104 | +duplicate the definition but apply `#[async_trait]` to this one. Then we add a |
| 105 | +blanket implementation so that we can triviall get a `DynTrait` for any trait |
| 106 | +that has a `Trait` implementation. As an example: |
| 107 | + |
| 108 | +```rust |
| 109 | +#[async_trait] |
| 110 | +pub trait DynComponent: 'static + Send + Sync { |
| 111 | + async fn save<'a>( |
| 112 | + &'a mut self, |
| 113 | + writer: StateWriter<'a>, |
| 114 | + ) -> Result<(), SaveError>; |
| 115 | + |
| 116 | + async fn restore<'a>( |
| 117 | + &'a mut self, |
| 118 | + reader: StateReader<'a>, |
| 119 | + ) -> Result<(), RestoreError>; |
| 120 | +} |
| 121 | + |
| 122 | +#[async_trait] |
| 123 | +impl<T: Component> DynComponent for T { |
| 124 | + async fn save(&mut self, writer: StateWriter<'_>) -> Result<(), SaveError> { |
| 125 | + <Self as Component>::save(self, writer).await |
| 126 | + } |
| 127 | + |
| 128 | + async fn restore( |
| 129 | + &mut self, |
| 130 | + reader: StateReader<'_>, |
| 131 | + ) -> Result<(), RestoreError> { |
| 132 | + <Self as Component>::restore(self, reader).await |
| 133 | + } |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +It is a little annoying to have to duplicate the trait definition and write a |
| 138 | +blanket implementation, but there are some mitigating factors. First of all, |
| 139 | +this only needs to be done in once per trait and can conveniently be done next |
| 140 | +to the non-`Dyn` version of the trait. Outside of that crate or module, the user |
| 141 | +just has to remember to use `dyn DynTrait` instead of just `DynTrait`. |
| 142 | + |
| 143 | +The second mitigating factor is that this is a mechanical change that could |
| 144 | +easily be automated using a proc macro (although we have not done so). |
| 145 | + |
| 146 | +### Return Type Notation |
| 147 | + |
| 148 | +Since there is an active PR implementing [Return Type Notation][RTN] (RTN), we |
| 149 | +gave that a try as well. The most obvious place this was applicable was on the |
| 150 | +`Component` trait we have already looked at. This turned out to be a little |
| 151 | +tricky. Because `async_trait` did not know how to parse RTN bounds, we had to |
| 152 | +forego the use of `#[async_trait]` and use a manually expanded version where |
| 153 | +each async function returned `Pin<Box<dyn Future<Output = ...> + Send + '_>>`. |
| 154 | +Once we did this, we needed to add `save(..): Send` and `restore(..): Send` |
| 155 | +bounds to the places where the `Component` trait was used as a `DynComponent`. |
| 156 | +There were only two methods we needed to bound, but this was still mildly |
| 157 | +annoying. |
| 158 | + |
| 159 | +The `#[async_trait]` limitation will likely go away almost immediately once the |
| 160 | +RTN PR merges, since it can be updated to support the new syntax. Still, this |
| 161 | +experience does highlight one thing, which is that the `DynTrait` workaround |
| 162 | +requires us to commit up front to whether that trait will guarantee `Send` |
| 163 | +futures or not. There does not seem to be an obvious way to push this decision |
| 164 | +to the use site like there is with Return Type Notation. |
| 165 | + |
| 166 | +This is something we will want to keep in mind with any macros we create to help |
| 167 | +automate these transformations as well as with built in language support for |
| 168 | +async functions in trait objects. |
| 169 | + |
| 170 | +[`async-trait`]: https://crates.io/crates/async-trait |
| 171 | +[StackFuture]: https://crates.io/crates/stackfuture |
| 172 | +[RTN]: https://github.com/rust-lang/rust/pull/109010 |
| 173 | + |
| 174 | +## Conclusion |
| 175 | + |
| 176 | +We have not seen any insurmountable problems with async functions in traits as |
| 177 | +they are currently implemented in the compiler. That said, it would be |
| 178 | +significantly more ergonomic with a few more improvements, such as: |
| 179 | + |
| 180 | +* A way to specify `Send` bounds without manually desugaring a function. Return |
| 181 | + type notation looks like it would work, but even in our limited experience |
| 182 | + with it we have run into ergonomics issues. |
| 183 | +* A way to simplify `dyn Trait` support. Language-provided support would be |
| 184 | + ideal, but a macro that automates something like our `DynTrait` workaround |
| 185 | + would be acceptable too. |
| 186 | + |
| 187 | +That said, we are eager to be able to use this feature in production! |
0 commit comments