|
| 1 | +# 😱 Status quo stories: Alan needs async in traits |
| 2 | + |
| 3 | +## 🚧 Warning: Draft status 🚧 |
| 4 | + |
| 5 | +This is a draft "status quo" story submitted as part of the brainstorming period. It is derived from real-life experiences of actual Rust users and is meant to reflect some of the challenges that Async Rust programmers face today. |
| 6 | + |
| 7 | +If you would like to expand on this story, or adjust the answers to the FAQ, feel free to open a PR making edits (but keep in mind that, as they reflect peoples' experiences, status quo stories [cannot be wrong], only inaccurate). Alternatively, you may wish to [add your own status quo story][htvsq]! |
| 8 | + |
| 9 | +## The story |
| 10 | + |
| 11 | +Alan is working on a project with Barbara which has already gotten off to a [somewhat rocky start](./barbara_anguishes_over_http.md). He is working on abstracting away the HTTP implementation the library uses so that users can provide their own. He wants the user to implement an async trait called `HttpClient` which has one method `perform(request: Request) -> Response`. Alan tries to create the async trait: |
| 12 | + |
| 13 | +```rust |
| 14 | +trait HttpClient { |
| 15 | + async fn perform(request: Request) -> Response; |
| 16 | +} |
| 17 | +``` |
| 18 | + |
| 19 | +When Alan tries to compile this, he gets an error: |
| 20 | + |
| 21 | +``` |
| 22 | + --> src/lib.rs:2:5 |
| 23 | + | |
| 24 | +2 | async fn perform(request: Request) -> Response; |
| 25 | + | -----^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 26 | + | | |
| 27 | + | `async` because of this |
| 28 | + | |
| 29 | + = note: `async` trait functions are not currently supported |
| 30 | + = note: consider using the `async-trait` crate: https://crates.io/crates/async-trait |
| 31 | +``` |
| 32 | + |
| 33 | +Alan, who has been using Rust for a little while now, has learned to follow compiler error messages and adds `async-trait` to his `Cargo.toml`. Alan follows the README of `async-trait` and comes up with the following code: |
| 34 | + |
| 35 | +```rust |
| 36 | +#[async_trait] |
| 37 | +trait HttpClient { |
| 38 | + async fn perform(request: Request) -> Response; |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +Alan's code now compiles, but he also finds that his compile times have gone from under a second to around 6s, at least for a clean build. |
| 43 | + |
| 44 | +After Alan finishes adding the new trait, he shows his work off to Barbara and mentions he's happy with the work but is a little sad that compile times have worsened. Barbara, an experienced Rust developer, knows that using `async-trait` comes with some additional issues. In this particular case she is especially worried about tying their public API to a third-party dependency. Even though it is technically possible to implement traits annotated with `async_trait` without using `async_trait`, doing so in practice is very painful. For example `async_trait`: |
| 45 | + |
| 46 | +* handles lifetimes for you if the returned future is tied to the lifetime of some inputs. |
| 47 | +* boxes and pins the futures for you. |
| 48 | + |
| 49 | +which the implementer will have to manually handle if they don't use `async_trait`. She decides to not worry Alan with this right now. Alan and Barbara are pretty happy with the results and go on to publish their crate which gets lots of users. |
| 50 | + |
| 51 | +Later on, a potential user of the library wants to use their library in a `no_std` context where they will be providing a custom HTTP stack. Alan and Barbara have done a pretty good job of limiting the use of standard library features and think it might be possible to support this use case. However, they quickly run into a show stopper: `async-trait` boxes all of the futures returned from a async trait function. They report this to Alan through an issue. |
| 52 | + |
| 53 | +Alan, feeling (over-) confident in his Rust skills, decides to try to see if he can implement async traits without using `async-trait`. |
| 54 | + |
| 55 | +```rust |
| 56 | +trait HttpClient { |
| 57 | + type Response: Future<Output = Response>; |
| 58 | + |
| 59 | + fn perform(request: Request) -> Self::Response; |
| 60 | +} |
| 61 | +``` |
| 62 | + |
| 63 | +Alan seems to have something working, but when he goes to update the examples of how to implement this trait in his crate's documentation, he realizes that he either needs to: |
| 64 | + |
| 65 | +* use trait object: |
| 66 | + |
| 67 | + ```rust |
| 68 | + struct ClientImpl; |
| 69 | + |
| 70 | + impl HttpClient for ClientImpl { |
| 71 | + type Response = Pin<Box<dyn Future<Output = Response>>>; |
| 72 | + |
| 73 | + fn perform(request: Request) -> Self::Response { |
| 74 | + Box::pin(async move { |
| 75 | + // Some async work here creating Reponse |
| 76 | + }) |
| 77 | + } |
| 78 | + } |
| 79 | + ``` |
| 80 | + |
| 81 | + which wouldn't work for `no_std`. |
| 82 | + |
| 83 | +* implement `Future` trait manually, which isn't particulary easy/straight-forward for non-trivial cases, especially if it involves making other async calls (likely). |
| 84 | + |
| 85 | +After a lot of thinking and discussion, Alan and Barbara accept that they won't be able to support `no_std` users of their library and add mention of this in crate documentation. |
| 86 | + |
| 87 | +## 🤔 Frequently Asked Questions |
| 88 | + |
| 89 | +### **What are the morals of the story?** |
| 90 | + |
| 91 | +* `async-trait` is awesome, but has some drawbacks |
| 92 | + * compile time increases |
| 93 | + * performance cost of boxing and dynamic dispatch |
| 94 | + * not a standard solution so when this comes to language, it might break things |
| 95 | +* Trying to have a more efficient implementation than `async-trait` is likely not possible. |
| 96 | + |
| 97 | +### **What are the sources for this story?** |
| 98 | + |
| 99 | +* [Zeeshan](https://github.com/zeenix/) is looking for a way to implement async version of the [service-side zbus API](https://docs.rs/zbus/1.9.1/zbus/trait.Interface.html). |
| 100 | +* [Ryan](https://github.com/rylev) had to use `async-trait` in an internal project. |
| 101 | + |
| 102 | +### **Why did you choose Alan to tell this story?** |
| 103 | + |
| 104 | +We could have used Barbara here but she'd probably know some of the work-arounds (likely even the details on why they're needed) and wouldn't need help so it wouldn't make for a good story. Having said that, Barbara is involved in the story still so it's not a pure Alan story. |
| 105 | + |
| 106 | +### **How would this story have played out differently for the other characters?** |
| 107 | + |
| 108 | +* Barbara: See above. |
| 109 | +* Grace: Probably won't know the solution to these issues much like Alan, but might have an easier time understanding the **why** of the whole situation. |
| 110 | +* Niklaus: would be lost - traits are somewhat new themselves. This is just more complexity, and Niklaus might not even know where to go for help (outside of compiler errors). |
0 commit comments