Pass tokio::net::TcpStream to function as parameter

This is a follow-up question to this question. There you can also view the current code in more detail.

Basically the user can define routes like this:

let routes: Arc<std::vec::Vec<Route>> = Arc::new(vec![
        route!("GET", "/", Routes::index),
        route!("GET", "/welcome/{name}", Routes::welcome),
        route!("GET", "/welcome/{name}/{age}", Routes::welcome_age),
        route!("POST", "/print", Routes::print_name),
        route!("GET", "/html-page", Routes::html_page),
    ]);

The type of a route is this:

// Define type for tuple containing routes. (HTTP-method, path, fn pointer to route function).
type Route = (&'static str, &'static str, fn(&Routes, Http) -> BoxFuture<Result<HttpResponse, String>>);

The type contains a function pointer to functions in a struct called Routes. These functions are the methods executed for a given specific path. e.g for http://127.0.0.1:8080/ the function executed is Routes::index:

struct Routes;
impl Routes {
    //...
    async fn index(&self, req: Http) -> Result<HttpResponse, String> {
        Ok(HttpResponse {
            status: 200,
            headers: vec![
                ("Content-Type".into(), "text/plain".into()),
            ],
            body: "This is the homepage.".into(),
        })
    }
    //...
}

There are two other important functions: process and goto. process basically takes the stream from a Tokio TcpListener and refers it to goto. goto checks the path of the request and then executes the matching function in Routes.

This works fine for basic HTTP requests. But now I want the client to be able to initiate a Websocket connection by sending a HTTP WebSocket upgrade request to ws://127.0.0.1:8080/ws.

I have made Routes::websocket: In that function I'd take the TcpStream and use tokio_tungstenite::accept_async() to initiate a Websocket connection. The problem is that Routes::websocket would need to accept the TcpStream as parameter async fn websocket(&self, req: Http, stream: &mut TcpStream) -> Result<HttpResponse, String>. process would pass the TcpStream to goto. And goto would do return ro(.., .., &mut TcpStream).

The problem is that for the Routes-functions to accept TcpStream as parameter I'd have to update my type Route to this:


// Define type for tuple containing routes. (HTTP-method, path, fn pointer to route function).
type Route = (&'static str, &'static str, fn(&Routes, Http, &mut TcpStream) -> BoxFuture<Result<HttpResponse, String>>);

This yields a lot of lifetime-errors. I also tried using Arc<tokio::sync::Mutex<>> instead of purely TcpStream, but also that didn't work.

How to solve this?

Does this make a difference?

type Route = (
    &'static str,
    &'static str,
    for<'a> fn(&'a Routes, Http, &'a mut TcpStream) 
        -> BoxFuture<'a, Result<HttpResponse, String>>
);

Incidentally I recommend using #![deny(elided_lifetimes_in_paths)] so that e.g. the fact that BoxFuture captures the input lifetime is apparent.

// Original definition without lifetime elision
type Route = (
    &'static str,
    &'static str, 
    fn(&Routes, Http) -> BoxFuture<'_, Result<HttpResponse, String>>,
    //                             ^^^
);
1 Like

Yes. That seems to work. I'll double test. Could you explain for<'a>? I tried the same type definition but with using Route<'a> instead and that failed.

Sure. With fn(...) types (and Fn(...) bounds), elided (non-named) lifetimes correspond to "any input lifetime". Each elided lifetime is a separate lifetime. If you want to name the lifetimes instead, you put them in a for<...> "binder".

So this type:

fn(&Routes, Http, &mut TcpStream)

Is short for this type:

for<'a, 'b> fn(&'a Routes, Http, &'b mut TcpStream)

Analogous to how these are the same.

fn foo(routes: &Routes, http: Http, tcp: &mut TcpStream) {}
fn foo<'a, 'b>(routes: &'a Routes, http: Http, tcp: &'b mut TcpStream) {}

If we look at your full signature with the binder, it was something like this:

for<'a, 'b> fn(&'a Routes, Http, &'b mut TcpStream) 
    -> BoxFuture<'_, Result<HttpResponse, String>>);

And since there's two input lifetimes, output lifetime elision can't apply. Moreover, you're probably capturing both 'a and 'b here, so these probably don't work for you:

for<'a, 'b> fn(&'a Routes, Http, &'b mut TcpStream) 
    -> BoxFuture<'a, Result<HttpResponse, String>>);
//               ^^

for<'a, 'b> fn(&'a Routes, Http, &'b mut TcpStream) 
    -> BoxFuture<'b, Result<HttpResponse, String>>);
//               ^^

Instead my suggestions just forces both input lifetimes (and the output lifetime) to be the same.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.