Skip to content

Commit 71898e2

Browse files
committed
add the embassy / microsoft case studies
1 parent 44062ef commit 71898e2

File tree

3 files changed

+224
-0
lines changed

3 files changed

+224
-0
lines changed

SUMMARY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
- [Builder + Provider API](./evaluation/case-studies/builder-provider-api.md)
5252
- [Socket Handler Refactor](./evaluation/case-studies/socket-handler.md)
5353
- [Tower](./evaluation/case-studies/tower.md)
54+
- [Microsoft AFIT](./evaluation/case-studies/microsoft.md)
55+
- [Use of AFIT in Embassy](./evaluation/case-studies/embassy.md)
5456
- [📚 Explainer](./explainer.md)
5557
- [Async fn in traits](./explainer/async_fn_in_traits.md)
5658
- [Async fn in dyn trait](./explainer/async_fn_in_dyn_trait.md)

evaluation/case-studies/embassy.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Use of AFIT in Embaassy
2+
3+
*The following are rough notes on the usage of Async Function in Traits from the [Embassy][] runtime. They are derived from a conversation between dirbaio and nikomatsakis.*
4+
5+
[Embassy]: https://github.com/embassy-rs/embassy
6+
7+
Links to uses of async functions in traits within Embassy:
8+
9+
* most popular ones are [embedded-hal-async](https://github.com/rust-embedded/embedded-hal/tree/master/embedded-hal-async/src)
10+
* HAL crates provide impls for particular microcontrollers (e.g., [gpiote](https://github.com/embassy-rs/embassy/blob/master/embassy-nrf/src/gpiote.rs#L518), [spim](https://github.com/embassy-rs/embassy/blob/master/embassy-nrf/src/spim.rs#L523), [i2c](https://github.com/embassy-rs/embassy/blob/master/embassy-stm32/src/i2c/v2.rs#L1061))
11+
* driver crates use the traits to [implement a driver for some chip that works on top of any HAL:
12+
* [nrf70](https://github.com/embassy-rs/nrf70/blob/main/src/main.rs#L811) (that one is interesting because it defines another async Bus trait on top, because that chip can be used with either SPI or QSPI)
13+
* [es-wifi-driver](https://github.com/drogue-iot/es-wifi-driver/blob/main/src/lib.rs#L132)
14+
* [hts221](https://github.com/drogue-iot/hts221-async/blob/main/src/lib.rs#L40)
15+
* [sx127x](https://github.com/embassy-rs/embassy/blob/master/embassy-lora/src/sx127x/sx127x_lora/mod.rs#L50)
16+
* there's also embedded-io, which is [std::io traits adapted for embedded](https://github.com/embassy-rs/embedded-io/blob/master/src/asynch.rs)
17+
* HALs have [impls for serial ports](https://github.com/embassy-rs/embassy/blob/master/embassy-nrf/src/buffered_uarte.rs#L600)
18+
* embassy-net has an [impl for TCP sockets](https://github.com/embassy-rs/embassy/blob/master/embassy-net/src/tcp.rs#L431)
19+
* here's some [driver using it](https://github.com/drogue-iot/esp8266-at-driver/blob/main/src/lib.rs#L74)
20+
* embassy-usb has a [Driver trait](https://github.com/embassy-rs/embassy/blob/master/embassy-usb-driver/src/lib.rs); that one is probably the most complex, it's an async trait with associated types with more async traits, and interesting lifetimes
21+
* HALs [impl these traits for one particular chip](https://github.com/embassy-rs/embassy/blob/master/embassy-nrf/src/usb/mod.rs#L178) and embassy-usb uses them to [implement a chip-independent USB stack](https://github.com/embassy-rs/embassy/blob/master/embassy-usb/src/lib.rs#L188)
22+
23+
24+
most of these are "abstract over hardware", and when you build a firmware for some product/board you know which actual hardware you have, so you use static generics, no need for dyn
25+
26+
the few instances I've wished for dyn is:
27+
* with embedded-io it does sometimes happen. For example, running the same terminal ui over a physical serial port and over telnet at the same time. Without dyn that code gets monomorphized two times, which is somewhat wasteful.
28+
* this trait https://github.com/embassy-rs/embassy/blob/master/embassy-usb/src/lib.rs#L89 . That one MUST use dyn because you want to register multiple handlers that might be different types. Sometimes it'd have been handy to be able to do async things within these callbacks. Workaround is to fire off a notification to some other async task, it's not been that bad.
29+
* niko: how is this used?
30+
* handlers are added [here](https://github.com/embassy-rs/embassy/blob/master/embassy-usb/src/builder.rs#L262), passed into UsbDevice [here](https://github.com/embassy-rs/embassy/blob/master/embassy-usb/src/lib.rs#L225), and then called when handling some bus-related stuff, for example [here](https://github.com/embassy-rs/embassy/blob/master/embassy-usb/src/lib.rs#L714-L715).
31+
* the tldr of what it's used for is you might have a "composite" usb device, which can have multiple "classes" at the same time (say, an Ethernet adapter and a serial port). Each class gets its own "endpoints" for data, so each launches its own independent async tasks reading/writing to these endpoints.
32+
* But there's also a "control pipe" endpoint that carries "control" requests that can be for any class for example for ethernet there's control requests for "bring the ethernet interface up/down", so each class registers a handler with callbacks to handle their own control requests, there's a "control pipe" task that dispatches them.
33+
* Sometimes when handling them, you want to do async stuff. For example for "bring the ethernet interface up" you might want to do some async SPI transfer to the ethernet chip, but currently you can't.
34+
* niko: would all methods be async if you could?
35+
* not sure if all methods, but probably `control_in`/`control_out` yes. and about where to store the future for the dyn... not sure. That crate is no-alloc so Box is out it'd probably be inline in the stack, like with StackFuture. Would need configuring the max size, probably some compile-time setting, or a const-generic in UsbDevice.

evaluation/case-studies/microsoft.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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

Comments
 (0)