| Crates.io | sansio |
| lib.rs | sansio |
| version | 1.0.1 |
| created_at | 2025-11-06 03:21:02.073517+00 |
| updated_at | 2025-12-19 22:21:43.734407+00 |
| description | sansio — an architectural pattern for writing protocol implementations that are completely decoupled from I/O operations |
| homepage | https://webrtc.rs |
| repository | https://github.com/webrtc-rs/sansio |
| max_upload_size | |
| id | 1919016 |
| size | 61,073 |
Rust in 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:
no_std by default - works in any environmentThis crate is no_std by default and works seamlessly in any environment - embedded systems,
bare-metal applications, WASM, or standard applications.
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 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>;
}
Add sansio to your Cargo.toml:
[dependencies]
sansio = "1"
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()));
}
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);
}
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:
Full API documentation is available at docs.rs/sansio
Licensed under either of:
at your option.