This blog post is the second in the Rust quickies series. In my training sessions, we often come up with quick examples to demonstrate some point. Instead of forgetting about them, I want to put short blog posts together focusing on these examples. Hopefully these will be helpful, enjoy!
FP Complete is looking for Rust and DevOps engineers. Interested in working with us?
Check out our jobs page.
Hello Hyper!
For those not familiar, Hyper is an HTTP implementation for Rust, built on top of Tokio. It's a low level library powering frameworks like Warp and Rocket, as well as the reqwest client library. For most people, most of the time, using a higher level wrapper like these is the right thing to do.
But sometimes we like to get our hands dirty, and sometimes working directly with Hyper is the right choice. And definitely from a learning perspective, it's worth doing so at least once. And what could be easier than following the example from Hyper's homepage? To do so, cargo new
a new project, add the following dependencies:
hyper = { version = "0.14", features = ["full"] }
tokio = { version = "1", features = ["full"] }
And add the following to main.rs
:
use std::convert::Infallible;
use std::net::SocketAddr;
use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
async fn hello_world(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
Ok(Response::new("Hello, World".into()))
}
#[tokio::main]
async fn main() {
// We'll bind to 127.0.0.1:3000
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
// A `Service` is needed for every connection, so this
// creates one from our `hello_world` function.
let make_svc = make_service_fn(|_conn| async {
// service_fn converts our function into a `Service`
Ok::<_, Infallible>(service_fn(hello_world))
});
let server = Server::bind(&addr).serve(make_svc);
// Run this server for... forever!
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
}
If you're interested, there's a quick explanation of this code available on Hyper's website. But our focus will be on making an ever-so-minor modification to this code. Let's go!
Counter
Remember the good old days of Geocities websites, where every page had to have a visitor counter? I want that. Let's modify our hello_world
function to do just that:
use std::sync::{Arc, Mutex};
type Counter = Arc<Mutex<usize>>; // Bonus points: use an AtomicUsize instead
async fn hello_world(counter: Counter, _req: Request<Body>) -> Result<Response<Body>, Infallible> {
let mut guard = counter.lock().unwrap(); // unwrap poisoned Mutexes
*guard += 1;
let message = format!("You are visitor number {}", guard);
Ok(Response::new(message.into()))
}
That's easy enough, and now we're done with hello_world
. The only problem is rewriting main
to pass in a Counter
value to it. Let's take a first, naive stab at the problem:
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let counter: Counter = Arc::new(Mutex::new(0));
let make_svc = make_service_fn(|_conn| async {
Ok::<_, Infallible>(service_fn(|req| hello_world(counter, req)))
});
let server = Server::bind(&addr).serve(make_svc);
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
Unfortunately, this fails due to moving out of captured variables. (That's a topic we cover in detail in our closure training module.)
error[E0507]: cannot move out of `counter`, a captured variable in an `FnMut` closure
--> src\main.rs:21:58
|
18 | let counter: Counter = Arc::new(Mutex::new(0));
| ------- captured outer variable
...
21 | Ok::<_, Infallible>(service_fn(|req| hello_world(counter, req)))
| ^^^^^^^ move occurs because `counter` has type `Arc<std::sync::Mutex<usize>>`, which does not implement the `Copy` trait
error[E0507]: cannot move out of `counter`, a captured variable in an `FnMut` closure
--> src\main.rs:20:50
|
18 | let counter: Counter = Arc::new(Mutex::new(0));
| ------- captured outer variable
19 |
20 | let make_svc = make_service_fn(|_conn| async {
| __________________________________________________^
21 | | Ok::<_, Infallible>(service_fn(|req| hello_world(counter, req)))
| | -------------------------------
| | |
| | move occurs because `counter` has type `Arc<std::sync::Mutex<usize>>`, which does not implement the `Copy` trait
| | move occurs due to use in generator
22 | | });
| |_____^ move out of `counter` occurs here
Clone
That error isn't terribly surprising. We put our Mutex
inside an Arc
for a reason: we'll need to make multiple clones of it and pass those around to each new request handler. But we haven't called clone
once yet! Again, let's do the most naive thing possible, and change:
Ok::<_, Infallible>(service_fn(|req| hello_world(counter, req)))
into
Ok::<_, Infallible>(service_fn(|req| hello_world(counter.clone(), req)))
This is where the error messages begin to get more interesting:
error[E0597]: `counter` does not live long enough
--> src\main.rs:21:58
|
20 | let make_svc = make_service_fn(|_conn| async {
| ____________________________________-------_-
| | |
| | value captured here
21 | | Ok::<_, Infallible>(service_fn(|req| hello_world(counter.clone(), req)))
| | ^^^^^^^ borrowed value does not live long enough
22 | | });
| |_____- returning this value requires that `counter` is borrowed for `'static`
...
29 | }
| - `counter` dropped here while still borrowed
Both async
blocks and closures will, by default, capture variables from their environment by reference, instead of taking ownership. Our closure needs to have a 'static
lifetime, and therefore can't hold onto a reference to data in our main
function.
move
all the things!
The standard solution to this is to simply sprinkle move
s on each async
block and closure. This will force each closure to own the Arc
itself, not a reference to it. Doing so looks simple:
let make_svc = make_service_fn(move |_conn| async move {
Ok::<_, Infallible>(service_fn(move |req| hello_world(counter.clone(), req)))
});
And this does in fact fix the error above. But it gives us a new error instead:
error[E0507]: cannot move out of `counter`, a captured variable in an `FnMut` closure
--> src\main.rs:20:60
|
18 | let counter: Counter = Arc::new(Mutex::new(0));
| ------- captured outer variable
19 |
20 | let make_svc = make_service_fn(move |_conn| async move {
| ____________________________________________________________^
21 | | Ok::<_, Infallible>(service_fn(move |req| hello_world(counter.clone(), req)))
| | --------------------------------------------
| | |
| | move occurs because `counter` has type `Arc<std::sync::Mutex<usize>>`, which does not implement the `Copy` trait
| | move occurs due to use in generator
22 | | });
| |_____^ move out of `counter` occurs here
Double the closure, double the clone!
Well, even this error makes a lot of sense. Let's understand better what our code is doing:
- Creates a closure to pass to
make_service_fn
, which will be called for each new incoming connection
- Within that closure, creates a new closure to pass to
service_fn
, which will be called for each new incoming request on an existing connection
This is where the trickiness of working directly with Hyper comes into play. Each of those layers of closure need to own their own clone of the Arc
. And in our code above, we're trying to move the Arc
from the outer closure's captured variable into the inner closure's captured variable. If you squint hard enough, that's what the error message above is saying. Our outer closure is an FnMut
, which must be callable multiple times. Therefore, we cannot move out of its captured variable.
It seems like this should be an easy fix: just clone
again!
let make_svc = make_service_fn(move |_conn| async move {
let counter_clone = counter.clone();
Ok::<_, Infallible>(service_fn(move |req| hello_world(counter_clone.clone(), req)))
});
And this is the point at which we hit a real head scratcher: we get almost exactly the same error message:
error[E0507]: cannot move out of `counter`, a captured variable in an `FnMut` closure
--> src\main.rs:20:60
|
18 | let counter: Counter = Arc::new(Mutex::new(0));
| ------- captured outer variable
19 |
20 | let make_svc = make_service_fn(move |_conn| async move {
| ____________________________________________________________^
21 | | let counter_clone = counter.clone();
| | -------
| | |
| | move occurs because `counter` has type `Arc<std::sync::Mutex<usize>>`, which does not implement the `Copy` trait
| | move occurs due to use in generator
22 | | Ok::<_, Infallible>(service_fn(move |req| hello_world(counter_clone.clone(), req)))
23 | | });
| |_____^ move out of `counter` occurs here
The paradigm shift
What we need to do is to rewrite our code ever so slightly so reveal what the problem is. Let's add a bunch of unnecessary braces. We'll convert the code above:
let make_svc = make_service_fn(move |_conn| async move {
let counter_clone = counter.clone();
Ok::<_, Infallible>(service_fn(move |req| hello_world(counter_clone.clone(), req)))
});
into this semantically identical code:
let make_svc = make_service_fn(move |_conn| { // outer closure
async move { // async block
let counter_clone = counter.clone();
Ok::<_, Infallible>(service_fn(move |req| { // inner closure
hello_world(counter_clone.clone(), req)
}))
}
});
The error message is basically identical, just slightly different source locations. But now I can walk through the ownership of counter
more correctly. I've added comments to highlight three different entities in the code above that can take ownership of values via some kind of environment:
- The outer closure, which handles each connection
- An
async
block, which forms the body of the outer closure
- The inner closure, which handles each request
In the original structuring of the code, we put move |_conn| async move
next to each other on one line, which—at least for me—obfuscated the fact that the closure and async
block were two completely separate entities. With that change in place, let's track the ownership of counter
:
- We create the
Arc
in the main
function; it's owned by the counter
variable.
- We move the
Arc
from the main
function's counter
variable into the outer closure's captured variables.
- We move the
counter
variable out of the outer closure and into the async
block's captured variables.
- Within the body of the
async
block, we create a clone of counter
, called counter_clone
. This does not move out of the async
block, since the clone
method only requires a reference to the Arc
.
- We move the
Arc
out of the counter_clone
variable and into the inner closure.
- Within the body of the inner closure, we clone the
Arc
(which, as explained in (4), doesn't move) and pass it into the hello_world
function.
Based on this breakdown, can you see where the problem is? It's at step (3). We don't want to move out of the outer closure's captured variables. We try to avoid that move by cloning counter
. But we clone too late! By using counter
from inside an async move
block, we're forcing the compiler to move. Hurray, we've identified the problem!
Non-solution: non-move async
It seems like we were simply over-ambitious with our "sprinkling move
" attempt above. The problem is that the async
block is taking ownership of counter
. Let's try simply removing the move
keyword there:
let make_svc = make_service_fn(move |_conn| {
async {
let counter_clone = counter.clone();
Ok::<_, Infallible>(service_fn(move |req| {
hello_world(counter_clone.clone(), req)
}))
}
});
Unfortunately, this isn't a solution:
error: captured variable cannot escape `FnMut` closure body
--> src\main.rs:21:9
|
18 | let counter: Counter = Arc::new(Mutex::new(0));
| ------- variable defined here
19 |
20 | let make_svc = make_service_fn(move |_conn| {
| - inferred to be a `FnMut` closure
21 | / async {
22 | | let counter_clone = counter.clone();
| | ------- variable captured here
23 | | Ok::<_, Infallible>(service_fn(move |req| {
24 | | hello_world(counter_clone.clone(), req)
25 | | }))
26 | | }
| |_________^ returns an `async` block that contains a reference to a captured variable, which then escapes the closure body
|
= note: `FnMut` closures only have access to their captured variables while they are executing...
= note: ...therefore, they cannot allow references to captured variables to escape
The problem here is that the outer closure will return the Future
generated by the async
block. And if the async
block doesn't move
the counter
, it will be holding a reference to the outer closure's captured variables. And that's not allowed.
Real solution: clone early, clone often
OK, undo the async move
to async
transformation, it's a dead end. It turns out that all we've got to do is clone the counter
before we start the async move
block, like so:
let make_svc = make_service_fn(move |_conn| {
let counter_clone = counter.clone(); // this moved one line earlier
async move {
Ok::<_, Infallible>(service_fn(move |req| {
hello_world(counter_clone.clone(), req)
}))
}
});
Now, we create a temporary counter_clone
within the outer closure. This works by reference, and therefore doesn't move anything. We then move the new, temporary counter_clone
into the async move
block via a capture, and from there move it into the inner closure. With this, all of our closure captured variables remain unmoved, and therefore the requirements of FnMut
are satisfied.
And with that, we can finally enjoy the glory days of Geocities visitor counters!
Async closures
The formatting recommended by rustfmt
hides away the fact that there are two different environments at play between the outer closure and the async block
, by moving the two onto a single line with move |_conn| async move
. That makes it feel like the two entities are somehow one and the same. But as we've demonstrated, they aren't.
Theoretically this could be solved by having an async closure. I tested with #![feature(async_closure)]
on nightly-2021-03-02
, but couldn't figure out a way to use an async closure to solve this problem differently than I solved it above. But that may be my own lack of familiarity with async_closure
.
For now, the main takeaway is that closures and async
blocks are two different entities, each with their own environment.
If you liked this post you may also be interested in:
Subscribe to our blog via email
Email subscriptions come from our Atom feed and are handled by Blogtrottr. You will only receive notifications of blog posts, and can unsubscribe any time.
Do you like this blog post and need help with Next Generation Software Engineering, Platform Engineering or Blockchain & Smart Contracts? Contact us.