Reading and writing

The futures crate contains an io module, which is the async counterpart to std::io. That module defines, in particular, the core primitives for doing async reading, writing, and flushing:

  1. trait AsyncRead {
  2. fn read(&mut self, buf: &mut [u8]) -> AsyncIoResult<usize>;
  3. }
  4. trait AsyncWrite {
  5. fn write(&mut self, buf: &[u8]) -> AsyncIoResult<usize>;
  6. fn flush(&mut self) -> AsyncIoResult<()>;
  7. }

These methods work exactly like their counterparts in std, except that if the underlying I/O object is not ready to perform the requested action, they return Ok(Async::WillWake), and stash the given wake to be used once I/O is ready. Once more, the fact that their result type involves Async is the clear signal that they plug into the async task system.

Example: echoing input

While the AsyncRead and AsyncWrite traits are simple enough, there are some significant differences in using them, compared to the synchronous versions. Most importantly, async tasks generally have an explicit overall state associated with them (which plays the role usually played by the stack in synchronous programming). To see this concretely, let’s write a task for echoing everything sent on a socket. First, the basic setup:

  1. use tokio::net::TcpStream;
  2. // The task structure -- echoing on a *single* connection
  3. struct Echo {
  4. // The connection
  5. io: TcpStream,
  6. // Buffered data to be echoed back
  7. buf: Vec<u8>,
  8. // The current state of the "echo state machine"
  9. state: EchoState,
  10. }
  11. enum EchoState {
  12. // The next step is reading into `buf`, from the front
  13. Reading,
  14. // The next step is echoing back out `buf`, from the
  15. // given start and end points.
  16. Writing(usize, usize),
  17. }
  18. impl Echo {
  19. fn new(io: TcpStream) -> Echo {
  20. Echo {
  21. io,
  22. state: EchoState::Reading,
  23. buf: vec![0; 4096],
  24. }
  25. }
  26. }

The idea then is that the Echo task alternates between reading and writing. If at any point it is unable to perform that task, it returns Async::WillWake, having been enrolled to be woken when the needed I/O is available. Such state-machine tasks almost always have an outer loop that continuously moves through the states until an obstruction is reached:

  1. impl Future for Echo {
  2. type Item = ();
  3. type Error = io::Error;
  4. fn complete(&mut self) -> AsyncIoResult<()> {
  5. loop {
  6. self.state = match self.state {
  7. EchoState::Reading => {
  8. match self.io.read(&mut self.buf)? {
  9. Async::WillWake => return Ok(Async::WillWake),
  10. Async::Done(len) => EchoState::Writing(0, len),
  11. }
  12. }
  13. EchoState::Writing(from, to) if from >= to => {
  14. EchoState::Reading
  15. }
  16. EchoState::Writing(from, to) => {
  17. match self.io.write(&self.buf[from..to])? {
  18. Async::WillWake => return Ok(Async::WillWake),
  19. Async::Done(n) => EchoState::Writing(from + n, to),
  20. }
  21. }
  22. };
  23. }
  24. }
  25. }

It’s important to note that we can freely “bubble up” WillWake, because if a function like read, returns it, that function has already guaranteed to wake up our task when reading is possible. In particular, the tokio crate takes care of stashing the WakeHandle as necessary whenever we attempt an AsyncRead::read, and so on. All we have to do is bubble out the WillWake result.

While the code here is not so complicated, it’s a bit noisy for something so simple. Much of the rest of this book will cover higher-level abstractions that cut down on the noise. For this kind of low-level programming, though, the futures crate provides a try_done macro that works much like the ? operator, except that it also bubbles out Async::WillWake. Using that macro, we can rewrite the code as follows:

  1. impl Future for Echo {
  2. type Item = ();
  3. type Error = io::Error;
  4. fn complete(&mut self) -> AsyncIoResult<()> {
  5. loop {
  6. self.state = match self.state {
  7. EchoState::Reading => {
  8. let let = try_done!(self.io.read(&mut self.buf));
  9. EchoState::Writing(0, len)
  10. }
  11. EchoState::Writing(from, to) if from >= to => {
  12. EchoState::Reading
  13. }
  14. EchoState::Writing(from, to) => {
  15. let n = try_done!(self.io.write(&self.buf[from..to]))
  16. EchoState::Writing(from + n, to)
  17. }
  18. };
  19. }
  20. }
  21. }

As we’ll see in the Futures chapter, though, we’ll ultimately do better than this, by avoid writing a state machine at all.

Exercises

  • What would happen if we did not include the outer loop?
  • Use the CurrentThread executor and TcpListener to turn the above code into a complete, working server.

https://gist.github.com/alexcrichton/da80683060f405d6be0e06b426588886