Skip to main content

Crate asansio

Crate asansio 

Source
Expand description

§asansio

This library contains the async/await state machine for the sans-io design pattern. 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.

This crate could be used also for non network protocol cases, everywhere there is a need for creating a state machine.

This is no_std crate and it doesn’t allocate on the heap.

§Usage

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));

This 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. There are two types to manage them: the Io and the Sans, which are constructed by the new function. These two parts communicate using Request and Respond types, which are defined by the user (for real scenarios they could be enums).

Sans starts communicating with Io using Sans::handle and providing the initial Request; it returns the Response from the Io. Io starts sans task by using Io::start which returns (IoHandle, Request) from Sans. The later communication is done using Sans::handle and Io::handle.

Possible design for the implementing protocol is to create an async interface trait, which Sans will use as an interface to the Io. Application which uses async runtime can provide implementation of this trait using runtime features and use Sans task directly . The one which uses only std should implement the interface trait and use asansio crate for communicating between that interface and main loop. See a similar design in examples.

§Safety

The crate uses unsafe parts for preparing a proper Waker with internal Channel<Request, Response>. Safety is guaranteed by consuming the latest IoHandle and that you cannot mix Request or Response types (it is guaranteed by generic types of Io, IoHandle and Sans. Request and Response types are consumed by values.

Structs§

Io
Manages the Io part
IoHandle
The holder of the Request from the Sans to Io
Sans
Manages the Sans part

Enums§

Error

Functions§

new
Creates a two parts: Sans and Io for the specified Request and Response.