I've been working at this for about two weeks now and I feel like I've hit that point where I'm going in circles with no progress. So, here goes...
I decided I wanted to work on a project in Rust to learn it; that sort of thing works better for me, and I've settled on a client/server that sends and receives HTTP requests. Hyper seemed like a decent choice for that as it covers both sending requests and handling requests.
I have gone through the basics with Hyper and gotten the basic server running. I've started now trying to expand this into a dynamic dispatcher/router of sorts, the idea being that at the start of the program we load up some routes, and the dynamic dispatcher will perform some (currently very arbitrary) action. All fine.
I decided I wanted to break the dispatcher into its own module to play a bit with Rust's module system, and built a struct (Dispatcher) that will handle storing a route-to-action map. I've also added a dispatcher function that would do the work of checking the Request against the route-to-action map and would, eventually, do the action (or hand it off to be done elsewhere - whatever.)
Here is where I start running into issues. If I try the following:
use crate::dispatcher::dispatcher::{Dispatcher, dispatch_request}
// my Dispatcher struct and dispatcher function
let dispatcher = Dispatcher::new();
let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
let server = Server::bind(&addr).serve
(
make_service_fn(|_conn| async move
{
Ok::<_, Infallible>
(
service_fn
(
|req| async move {dispatch_request(req, dispatcher)}
)
)
})
);
Apologies for the (\{ characters on their own line, I just find it easier to read closures if I blow them up like that.
Here's Dispatcher, if it matters:
#[derive(Clone)]
pub struct Dispatcher
{
route_map: HashMap<String, String>,
}
impl Dispatcher
{
// pub fn new(prologue_routes: HashMap<String, String>) -> Self
pub fn new() -> Self
{
Dispatcher { route_map: HashMap::new() }
}
}
If I build it (or just using the rust-analyzer VS code plugin) I get cannot move out of 'dispatcher', a captured variable in an 'FnMut' closure
, with the further notes move occurs because dispatcher has type 'Dispatcher', which does not implement the 'Copy' trait
and move occurs due to use in generator
. For reference, the actual compiler errors are:
error[E0507]: cannot move out of `dispatcher`, a captured variable in an `FnMut` closure
--> src/main.rs:64:51
38 | let dispatcher = Dispatcher::new(prologue_routes);
| ---------- captured outer variable
...
64 | |req| async move {dispatch_request(req, dispatcher)}
| ^^^^^^^^^^^^^^^^^^^^^^^----------^^
| | |
| | move occurs because `dispatcher` has type `Dispatcher`, which does not implement the `Copy` trait
| | move occurs due to use in generator
move out of `dispatcher` occurs here
I can kinda see that there would be lifetime issues here - if I understand the operation of Hyper correctly, the make_service_fn function takes a closure that is used to generate additional closures dynamically, one for each inbound HTTP request. So any data referenced in a variable that is in the innermost scope would go out of scope at the end of that closure, correct? If so, I can see that just moving a one-off reference to a variable into the innermost closure would not go well.
However - I'm still not sure how to solve the problem, or if what I'm trying can even be done given the mechanisms involved - nor am I positive that the error message matches the lifetime issues that I am guessing at. I've been hunting up and down trying to come to a better understanding of the mechanisms at play and clearly I've been missing something.
The closest I've found to someone with this exact same problem is here: https://users.rust-lang.org/t/solved-channel-in-a-loop-in-a-thread-borrowed-value-does-not-live-long-enough/26733/4. Yandros' answer partway down suggests wrapping the Dispatcher in an Arc and then cloning it in. On the surface seems to make sense...but doing so brings the exact same cannot move out of 'dispatcher'
errors:
let arc_dispatcher = Arc::new(Dispatcher::new(prologue_routes));
let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
let server = Server::bind(&addr).serve
(
make_service_fn(|_conn| async move
{
let arc_dispatcher2 = arc_dispatcher.clone();
Ok::<_, Infallible>
(
service_fn
(
|req| async move
{
let arc_dispatcher3 = arc_dispatcher2.clone();
dispatch_request(req, arc_dispatcher3)
}
)
)
})
);
Trying with only one or the other of the arc_dispatcher.clone() results in no difference. Any attempt to move a var declared outside into any of the interior closures results in the cannot move
error. And as I start to think about it - why would an Arc behave differently? The arc_dispatcher variable is still a variable subject to ownership and borrowing rules - so by rights when I reference arc_dispatcher in the make_service_fn closure it should get moved before the clone function is called. Or am I misunderstanding something here?
Sidenote: Removing the move
keyword simply changes the error to a lifetime one - that the arc_dispatcher variable (or plain dispatcher in example 1) does not live long enough. Which maybe suggests my guess about lifetimes wasn't entirely wrong.
So at this point - I'm stuck. I guess I'm looking for help in fully understanding what, exactly, is happening here, and if my idea of passing in a struct to a dispatch function called by the Hyper service closure is even viable, or if I need to come up with some other solution entirely. Any help would be appreciated!