I've had the idea of designing Sans I/O protocols using Rust async/await concept for preparing a state machine. I've prepared the crate to find if this could have better developer experience. The crate brings this concept to experiment: asansio@crates.io
Sample usage could be described like this:
struct Request<'a>(&'a [u8]);
struct Response<'a>(&'a [u8]);
async fn sans_task<'a>(sans: Sans<Request<'a>, Response<'a>>) {
let mut request_buf = [1u8; 10];
let response = sans.start(&Request(&request_buf)).await;
assert_eq!(response.response().unwrap().0, [2; 20]);
request_buf.fill(3);
let response = sans.handle(response, &Request(&request_buf)).await;
assert_eq!(response.response().unwrap().0, [4; 20]);
}
let (sans, io) = asansio::new();
let task = pin!(sans_task(sans));
let request = io.start(task).unwrap();
assert_eq!(request.request().unwrap().0, [1; 10]);
let mut response_buf = [2; 20];
let request = io.handle(request, &Response(&response_buf)).unwrap();
assert_eq!(request.request().unwrap().0, [3; 10]);
response_buf.fill(4);
assert!(io.handle(request, &Response(&response_buf)).is_none());
The crate could be useful also in other cases when you need to create a state machine with clear communication around Request/Response enum/struct messages.
The next steps are checking API for developer experience, safety, benchmarks.
I don't understand the point of this framework. Sans IO is usually created to abstract "sync-ness" of IO. It defines a state machine that has to be driven by either sync, or async "runtime". The idea to create this state machine using async is nice, because it basically allows to crate state machine using generators, which are unstable today, but are used by async blocks.
However the question is, what should drive this this async state machine? Should I write an async executor for each project that uses this framework? How should this executor work? What should happen if polling state machine would block (poll returns Poll::Pending)? What prevents me from using in the state machine futures, that require some specific runtime, for example tokio.
Io::start() and Io::handle() drives this async state machine. You only provide pinned async task as a state machine and if you want to communicate from sans state machine you call Sans::start or State::handle - there is a specific Future SansHandle which communicates with Io part using Pending state. Pending means there is a request from Sans to Io. Io part sends response using Io::handle - then SansHandle will resolve to Ready(Response).
This SansHandle could be run only in this specific runtime. You cannot mix runtimes, so Futures from other runtimes won't work in Sans part. However you can use other runtimes to drive Io part.