FP Complete


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
  --> srcmain.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
  --> srcmain.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
  --> srcmain.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 moves 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
  --> srcmain.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:

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
  --> srcmain.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:

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:

  1. We create the Arc in the main function; it’s owned by the counter variable.
  2. We move the Arc from the main function’s counter variable into the outer closure’s captured variables.
  3. We move the counter variable out of the outer closure and into the async block’s captured variables.
  4. 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.
  5. We move the Arc out of the counter_clone variable and into the inner closure.
  6. 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
  --> srcmain.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.

Tagged