Skip to content

(ab)using Rust's async/await syntax to write simple state machine based coroutines

License

Notifications You must be signed in to change notification settings

sminez/simple_coro

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Simple Coro

Build crates.io version docs.rs

(ab)using Rust's async/await syntax to write simple state-machine based coroutines.

⚠️ Formally known as crimes (see below) ⚠️

Why is this criminal?

Originally the "crimes" aspect of this was due to it being a quickly hacked together proof of concept that had a safe(ish) API. As I've continued to work on things it's beginning to look more and more reasonable but I'd still argue that this is probably not what you want, particularly if you are happy using the nightly compiler support for coroutines.

A note on API changes from the blog post

After writing my Socrates is a state machine blog post I couldn't resist trying to simplify the API, so rather than what is presented there (a paired runner and state machine) the updated API now handles everything with a single struct: Coro. It also enforces that replies are sent before calling resume again via a lifecycle typestate and provides a number of quality of life functionality that simplifies writing simple coroutines.

To take a look through the API as it was when I wrote the blog post you'll need to go here.

use simple_coro::{Coro, CoroState, Handle};
use std::io::{self, Cursor, ErrorKind};

// ["Hello", "世界"] in 9p wire format.
const HELLO_WORLD: [u8; 17] = [
    0x02, 0x00, 0x05, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x06, 0x00, 0xe4, 0xb8, 0x96, 0xe7, 0x95,
    0x8c,
];

#[tokio::main]
async fn main() {
    parse_sync();
    parse_async().await;
}

fn parse_sync() {
    use std::io::Read;

    let mut r = Cursor::new(HELLO_WORLD.to_vec());
    let parsed = Coro::from(read_9p_string_vec)
        .run_sync(|n| {
            let mut buf = vec![0; n];
            r.read_exact(&mut buf).unwrap();
            buf
        })
        .expect("parsing to be successful");

    assert_eq!(parsed, &["Hello", "世界"]);
}

async fn parse_async() {
    use tokio::io::AsyncReadExt;

    let mut coro = Coro::from(read_9p_string_vec);
    let mut r = Cursor::new(HELLO_WORLD.to_vec());

    loop {
        coro = match coro.resume() {
            CoroState::Complete(parsed) => {
                assert_eq!(parsed.unwrap(), &["Hello", "世界"]);
                return;
            }
            CoroState::Pending(c, n) => c.send({
                let mut buf = vec![0; n];
                r.read_exact(&mut buf).await.unwrap();
                buf
            }),
        };
    }
}

async fn read_9p_u16(handle: Handle<usize, Vec<u8>>) -> io::Result<u16> {
    let n = size_of::<u16>();
    let buf = handle.yield_value(n).await;
    let data = buf[0..n].try_into().unwrap();

    Ok(u16::from_le_bytes(data))
}

async fn read_9p_string(handle: Handle<usize, Vec<u8>>) -> io::Result<String> {
    let len = handle.yield_from(read_9p_u16).await? as usize;
    let buf = handle.yield_value(len).await;

    String::from_utf8(buf).map_err(|e| io::Error::new(ErrorKind::InvalidData, e.to_string()))
}

async fn read_9p_string_vec(handle: Handle<usize, Vec<u8>>) -> io::Result<Vec<String>> {
    let len = handle.yield_from(read_9p_u16).await? as usize;
    let mut buf = Vec::with_capacity(len);
    for _ in 0..len {
        buf.push(handle.yield_from(read_9p_string).await?);
    }

    Ok(buf)
}

About

(ab)using Rust's async/await syntax to write simple state machine based coroutines

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages