Thanks to visit codestin.com
Credit goes to lib.rs

#network-protocol #networking

sansio

sansio — an architectural pattern for writing protocol implementations that are completely decoupled from I/O operations

19 releases (2 stable)

Uses new Rust 2024

1.0.1 Dec 19, 2025
0.10.0 Dec 19, 2025
0.7.0 Dec 18, 2025
0.0.7 Nov 27, 2025

#703 in Network programming

Codestin Search App Codestin Search App Codestin Search App Codestin Search App Codestin Search App Codestin Search App Codestin Search App

633 downloads per month
Used in 8 crates (7 directly)

MIT/Apache

35KB
109 lines

sansio

License: MIT/Apache 2.0

Rust in Sans-IO

What is Sans-IO?

Sans-IO (French for "without I/O") is an architectural pattern for writing protocol implementations that are completely decoupled from I/O operations. This makes protocols:

  • Testable: Test protocol logic without mocking sockets or async runtimes
  • Portable: Run the same protocol code in sync, async, embedded, or WASM environments
  • Composable: Stack and combine protocol layers easily
  • Debuggable: Step through protocol state machines without I/O side effects

Features

  • Clean push-pull API for message handling
  • Zero-cost abstractions with generic type parameters
  • Support for custom events and timeout handling
  • Works with any I/O backend (sync, async, embedded)
  • No dependencies, minimal footprint
  • Fully documented with examples
  • no_std by default - works in any environment

no_std Support

This crate is no_std by default and works seamlessly in any environment - embedded systems, bare-metal applications, WASM, or standard applications.

Time Handling

The Time associated type is fully generic, so you can use any time representation that fits your environment:

Using std::time::Instant:

impl Protocol<Vec<u8>, Vec<u8>, ()> for MyProtocol {
    type Time = std::time::Instant;
    // ...
}

Using tick counts (embedded):

impl Protocol<Vec<u8>, Vec<u8>, ()> for MyProtocol {
    type Time = u64;  // System ticks
    // ...
}

Using milliseconds:

impl Protocol<Vec<u8>, Vec<u8>, ()> for MyProtocol {
    type Time = i64;  // Milliseconds since epoch
    // ...
}

No timeout needed:

impl Protocol<Vec<u8>, Vec<u8>, ()> for MyProtocol {
    type Time = ();  // Unit type when timeouts aren't used
    // ...
}

The Protocol Trait

The Protocol trait provides a simplified Sans-IO interface for building network protocols:

pub trait Protocol<Rin, Win, Ein> {
    type Rout;  // Output read type
    type Wout;  // Output write type
    type Eout;  // Output event type
    type Error; // Error type
    type Time;  // Time type (u64, Instant, i64, (), etc.)

    // Push data into protocol
    fn handle_read(&mut self, msg: Rin) -> Result<(), Self::Error>;
    fn handle_write(&mut self, msg: Win) -> Result<(), Self::Error>;
    fn handle_event(&mut self, evt: Ein) -> Result<(), Self::Error>;
    fn handle_timeout(&mut self, now: Self::Time) -> Result<(), Self::Error>;

    // Pull results from protocol
    fn poll_read(&mut self) -> Option<Self::Rout>;
    fn poll_write(&mut self) -> Option<Self::Wout>;
    fn poll_event(&mut self) -> Option<Self::Eout>;
    fn poll_timeout(&mut self) -> Option<Self::Time>;

    // Lifecycle
    fn close(&mut self) -> Result<(), Self::Error>;
}

Quick Start

Add sansio to your Cargo.toml:

[dependencies]
sansio = "1"

Example: Uppercase Protocol

A simple protocol that converts incoming strings to uppercase:

use sansio::Protocol;
use std::collections::VecDeque;

#[derive(Debug)]
struct MyError;
impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "MyError")
    }
}
impl std::error::Error for MyError {}

/// A simple uppercase protocol: converts incoming strings to uppercase
struct UppercaseProtocol {
    routs: VecDeque<String>,
    wouts: VecDeque<String>,
}

impl UppercaseProtocol {
    fn new() -> Self {
        Self {
            routs: VecDeque::new(),
            wouts: VecDeque::new(),
        }
    }
}

impl Protocol<String, String, ()> for UppercaseProtocol {
    type Rout = String;
    type Wout = String;
    type Eout = ();
    type Error = MyError;
    type Time = ();  // No timeout handling needed

    fn handle_read(&mut self, msg: String) -> Result<(), Self::Error> {
        // Process incoming message
        self.routs.push_back(msg.to_uppercase());
        Ok(())
    }

    fn poll_read(&mut self) -> Option<Self::Rout> {
        // Return processed message
        self.routs.pop_front()
    }

    fn handle_write(&mut self, msg: String) -> Result<(), Self::Error> {
        // For this simple protocol, just pass through
        self.wouts.push_back(msg);
        Ok(())
    }

    fn poll_write(&mut self) -> Option<Self::Wout> {
        self.wouts.pop_front()
    }
}

// Usage example
fn main() {
    let mut protocol = UppercaseProtocol::new();

    // Push data in
    protocol.handle_read("hello world".to_string()).unwrap();

    // Pull results out
    assert_eq!(protocol.poll_read(), Some("HELLO WORLD".to_string()));
}

Testing Made Easy

Sans-IO protocols are trivial to test because they don't involve any I/O:

#[test]
fn test_uppercase_protocol() {
    let mut protocol = UppercaseProtocol::new();

    // Test single message
    protocol.handle_read("test".to_string()).unwrap();
    assert_eq!(protocol.poll_read(), Some("TEST".to_string()));

    // Test multiple messages
    protocol.handle_read("hello".to_string()).unwrap();
    protocol.handle_read("world".to_string()).unwrap();
    assert_eq!(protocol.poll_read(), Some("HELLO".to_string()));
    assert_eq!(protocol.poll_read(), Some("WORLD".to_string()));
    assert_eq!(protocol.poll_read(), None);
}

Use Cases

  • Network Protocols: HTTP, WebSocket, custom TCP/UDP protocols
  • Message Parsers: Protocol buffers, JSON-RPC, custom formats
  • State Machines: Connection handling, handshakes, negotiations
  • Protocol Testing: Unit test protocol logic without network I/O
  • Embedded Systems: Protocols that need to work with both blocking and non-blocking I/O
  • WASM: Browser-based protocol implementations

Why Sans-IO?

Traditional protocol implementations mix I/O and protocol logic:

// Traditional approach - tightly coupled to async I/O
async fn handle_connection(mut stream: TcpStream) {
    let mut buf = [0u8; 1024];
    while let Ok(n) = stream.read(&mut buf).await {
        // Protocol logic mixed with I/O
        let response = process(&buf[..n]);
        stream.write_all(&response).await.unwrap();
    }
}

Sans-IO separates concerns:

// Sans-IO approach - protocol logic is independent
struct MyProtocol {
    /* ... */
}
impl Protocol<Vec<u8>, Vec<u8>, ()> for MyProtocol { /* ... */ }

// I/O layer is separate and can be swapped
async fn handle_connection(mut stream: TcpStream, mut protocol: MyProtocol) {
    let mut buf = [0u8; 1024];
    while let Ok(n) = stream.read(&mut buf).await {
        protocol.handle_read(buf[..n].to_vec()).unwrap();
        while let Some(response) = protocol.poll_write() {
            stream.write_all(&response).await.unwrap();
        }
    }
}

Benefits:

  • Protocol logic can be tested without async runtime
  • Same protocol works with sync, async, or embedded I/O
  • Easier to debug and reason about
  • Protocol layers can be composed and reused

Documentation

Full API documentation is available at docs.rs/sansio

License

Licensed under either of:

at your option.

No runtime deps