Add async functions to hashmap

I'm working with hyper and I want to add my async functions to a hashmap.

I got some errors like this:

error[E0308]: mismatched types
   --> src\server.rs:23:58
    |
23  |     let result = funcs.get(&selector).copied().unwrap_or(&not_found);
    |                                                --------- ^^^^^^^^^^ expected `&fn(..., ...) -> ... {serve_file}`, found `&fn(..., ...) -> ... {not_found}`
    |                                                |
    |                                                arguments to this method are incorrect
    |
    = note: expected reference `&for<'a> fn(SocketAddr, &'a Uri) -> impl futures_util::Future<Output = Response<BoxBody<bytes::Bytes, std::io::Error>>> {serve_file}`
               found reference `&for<'a> fn(SocketAddr, &'a Uri) -> impl futures_util::Future<Output = Response<BoxBody<bytes::Bytes, std::io::Error>>> {not_found}`
help: the return type of this call is `&for<'a> fn(SocketAddr, &'a Uri) -> impl futures_util::Future<Output = Response<BoxBody<bytes::Bytes, std::io::Error>>> {not_found}` due to the type of the argument passed
   --> src\server.rs:23:18
    |
23  |     let result = funcs.get(&selector).copied().unwrap_or(&not_found);
    |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^----------^
    |                                                          |
    |                                                          this argument influences the return type of `unwrap_or`
note: method defined here
   --> C:\Users\AZ\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib/rustlib/src/rust\library\core\src\option.rs:951:12
    |
951 |     pub fn unwrap_or(self, default: T) -> T {
    |            ^^^^^^^^^

So far, I've tried many solutions found on the Internet but none of them worked. The compiler just gave me more errors :frowning:

My code now:

use std::collections::HashMap;
use std::io::Error;
use std::net::SocketAddr;
use bytes::Bytes;
use futures_util::TryStreamExt;
use http_body_util::combinators::BoxBody;
use http_body_util::{BodyExt, Full, StreamBody};
use hyper::{Request, Response, StatusCode, Uri};
use hyper::body::Frame;
use tokio::fs::File;
use tokio_util::io::ReaderStream;

// An async function that route requests to functions
pub async fn router(
    req: Request<hyper::body::Incoming>,
    remote_addr: SocketAddr
) -> hyper::Result<Response<BoxBody<Bytes, std::io::Error>>> {
    trace!("[{}] {:?} {} {}", remote_addr, req.version(), req.method(), req.uri());
    let selector = req.uri().path().split("/").collect::<Vec<_>>()[1];
    let mut funcs = HashMap::new();
    funcs.insert("docs", &serve_file);

    let result = funcs.get(&selector).copied().unwrap_or(&not_found);
    Ok(result(remote_addr, req.uri()).await)
}

async fn serve_file(addr: SocketAddr, uri: &Uri) -> Response<BoxBody<Bytes, Error>> {
    let filename = &uri.query().get_or_insert("path")[5..];
    trace!("[{}] Request for file: {}", addr, filename);
    // Open file for reading
    let file = File::open(filename).await;
    if file.is_err() {
        error!("ERROR: Unable to open file.");
        return not_found(addr, uri).await;
    }

    let file: File = file.unwrap();

    // Wrap to a tokio_util::io::ReaderStream
    let reader_stream = ReaderStream::new(file);

    // Convert to http_body_util::BoxBody
    let stream_body = StreamBody::new(reader_stream.map_ok(Frame::data));
    let boxed_body = BoxBody::new(stream_body);

    // Send response
    Response::builder()
        .status(StatusCode::OK)
        .body(boxed_body)
        .unwrap()
}

/// HTTP status code 404
async fn not_found(addr: SocketAddr, uri: &Uri) -> Response<BoxBody<Bytes, std::io::Error>> {
    Response::builder()
        .status(StatusCode::NOT_FOUND)
        .body(Full::new("Not found".into()).map_err(|e| match e {}).boxed())
        .unwrap()
}

You'd need to type erase the output types at least.

async fn serve_file(addr: SocketAddr, uri: &Uri) 
    -> Response<BoxBody<Bytes, Error>>
{ ... }

// Desugared

fn serve_file(addr: SocketAddr, uri: &Uri)
    -> impl Future<Output = Response<BoxBody<Bytes, Error>> + Captures<'_>
{
    async move { let(addr, uri) = (addr, uri); ... }
}

// Erased

fn serve_file(addr, SocketAddr, uri: &Uri)
    -> Pin<Box<dyn Future<Output = BoxBody<Bytes, Error>> + '_ + Send>>
{
    Box::pin(async move {
        ...
    })
}

The exact auto traits (Send, Sync, ...) can vary and sometimes you can get rid of the lifetime in the Pin<Box<_>>. If you can get rid of it, you usually want to.

You probably also want to be using store fn(...) and not &fn(...). Or maybe Box<FnMut(...)>.

I'll see if I can hack it into a playground.

This is enough for the code sample it seems.

type CallbackFuture<'a> = dyn Future<Output = Response<BoxBody<Bytes, Error>>> + Send + 'a;
type CallbackFn = fn(SocketAddr, &Uri) -> Pin<Box<CallbackFuture<'_>>>;
    let bp_serve_file: CallbackFn = |addr, uri| Box::pin(serve_file(addr, uri));
    let bp_not_found: CallbackFn = |addr, uri| Box::pin(not_found(addr, uri));

    let mut funcs = HashMap::new();
    funcs.insert("docs", bp_serve_file);

    let result = funcs.get(&selector).copied().unwrap_or(bp_not_found);

It may not have been clear from my above comment, but

  • async fn return opaque types (impl Future ...)
  • Every opaque type is unique
  • fn pointers with different output types are themselves different types
  • Therefore two async fn can never coerce to the same fn pointer type

The workaround is type erase the future (Pin<Box<dyn ...>>) so that you can have function pointers with the same type.

3 Likes

It worked!!!! Thanks bro! :wink: