Expand description
§Sans-IO Protocol Framework
A lightweight, zero-dependency framework for building protocol implementations that are completely decoupled from I/O operations.
§What is Sans-IO?
Sans-IO (French for “without I/O”) is an architectural pattern that separates protocol logic from I/O handling. This separation provides several key benefits:
- Testability: Protocol logic can be tested without mocking sockets or async runtimes
- Portability: Same protocol works in sync, async, embedded, or WASM environments
- Composability: Multiple protocol layers can be easily stacked and combined
- Debuggability: State machines can be inspected without I/O side effects
§no_std Support
This crate is no_std by default and works in any environment - embedded systems,
WASM, or standard applications.
§Time Handling
The Time associated type is fully generic, allowing you to use any time representation:
- With std: Use
std::time::Instantorstd::time::SystemTime - Embedded/bare-metal: Use
u64for tick counts,i64for milliseconds, or custom time types - No timeouts: Use
()(unit type) when timeout handling isn’t needed
This flexibility means timeout functionality works everywhere - you just choose the appropriate time type for your platform.
§The Protocol Trait
The Protocol trait provides a push-pull API for handling messages:
- Push API (
handle_*): Push data/events into the protocol - Pull API (
poll_*): Poll results from the protocol
This design enables complete I/O independence while maintaining a clean, intuitive interface.
§Type Parameters
Rin: Input read message type (what you push in for reading)Win: Input write message type (what you push in for writing)Ein: Input event type (custom events specific to your protocol)
§Associated Types
Rout: Output read message type (what you poll after reading)Wout: Output write message type (what you poll after writing)Eout: Output event type (events generated by the protocol)Error: Error type for operationsTime: Time/instant type for timeout handling (can beInstant,u64,i64,(), etc.)
§Basic Example
use sansio::Protocol;
/// 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
let mut protocol = UppercaseProtocol::new();
// Push data in
protocol.handle_read("hello".to_string()).unwrap();
// Pull results out
assert_eq!(protocol.poll_read(), Some("HELLO".to_string()));§Timeout Handling Example
Protocols can handle time-based operations like heartbeats or retransmissions.
§Using std::time::Instant
use sansio::Protocol;
use std::time::{Duration, Instant};
/// A protocol that sends periodic heartbeats
struct HeartbeatProtocol {
next_heartbeat: Option<Instant>,
heartbeat_interval: Duration,
pending_write: Option<Vec<u8>>,
}
impl HeartbeatProtocol {
fn new(interval: Duration) -> Self {
Self {
next_heartbeat: None,
heartbeat_interval: interval,
pending_write: None,
}
}
}
impl Protocol<Vec<u8>, Vec<u8>, ()> for HeartbeatProtocol {
type Rout = Vec<u8>;
type Wout = Vec<u8>;
type Eout = ();
type Error = MyError;
type Time = Instant; // Using std::time::Instant
fn handle_read(&mut self, msg: Vec<u8>) -> Result<(), Self::Error> {
// Reset heartbeat timer on any received message
self.next_heartbeat = Some(Instant::now() + self.heartbeat_interval);
Ok(())
}
fn poll_read(&mut self) -> Option<Self::Rout> {
None
}
fn handle_write(&mut self, msg: Vec<u8>) -> Result<(), Self::Error> {
self.pending_write = Some(msg);
Ok(())
}
fn poll_write(&mut self) -> Option<Self::Wout> {
self.pending_write.take()
}
fn handle_timeout(&mut self, now: Instant) -> Result<(), Self::Error> {
// Send heartbeat
self.pending_write = Some(b"HEARTBEAT".to_vec());
self.next_heartbeat = Some(now + self.heartbeat_interval);
Ok(())
}
fn poll_timeout(&mut self) -> Option<Instant> {
self.next_heartbeat
}
}§Using tick counts (no_std friendly)
use sansio::Protocol;
/// A protocol with timeout using tick counts (no_std friendly)
struct TickProtocol {
next_timeout_tick: Option<u64>,
timeout_interval: u64,
messages: VecDeque<Vec<u8>>,
}
impl TickProtocol {
fn new(timeout_interval: u64) -> Self {
Self {
next_timeout_tick: None,
timeout_interval,
messages: VecDeque::new(),
}
}
}
impl Protocol<Vec<u8>, Vec<u8>, ()> for TickProtocol {
type Rout = Vec<u8>;
type Wout = Vec<u8>;
type Eout = ();
type Error = MyError;
type Time = u64; // Using tick counts for embedded systems
fn handle_read(&mut self, msg: Vec<u8>) -> Result<(), Self::Error> {
self.messages.push_back(msg);
Ok(())
}
fn poll_read(&mut self) -> Option<Self::Rout> {
self.messages.pop_front()
}
fn handle_write(&mut self, msg: Vec<u8>) -> Result<(), Self::Error> {
self.messages.push_back(msg);
Ok(())
}
fn poll_write(&mut self) -> Option<Self::Wout> {
self.messages.pop_front()
}
fn handle_timeout(&mut self, current_tick: u64) -> Result<(), Self::Error> {
// Handle timeout at current tick
self.next_timeout_tick = Some(current_tick + self.timeout_interval);
Ok(())
}
fn poll_timeout(&mut self) -> Option<u64> {
self.next_timeout_tick
}
}§Event Handling Example
Protocols can generate and handle custom events:
use sansio::Protocol;
/// Custom event types
#[derive(Debug, PartialEq)]
enum ConnectionEvent {
Connected,
Disconnected,
Error(String),
}
/// Protocol that tracks connection state
struct ConnectionProtocol {
connected: bool,
event_queue: VecDeque<ConnectionEvent>,
}
impl ConnectionProtocol {
fn new() -> Self {
Self {
connected: false,
event_queue: VecDeque::new(),
}
}
}
impl Protocol<String, String, ConnectionEvent> for ConnectionProtocol {
type Rout = String;
type Wout = String;
type Eout = ConnectionEvent;
type Error = MyError;
type Time = (); // No timeout needed
fn handle_read(&mut self, msg: String) -> Result<(), Self::Error> {
Ok(())
}
fn poll_read(&mut self) -> Option<Self::Rout> {
None
}
fn handle_write(&mut self, msg: String) -> Result<(), Self::Error> {
Ok(())
}
fn poll_write(&mut self) -> Option<Self::Wout> {
None
}
fn handle_event(&mut self, evt: ConnectionEvent) -> Result<(), Self::Error> {
match evt {
ConnectionEvent::Connected => {
self.connected = true;
self.event_queue.push_back(ConnectionEvent::Connected);
}
ConnectionEvent::Disconnected => {
self.connected = false;
self.event_queue.push_back(ConnectionEvent::Disconnected);
}
ConnectionEvent::Error(msg) => {
self.event_queue.push_back(ConnectionEvent::Error(msg));
}
}
Ok(())
}
fn poll_event(&mut self) -> Option<Self::Eout> {
self.event_queue.pop_front()
}
}§Protocol Composition
Protocols can be layered and composed. Here’s a simple example of wrapping one protocol with another:
use sansio::Protocol;
/// A protocol wrapper that logs all messages
struct LoggingWrapper<P> {
inner: P,
}
impl<P> LoggingWrapper<P> {
fn new(inner: P) -> Self {
Self { inner }
}
}
impl<P, Rin, Win, Ein> Protocol<Rin, Win, Ein> for LoggingWrapper<P>
where
P: Protocol<Rin, Win, Ein>,
Rin: std::fmt::Debug,
Win: std::fmt::Debug,
{
type Rout = P::Rout;
type Wout = P::Wout;
type Eout = P::Eout;
type Error = P::Error;
type Time = P::Time; // Inherit time type from wrapped protocol
fn handle_read(&mut self, msg: Rin) -> Result<(), Self::Error> {
println!("READ: {:?}", msg);
self.inner.handle_read(msg)
}
fn poll_read(&mut self) -> Option<Self::Rout> {
self.inner.poll_read()
}
fn handle_write(&mut self, msg: Win) -> Result<(), Self::Error> {
println!("WRITE: {:?}", msg);
self.inner.handle_write(msg)
}
fn poll_write(&mut self) -> Option<Self::Wout> {
self.inner.poll_write()
}
}§Testing Protocols
Sans-IO protocols are trivial to test since they don’t involve any I/O:
#[test]
fn test_protocol() {
let mut protocol = UppercaseProtocol::new();
// Test single message
protocol.handle_read("hello".to_string()).unwrap();
assert_eq!(protocol.poll_read(), Some("HELLO".to_string()));
// Test multiple messages
protocol.handle_read("foo".to_string()).unwrap();
protocol.handle_read("bar".to_string()).unwrap();
assert_eq!(protocol.poll_read(), Some("FOO".to_string()));
assert_eq!(protocol.poll_read(), Some("BAR".to_string()));
assert_eq!(protocol.poll_read(), None);
}Traits§
- Protocol
- A Sans-IO protocol abstraction.