An example of hand-written coroutines – Coroutines and async/await

The example we’ll use going forward is a simplified version of Rust’s asynchronous model. We’ll create and implement the following:

  • Our own simplified Future trait
  • A simple HTTP client that can only make GET requests
  • A task we can pause and resume implemented as a state machine
  • Our own simplified async/await syntax called coroutine/wait
  • A homemade preprocessor to transform our coroutine/wait functions into state machines the same way async/await is transformed

So, to actually demystify coroutines, futures, and async/await, we will have to make some compromises. If we didn’t, we’d end up re-implementing everything that is async/await and futures in Rust today, which is too much for just understanding the underlying techniques and concepts.

Therefore, our example will do the following:

  • Avoid error handling. If anything fails, we panic.
  • Be specific and not generic. Creating generic solutions introduces a lot of complexity and makes the underlying concepts harder to reason about since we consequently have to create extra abstraction levels. Our solution will have some generic aspects where needed, though.
  • Be limited in what it can do. You are of course free to expand, change, and play with all the examples (I encourage you to do so), but in the example, we only cover what we need and not anything more.
  • Avoid macros.

So, with that out of the way, let’s get started on our example.

The first thing you need to do is to create a new folder. This first example can be found in ch07/a-coroutine in the repository, so I suggest you name the folder a-coroutine as well.

Then, initialize a new crate by entering the folder and write cargo init.

Now that we have a new project up and running, we can create the modules and folders we need:

First, in main.rs, declare two modules as follows:

ch07/a-coroutine/src/main.rs
mod http;
mod future;

Next, create two new files in the src folder:

  • future.rs, which will hold our future-related code
  • http.rs, which will be the code related to our HTTP client

One last thing we need to do is to add a dependency on mio. We’ll be using TcpStream from mio, as we’ll build on this example in the following chapters and use mio as our non-blocking I/O library since we’re already familiar with it:

ch07/a-coroutine/Cargo.toml
[dependencies]
mio = { version = “0.8”, features = [“net”, “os-poll”] }

Let’s start in future.rs and implement our future-related code first.

Futures module

In futures.rs, the first thing we’ll do is define a Future trait. It looks as follows:

ch07/a-coroutine/src/future.rs
pub trait Future {
    type Output;
    fn poll(&mut self) -> PollState<Self::Output>;
}

If we contrast this with the Future trait in Rust’s standard library, you’ll see it’s very similar, except that we don’t take cx: &mut Context<‘_> as an argument and we return an enum with a slightly different name just to differentiate it so we don’t mix them up:
pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<‘_>) -> Poll<Self::Output>;
}

The next thing we do is to define a PollState<T> enum:

ch07/a-coroutine/src/future.rs
pub enum PollState<T> {
    Ready(T),
    NotReady,
}

Again, if we compare this to the Poll enum in Rust’s standard library, we see that they’re practically the same:
pub enum Poll<T> {
    Ready(T),
    Pending,
}

For now, this is all we need to get the first iteration of our example up and running. Let’s move on to the next file: http.rs.

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Post