Skip to content
Oeiuwq Faith Blog OpenSource Porfolio

ewienik/asansio

Async/await state machine lite framework for sans-io functionality

ewienik/asansio.json
{
"createdAt": "2025-09-09T21:54:50Z",
"defaultBranch": "master",
"description": "Async/await state machine lite framework for sans-io functionality",
"fullName": "ewienik/asansio",
"homepage": null,
"language": "Rust",
"name": "asansio",
"pushedAt": "2026-06-27T20:23:52Z",
"stargazersCount": 10,
"topics": [],
"updatedAt": "2026-07-18T18:20:13Z",
"url": "https://github.com/ewienik/asansio"
}

Async/await state machine for the Sans I/O design pattern.

crates.io docs.rs

This crate is a experiment for Sans I/O in Rust (see sans I/O for network protocols documentation to familiar with the concept). Writing network protocol without performing I/O operations means creating a state machine. Manually creating a state machine could be a tedious process. As Rust async/await concept is an actual state machine with implicit states created during compilation, this library is an experiment with using async/await to provide such state machine automatically. Let’s check if this is good solution to the problem.

It is no_std crate without allocations on the heap. It depends only on the core, no other crates. Only examples uses std, clap and tokio as dev-dependencies.

You can provide async iface trait that can handle all SansIo handling. This way you can use single protocol defined in async way with std or async runtime.

This is the example of std interface and client:

use asansio::Io;
use asansio::IoHandle;
use asansio::Sans;
use std::pin::Pin;
pub trait Iface {
async fn recv(&mut self) -> Option<usize>;
async fn send(&mut self, value: usize) -> Option<()>;
}
async fn run(mut iface: impl Iface) -> Option<()> {
loop {
let value = iface.recv().await?;
iface.send(value + 1).await?;
}
}
type Request = Option<usize>;
type Response = Option<usize>;
struct IfaceStd {
response: Option<usize>,
sans: Sans<Request, Response>,
}
impl Iface for IfaceStd {
async fn recv(&mut self) -> Option<usize> {
if let Some(response) = self.response.take() {
return Some(response);
}
self.sans.handle(None).await.unwrap()
}
async fn send(&mut self, value: usize) -> Option<()> {
self.response = self.sans.handle(Some(value)).await.unwrap();
self.response.map(|_| ())
}
}
struct ClientStd {
io: Io<Request, Response>,
handle: Option<IoHandle<Request, Response, Box<dyn Future<Output = ()>>>>,
}
impl ClientStd {
fn new() -> Option<Self> {
let (sans, io) = asansio::new::<Request, Response>();
let proto = Box::pin({
async move {
run(IfaceStd { response: None, sans }).await.unwrap_or(());
}
}) as Pin<Box<dyn Future<Output = ()>>>;
let (handle, request) = io.start(proto).ok().flatten()?;
assert_eq!(request, None);
let handle = Some(handle);
Some(Self { io, handle })
}
fn call(&mut self, value: usize) -> Option<usize> {
let handle = self.handle.take().unwrap();
let (handle, request) = self.io.handle(handle, Some(value)).unwrap()?;
self.handle = Some(handle);
request
}
}
let mut client = ClientStd::new().unwrap();
assert_eq!(client.call(0), Some(1));
assert_eq!(client.call(2), Some(3));
assert_eq!(client.call(8), Some(9));

The crate divides a problem into two parts. The first Sans takes care of the state machine independent of the I/O and the second Io is responsible with I/O communication. These two parts communicate using Request and Respond types, which are defined by the user (for real scenarios they could be enums).

See also more [examples]!(examples).

Licensed under either of

at your option.