Experimental async / await support for Tokio

August 27, 2018

Happy Monday!

In case you haven't heard, async / await is a big new feature that is being worked on for Rust. It aims to make asynchronous programming easy (well, at least a little bit easier than it is today). The work has been on going for a while and is already usable today on the Rust nightly channel.

I'm happy to announce that Tokio now has experimental async / await support! Let's dig in a bit.

Getting started

First, Tokio async/await support is provided by a new crate, creatively named tokio-async-await. This crate is a shim on top of tokio. It contains all of the same types and functions as tokio (as re-exports) as well as additional helpers to work with async / await.

To use tokio-async-await, you need to depend on it from a crate that is configured to use Rust's 2018 edition. It also only works with recent Rust nightly releases.

In your application's Cargo.toml, add the following:

# At the very top of the file
cargo-features = ["edition"]

# In the `[packages]` section
edition = "2018"

# In the `[dependencies]` section
tokio = {version = "0.1", features = ["async-await-preview"]}

Then, in your application, do the following:

// The nightly features that are commonly needed with async / await
#![feature(await_macro, async_await, futures_api)]

// This pulls in the `tokio-async-await` crate. While Rust 2018
// doesn't require `extern crate`, we need to pull in the macros.
#[macro_use]
extern crate tokio;

fn main() {
    // And we are async...
    tokio::run_async(async {
        println!("Hello");
    });
}

and run it (with nightly):

cargo +nightly run

and you are using Tokio + async / await!

Note that, to spawn async blocks, the tokio::run_async function should be used (instead of tokio::run).

Going deeper

Now, let's build something simple: an echo server (yay).

// Somewhere towards the top

#[macro_use]
extern crate tokio;

use tokio::net::{TcpListener, TcpStream};
use tokio::prelude::*;

// more to come...

// The main function
fn main() {
  let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
  let listener = TcpListener::bind(&addr).unwrap();

    tokio::run_async(async {
        let mut incoming = listener.incoming();

        while let Some(stream) = await!(incoming.next()) {
            let stream = stream.unwrap();
            handle(stream);
        }
    });
}

In this example, incoming is a stream of accepted TcpStream values. We are using async / await to iterate the stream. Currently, there is only syntax for awaiting on a single value (future), so we use the next combinator to get a future of the next value in the stream. This lets us iterate the stream with while syntax.

Once we get the stream, it is passed to the handle function to process. Lets see how that is implemented.

fn handle(mut stream: TcpStream) {
    tokio::spawn_async(async move {
        let mut buf = [0; 1024];

        loop {
            match await!(stream.read_async(&mut buf)).unwrap() {
                0 => break, // Socket closed
                n => {
                    // Send the data back
                    await!(stream.write_all_async(&buf[0..n])).unwrap();
                }
            }
        }
    });
}

Just like run_async, there is a spawn_async function to spawn async blocks as tasks.

Then, to perform the echo logic, we read from the socket into a buffer and write the data back to the same socket. Because we are using async / await, we can use an array that looks stack allocated (it actually ends up in the heap).

Note that TcpStream has read_async and write_all_async functions. These functions perform the same logic as the synchronous equivalents that exist on the Read and Write traits in std. The difference, they return futures that can be awaited on.

The *_async functions are defined in the tokio-async-await crate by using extension traits. These traits got imported with the use tokio::prelude::*; line.

This is just a start, check the examples directory in the repository for more. There even is one using hyper.

Some notes

First, the tokio-async-await crate only provides compatibility for async / await syntax. It does not provide support for the futures 0.3 crate. It is expected that users continue using futures 0.1 to remain compatible with Tokio.

To make this work, the tokio-async-await crate defines its own await! macro. This macro is a shim on top of the one provided by std that enables waiting for futures 0.1 futures. This is how the compatibility layer is able to stay lightweight and boilerplate free.

This is just a start. The async / await support will continue to evolve and improve over time, but this is enough to get everyone going!

And with that, have a great week!

—Carl Lerche