Aaron Turon

Tech | Personal | Academic | Music | About


Project maintained by aturon Hosted on GitHub Pages — Theme by mattgraham

Borrowing in async code

The networking working group is pushing hard on async/await notation for Rust, and @withoutboats in particular wrote a fantastic blog series working through the design space (final post here).

I wanted to talk a little bit about some of the implications of async/await, which may not have been entirely clear. In particular, async/await is not just about avoiding combinators; it completely changes the game for borrowing.

The core issue is that, while the Future trait does not itself impose a 'static bound, in practice futures have to be 'static because they are tossed onto executors like thread pools and hence not tied to any particular stack frame. Today, what that means is that futures-based APIs have to be careful not to hold on to borrows, and instead take ownership of whatever they need. That in turn leads to all kinds of unidiomatic patterns, including threading through ownership and widespread use of Rc and RefCell.

Idioms in the standard library

To see what I mean, it’s helpful to work through an example. Let’s take the read method from the standard library:

fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error>

This method takes a mutable reference to both an I/O object and a buffer to read into, then does the read synchronously. That lets you write idiomatic code like the following:

let mut buf = [0; 1024];
let mut cursor = 0;

while cursor < 1024 {
    cursor += socket.read(&mut buf[cursor..])?;
}

This is perfectly ordinary code, in which we repeatedly take mutable borrows within a loop.

Idioms in futures today

If we wanted to translate the above to an asynchronous setting using futures, we’d need to use a futures-based analog to the read method. That exists today with roughly the following signature:

fn read<T: AsMut<[u8]>>(self, buf: T) ->
    impl Future<Item = (Self, T, usize), Error = (Self, T, io::Error)>

That signature looks rather different! The reason is that we want the returned future to be 'static, so we have to pass in (and return) ownership of both the I/O object and the buffer.

Not only is the signature more complicated: it’s also unwieldy to use, even if we employ async/await notation:

struct Buf {
    // box this up so we're not moving it around
    data: Box<[u8, 1024]>,

    cursor: usize,
}

impl AsMut<[u8]> for Buf {
    fn as_mut(&mut self) -> &mut [u8] {
        &mut self.data[self.cursor..]
    }
}

let mut buf = Buf {
    data: Box::new([0; 1024]),
    cursor: 0,
};

while buf.cursor < 1024 {
    match await!(socket.read(buf)) {
        Ok((new_socket, new_buf, n)) => {
            socket = new_socket;
            buf = new_buf;
            buf.cursor += n;
        }
        Err((new_socket, new_buf, e)) => {
            socket = new_socket;
            buf = new_buf;
            Err(e)?
        }
    }
}

While we could take steps to make this particular example easier, the fact is that requiring you to always move values in and out of async code prevents you from following the usual Rust idioms for borrowing.

Borrowing in async code

You might wonder: why can’t we just use the following signature instead?

fn read<'a>(&'a mut self, buf: &'a mut [u8]) -> impl Future<Item = usize, Error = io::Error> + 'a

And indeed, you can write and implement such a function; you just can’t effectively use it. The problem is that the future you get back contains borrowed values, which today will prevent it from being used in most futures-based code, due to there being a 'static requirement to ultimately execute futures.

This is where the async/await plan comes in: you can await a future with borrowed data, while still being 'static overall!. This is what it means to support “borrowing across yield points”, as explained in @withoutboats’s post.

In particular, using this borrowing version of read, we can write:

async {
    let mut socket = /* .. */;
    let mut buf = [0; 1024];
    let mut cursor = 0;

    while cursor < 1024 {
        cursor += await!(socket.read(&mut buf[cursor..]))?;
    };

    buf
}

and the type of the async block will be:

impl Future<Item = [0; 1024], Error = io::Error> + 'static

Despite the fact that we borrow internally within the async block, the block as a whole produces a 'static future which we can spawn onto a thread pool or other executor.

In other words, the async/await proposal allows you to write fully idiomatic Rust code that runs asynchronously. That applies even to signatures; the borrowing version of async read will ultimately look as follows:

async fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error>

This signature is exactly the same as for the synchronous version, just with an async on the front.

The implications

The bottom line is that async/await isn’t just about not having to use combinators like and_then. It also fundamentally changes API design in the async world, allowing us to use borrowing in the idiomatic style. Those who have written much futures-based code in Rust will be able to tell you just how big a deal this is.

Right now the networking WG is focused on landing async/await itself (which will probably happen soon), and providing a migration path for the futures crate. Once those basics are in place, though, we’ll be able to revisit APIs throughout the async stack and make them more idiomatic. With luck, we’ll have a very strong story in place for Rust 2018.

If you’re interested in getting involved in this effort, please check out the Net WG gitter and repo!