Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port server to async #20

Open
willcrichton opened this issue Jul 30, 2024 · 0 comments
Open

Port server to async #20

willcrichton opened this issue Jul 30, 2024 · 0 comments

Comments

@willcrichton
Copy link
Contributor

willcrichton commented Jul 30, 2024

Task

In {{ 01-async-await pr }}, the miniserve library was refactored to use Rust's async feature. Unfortunately this broke the build, because the new API is not backwards-compatible. Once you merge {{ 01-async-await pr }}, your task is to port the server crate so it has the same functionality as before, but such that it works with the new miniserve API.

Background

Remember — try solving the problem before reading the background. What compilers errors do you get? Do they give any helpful advice? How far can you get before you get stuck? Don't forget to pull the merged PR before you begin.

Futures

A future is a computation that eventually returns a value. Rust represents futures as types that implement the std::future::Future trait. The Future trait has an associated type Output which indicates the kind of value returned by the future. For example, if a function returns impl Future<Output = i32>, then that means "this function returns a future which eventually outputs an i32." If you need a refresher on impl Trait syntax, check out the Rust book chapter on traits.

async keyword

The easiest way to make a future is to use the async keyword. The async keyword can annotate a function:

async fn returns_a_future() -> i32 { 
  0
}

Or it can annotate block:

fn returns_a_future() -> impl Future<Output = i32> { 
  async { 0 }
}

These two pieces of code do the same thing.

Future bounds

The Future trait commonly occurs as a trait bound in library functions that take futures as input, or return futures as outputs. For example:

fn i_want_a_future<F: Future>(_f: F) {}
fn i_want_an_async_fn<A: Future, B: Fn() -> A>(_f: B) {}

fn main() {
  let fut = async { println!("Hello world"); };
  i_want_a_future(fut);
  
  
  async fn fut_fn() { println!("Hello world"); }
  i_want_an_async_fn(fut_fn);
}

If you get a compiler error that says Foo is not a future, that means that Foo needs to implement the Future trait.

await keyword

The easiest way to get the value out of a future is to use the await keyword. For example, you could await the output of returns_a_future like this:

async fn print_zero() {
  let zero = returns_a_future().await;
  println!("{zero}");
}

The await keyword can only be used in async functions or blocks. It basically means: "wait until this future has completed, and then get its value."

Async in main

Rust does not allow the main function to be async. The reason has to do with how Rust futures are implemented, which we will discuss later. For now, the key thing to know is that async functions need to be executed in a runtime. An async runtime is code that runs futures and checks them for completion. Unlike most languages with async (e.g., NodeJS, C#, Go, etc.), Rust does not have a default async runtime. You have to provide one yourself.

Providing an async runtime

For this tutorial, we will be using Tokio, a popular and featureful async runtime (but the core concepts will translate to other runtimes). miniserve is already using Tokio. To use it within server, add it as a dependency using the full feature, e.g.,

cargo add tokio --features full -p server

The simplest way to setup the Tokio runtime is to annotate the main function like this:

#[tokio::main]
async fn main() {
 // ...
}
How does #[tokio::main] work?

The macro isn't too complicated — try seeing what it expands to by running cargo expand. Basically, it moves the body of the main function into an async block, and then defines a new (non-async) main which passes that async block to a newly-created Tokio runtime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant