Skip to content

Commit 46f3b82

Browse files
authored
Merge pull request #92 from ThatGeoGuy/status-quo
Status Quo: Grace integrates a C API
2 parents 84f1d26 + ce364df commit 46f3b82

File tree

1 file changed

+194
-0
lines changed

1 file changed

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

Comments
 (0)