|
| 1 | +# 😱 Status quo stories: Grace wants to integrate a C-API |
| 2 | + |
| 3 | +[Alan]: ../characters/alan.md |
| 4 | +[Grace]: ../characters/grace.md |
| 5 | +[Niklaus]: ../characters/niklaus.md |
| 6 | +[Barbara]: ../characters/barbara.md |
| 7 | + |
| 8 | +[bindgen]: //docs.rs/bindgen/ |
| 9 | +[`stream::unfold`]: //docs.rs/futures/0.1.17/futures/stream/fn.unfold.html |
| 10 | + |
| 11 | +## 🚧 Warning: Draft status 🚧 |
| 12 | + |
| 13 | +This is a draft "status quo" story submitted as part of the brainstorming period. It is derived from real-life |
| 14 | +experiences of actual Rust users and is meant to reflect some of the challenges that Async Rust programmers face today. |
| 15 | + |
| 16 | +## The story |
| 17 | + |
| 18 | +[Grace] is integrating a camera into an embedded project. Grace has done similar projects before in the past, and has |
| 19 | +even used this particular hardware before. Fortunately, the camera manufacturer provides a library in C to interface |
| 20 | +with the driver. |
| 21 | + |
| 22 | +Grace knows that Rust provides strong memory safety guarantees, and the library provided by the manufacturer sports an |
| 23 | +API that is easy to misuse. In particular, ownership concerns are tricky and Grace and her team have often complained in |
| 24 | +the past that making memory mistakes is very easy and one has to be extremely careful to manage lifetimes. Therefore, |
| 25 | +for this project, Grace opts to start with Rust as many of the pitfalls of the manufacturer's library can be |
| 26 | +automatically caught by embedding the lifetimes into a lightweight wrapper over code bridged into Rust with [bindgen]. |
| 27 | + |
| 28 | +Grace's team manages to write a thin Rust wrapper over the manufacturer's library with little complication. This library |
| 29 | +fortunately offers two interfaces for grabbing frames from the camera: a blocking interface that waits for the next |
| 30 | +frame, and a non-blocking interface that polls to check if there are any frames currently available and waiting. Grace |
| 31 | +is tempted to write a callback-based architecture by relying on the blocking interface that waits; however, early the |
| 32 | +next morning the customer comes back and informs her that they are scaling up the system, and that there will now be 5 |
| 33 | +cameras instead of 1. |
| 34 | + |
| 35 | +She knows from experience that she cannot rely on having 5 threads blocking just for getting camera frames, because the |
| 36 | +embedded system she is deploying to only has 2 cores total! Her team would be introducing a lot of overhead into the |
| 37 | +system with the continuous context switching of every thread. Some folks were unsure of Rust's asynchronous |
| 38 | +capabilities, and with the requirements changing there were some that argued maybe they should stick to the tried and |
| 39 | +true in pure C. However, Grace eventually convinced them that the benefits of memory safety were still applicable, and |
| 40 | +that a lot of bugs that have taken weeks to diagnose in the past have already been completely wiped out. The team |
| 41 | +decided to stick with Rust, and dig deeper into implementing this project in async Rust. |
| 42 | + |
| 43 | +Fortunately, Grace notices the similarities between the polling interface in the underlying C library and the `Poll` |
| 44 | +type returned by Rust's `Future` trait. "Surely," she thinks, "I can asynchronously interleave polls to each camera over |
| 45 | +a single thread, and process frames as they become available!" Such a thing would be quite difficult in C while |
| 46 | +guaranteeing memory safety was maintained. However, Grace's team has already dodged that bullet thanks to writing a thin |
| 47 | +wrapper in Rust that manages these tricky lifetimes! |
| 48 | + |
| 49 | +### The first problem: polls and wake-ups |
| 50 | + |
| 51 | +Grace sets out to start writing the pipeline to get frames from the cameras. She realizes that while the polling call |
| 52 | +that the manufacturer provided in their library is similar in nature to a future, it doesn't quite encompass everything. |
| 53 | +In C, one might have to set some kind of heartbeat timer for polling. Grace explains to her team that this heartbeat is |
| 54 | +similar to how the `Waker` object works in a `Future`'s `Context` type, in that it is how often the execution |
| 55 | +environment should re-try the future if the call to `poll` returns `Poll::Pending`. |
| 56 | + |
| 57 | +A member of Grace's team asks her how she was able to understand all this. After all, Grace had been writing Rust about |
| 58 | +as long as the rest of her team. The main difference was that she had many more years of systems programming under C and |
| 59 | +C++ under her belt than they had. Grace responded that for the most part she had just read the documentation for the |
| 60 | +`Future` trait, and that she had intuited how async-await de-sugars itself into a regular function that returns a future |
| 61 | +of some kind. The de-sugaring process was, after all, very similar to how lambda objects in C++ were de-sugared as well. |
| 62 | +She leaves her teammate with [an |
| 63 | +article](//smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/) she once found online that |
| 64 | +explained the process in a lot more detail for a problem much harder than they were trying to solve. |
| 65 | + |
| 66 | +Something Grace and her team learn to love immediately about Rust is that writing the `Future` here does not require her |
| 67 | +team to write their own execution environment. In fact, the future can be entirely written independently of the |
| 68 | +execution environment. She quickly writes an async method to represent the polling process: |
| 69 | + |
| 70 | +```rust |
| 71 | +/// Gets the next frame from the camera, waiting `retry_after` time until polling again if it fails. |
| 72 | +/// |
| 73 | +/// Returns Some(frame) if a frame is found, or None if the camera is disconnected or goes down before a frame is |
| 74 | +/// available. |
| 75 | +async fn next_frame(camera: &Camera, retry_after: Duration) -> Option<Frame> { |
| 76 | + while camera.is_available() { |
| 77 | + if let Some(frame) = camera.poll() { |
| 78 | + return Some(frame); |
| 79 | + } else { |
| 80 | + task::sleep_for(retry_after).await; |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + None |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +The underlying C API doesn't provide any hooks that can be used to wake the `Waker` object on this future up, so Grace |
| 89 | +and her team decide that it is probably best if they just choose a sufficiently balanced `retry_after` period in which |
| 90 | +to try again. It does feel somewhat unsatisfying, as calling `sleep_for` feels about as hacky as calling |
| 91 | +`std::this_thread::sleep_for` in C++. However, there is no way to directly interoperate with the waker without having a |
| 92 | +separate thread of execution wake it up, and the underlying C library doesn't have any interface offering a notification |
| 93 | +for when that should be. In the end, this is the same kind of code that they would write in C, just without having to |
| 94 | +implement a custom execution loop themselves, so the team decides it is not a total loss. |
| 95 | + |
| 96 | +### The second problem: doing this many times |
| 97 | + |
| 98 | +Doing this a single time is fine, but an end goal of the project is to be able to stream frames from the camera for |
| 99 | +unspecified lengths of time. Grace spends some time searching, and realizes that what she actually wants is a `Stream` |
| 100 | +of some kind. `Stream` objects are the asynchronous equivalent of iterators, and her team wants to eventually write |
| 101 | +something akin to: |
| 102 | + |
| 103 | +```rust |
| 104 | +let frame_stream = stream_from_camera(camera, Duration::from_millis(5)); |
| 105 | + |
| 106 | +while let Some(frame) = frame_stream.next().await { |
| 107 | + // process frames |
| 108 | +} |
| 109 | + |
| 110 | +println!("Frame stream closed."); |
| 111 | +``` |
| 112 | + |
| 113 | +She scours existing crates, in particular looking for one way to transform the above future into a stream that can be |
| 114 | +executed many times. The only available option to transform a future into a series of futures is [`stream::unfold`], |
| 115 | +which seems to do exactly what Grace is looking for. Grace begins by adding a small intermediate type, and then plugging |
| 116 | +in the remaining holes: |
| 117 | + |
| 118 | +```rust |
| 119 | +struct StreamState { |
| 120 | + camera: Camera, |
| 121 | + retry_after: Duration, |
| 122 | +} |
| 123 | + |
| 124 | +fn stream_from_camera(camera: Camera, retry_after: Duration) -> Unfold<Frame, ??, ??> { |
| 125 | + let initial_state = StreamState { camera, retry_after }; |
| 126 | + |
| 127 | + stream::unfold(initial_state, |state| async move { |
| 128 | + let frame = next_frame(&state.camera, state.retry_after).await |
| 129 | + (frame, state) |
| 130 | + }) |
| 131 | +} |
| 132 | +``` |
| 133 | + |
| 134 | +This looks like it mostly hits the mark, but Grace is left with a couple of questions for how to get the remainder of |
| 135 | +this building: |
| 136 | + |
| 137 | +1. What is the type that fills in the third template parameter in the return? It should be the type of the future that |
| 138 | + is returned by the async closure passed into `stream::unfold`, but we don't know the type of a closure! |
| 139 | +2. What is the type that fills in the second template parameter of the closure in the return? |
| 140 | + |
| 141 | +Grace spends a lot of time trying to figure out how she might find those types! She asks [Barbara] what the idiomatic |
| 142 | +way to get around this in Rust would be. Barbara explains again how closures don't have concrete types, and that the |
| 143 | +only way to do this will be to use the `impl` keyword. |
| 144 | + |
| 145 | +```rust |
| 146 | +fn stream_from_camera(camera: Camera, retry_after: Duration) -> impl Stream<Item = Frame> { |
| 147 | + // same as before |
| 148 | +} |
| 149 | +``` |
| 150 | + |
| 151 | +While Grace was was on the correct path and now her team is able to write the code they want to, she realizes that |
| 152 | +sometimes writing the types out explicitly can be very hard. She reflects on what it would have taken to write the type |
| 153 | +of an equivalent function pointer in C, and slightly laments that Rust cannot express such as clearly. |
| 154 | + |
| 155 | +## 🤔 Frequently Asked Questions |
| 156 | + |
| 157 | +* **What are the morals of the story?** |
| 158 | + * Rust was the correct choice for the team across the board thanks to its memory safety and ownership. The |
| 159 | + underlying C library was just too complex for any single programmer to be able to maintain in their head all at |
| 160 | + once while also trying to accomplish other tasks. |
| 161 | + * Evolving requirements meant that the team would have had to either start over in plain C, giving up a lot of the |
| 162 | + safety they would gain from switching to Rust, or exploring async code in a more rigorous way. |
| 163 | + * The async code is actually much simpler than writing the entire execution loop in C themselves. However, the |
| 164 | + assumption that you would write the entire execution loop is baked into the underlying library which Grace's team |
| 165 | + cannot rewrite entirely from scratch. Integrating Rust async code with other languages which might have different |
| 166 | + mental models can sometimes lead to unidiomatic or unsatisfying code, even if the intent of the code in Rust is |
| 167 | + clear. |
| 168 | + * Grace eventually discovered that the problem was best modeled as a stream, rather than as a single future. |
| 169 | + However, converting a future into a stream was not necessarily something that was obvious for someone with a C/C++ |
| 170 | + background. |
| 171 | + * Closures and related types can be very hard to write in Rust, and if you are used to being very explicit with your |
| 172 | + types, tricks such as the `impl` trick above for `Stream`s aren't immediately obvious at first glance. |
| 173 | +* **What are the sources for this story?** |
| 174 | + * My own personal experience trying to incorporate the Intel RealSense library into Rust. |
| 175 | +* **Why did you choose Grace to tell this story?** |
| 176 | + * I am a C++ programmer who has written many event / callback based systems for streaming from custom camera |
| 177 | + hardware. I mirror Grace in that I am used to using other systems languages, and even rely on libraries in those |
| 178 | + languages as I've moved to Rust. I did not want to give up the memory and lifetime benefits of Rust because of |
| 179 | + evolving runtime requirements. |
| 180 | + * In particular, C and C++ do not encourage async-style code, and often involve threads heavily. However, some |
| 181 | + contexts cannot make effective use of threads. In such cases, C and C++ programmers are often oriented towards |
| 182 | + writing custom execution loops and writing a lot of logic to do so. Grace discovered the benefit of not having to |
| 183 | + choose an executor upfront, because the async primitives let her express most of the logic without relying on a |
| 184 | + particular executor's behaviour. |
| 185 | +* **How would this story have played out differently for the other characters?** |
| 186 | + * [Alan] would have struggled with understanding the embedded context of the problem, where GC'd languages don't see |
| 187 | + much use. |
| 188 | + * [Niklaus] and [Barbara] may not have approached the problem with the same assimilation biases from C and C++ as |
| 189 | + Grace. Some of the revelations in the story such as discovering that Grace's team didn't have to write their own |
| 190 | + execution loop were unexpected benefits when starting down the path of using Rust! |
| 191 | +* **Could Grace have used another runtime to achieve the same objectives?** |
| 192 | + * Grace can use _any_ runtime, which was an unexpected benefit of her work! |
| 193 | +* **How did Grace know to use `Unfold` as the return type in the first place?** |
| 194 | + * She saw it in the [rustdoc](https://docs.rs/futures/0.3.13/futures/stream/fn.unfold.html) for `stream::unfold`. |
0 commit comments