owned this note
owned this note
Published
Linked with GitHub
# Deep dive notes: AFIT case studies
Link to document:
* [Async Builder + Provider API Case Study
](https://github.com/compiler-errors/async-fundamentals-initiative/blob/provider-api/evaluation/case-studies/builder-provider-api.md)
* [Microsoft Async Case Study](https://hackmd.io/@eholk/S1LZmEXW3)
* [Netstack3 Async Socket Handler Case Study
](https://github.com/akonradi/async-fundamentals-initiative/blob/netstack3-case-study/evaluation/case-studies/socket-handler.md)
###### tags: `deep-dive`
---
## Notes from embassy
links to uses of async functions in traits within Embassy:
* most popular ones are [embedded-hal-async](https://github.com/rust-embedded/embedded-hal/tree/master/embedded-hal-async/src)
* 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))
* driver crates use the traits to [implement a driver for some chip that works on top of any HAL:
* [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)
* [es-wifi-driver](https://github.com/drogue-iot/es-wifi-driver/blob/main/src/lib.rs#L132)
* [hts221](https://github.com/drogue-iot/hts221-async/blob/main/src/lib.rs#L40)
* [sx127x](https://github.com/embassy-rs/embassy/blob/master/embassy-lora/src/sx127x/sx127x_lora/mod.rs#L50)
* 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)
* HALs have [impls for serial ports](https://github.com/embassy-rs/embassy/blob/master/embassy-nrf/src/buffered_uarte.rs#L600)
* embassy-net has an [impl for TCP sockets](https://github.com/embassy-rs/embassy/blob/master/embassy-net/src/tcp.rs#L431)
* here's some [driver using it](https://github.com/drogue-iot/esp8266-at-driver/blob/main/src/lib.rs#L74)
* 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
* 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)
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
the few instances I've wished for dyn is:
* 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.
* 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.
* niko: how is this used?
* 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).
* 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.
* 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.
* 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.
* niko: would all methods be async if you could?
* 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.
---
Leave questions, observations, discussion topics below.
---
## dyn, rtn, send, etc
nikomatsakis: The Microsoft case study says
> 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.
I *think* this is highlighting an existing challenge with dyn -- i.e., today, if I write an iterator:
```rust
struct MyIteratorOp<I: Iterator> { base_iter: I }
```
and then I do `some_base_iter.my_iterator_op()`, the resulting iterator type (`MyIteratorOp<SomeBaseIter>`) will be `Send` iff `SomeBaseIter` is `Send`.
But if I were to write `MyIteratorOp` to use dyn dispatch:
```rust
struct MyIteratorOp<T> { base_iter: Box<dyn Iterator<Item = T>> }
```
then `MyIteratorOp` would *never* be send, unless I commit up front by making it `dyn Iterator + Send`.
Is this the same general problem encountered, or is there some async specific twist to it?
I think it's a definite footgun we should think about how to solve, but I also think it's an orthogonal point from send bounds in AFIT in general.
eholk: I think for me the main realization here is that there's a version of the Send bound problem for AFIDT too, although I agree with you that it seems more like a limitation of dyn rather than AFIT in particular. I wonder if we could have RTN push the extra bounds into the `dyn*` that gets generated for dyn async traits? e.g.
```rust=
fn foo(x: &dyn Foo<bar(): Send) { ... }
```
becomes something like
```rust
fn foo(x: &dyn Foo<bar() = dyn* BarTrait + Send>) { ... }
```
The coercion story wouldn't be great though, because you have to remember to put Send bounds all the way up the stack.
nikomatsakis: yes, this is what tmandry and I intended had discussed in the past, or something like it.
Yosh (galaxy brain): Could we think of the `dyn` + `Send` definition as (yet another) effect composition problem?
nikomatsakis: I have wondered same thing... certainly feels similar. i.e., being able to make the "send version" of a type and have that propagate down.
---
## Yosh told me you disliked RTN, eholk
nikomatsakis: I was surprised by how positive the microsoft case study sounded because yosh hinted to me that RTN was more painful than expected. I was expecting to read that it was annoying because we had to repeat bounds a lot in some trait with a lot of methods. There was a hint of those, i.e., the trait had 2 methods. Was this the point of ergonomic pain or was there something else?
eholk: I wrote this right after trying RTN, but after some time I felt more negative about it.
eholk: One thing was that before we had the dyn version that used async-trait with a blanket impl. Since async trait didn't support RTN, I had to rewrite the whole thing to return boxes. Async trait will fix this. But having to do that was annoying.
eholk: A lot of punctuation you have to type. `(..): Send` etc, and you have to repeat it for every function. It surprised me to see lower-case letters, e.g. `Foo<blah(): Send>`. I'd like to try one of the more coarse grained things like trait transformers. RTN seemed overly verbose.
nikomatsakis: My favored syntax is `Foo<bar(): Send, baz(): Send>`.
nikomatsakis: I also imagined us shipping a proc macro that would create the dyn + a send alias.
nikomatsakis: it sounds like the min version of this feedback is that we need to have the macro or it's going to be super annoying.
eholk: can we build the macro without stabilizing RTN?
nikomatsakis: by building it into the compiler, this is what we called the tmandry special, but I tend to think we want RTN anyway -- i.e., stabilizing the building block lets people use other trait bounds besides Send or other things.
yoshuawuyts: I have other use cases for RTN besides this one; I think it's a good primitive to make available.
tmandry: it sounds like you have a use case that is not addressed by trait transformers?
yoshuawuyts: correct.
---
nikomatsakis: So
* Fuchsia worked awesome, able to refactor away Send bound needs
* AWS worked fine, would like dyn
* Microsoft doing more complicated stuff surfaced that RTN was kind of an annoying primitive, also would like dyn
eholk: Some of the Send bounds might've been accidental from use of `async_trait`
---