Low-Level Systems Programming

Serialization Concepts

Serialization converts in-memory Rust data structures into a linear sequence of bytes (or text tokens) for storage or transmission, while deserialization reconstructs typed values from those bytes after validating structure and invariants.

In-Memory Rust Struct (`Order { id: 42, symbol: "BTC", qty: 100 }`)
       |
       +---[ Text Format (JSON / TOML) ]--------> `{"id":42,"symbol":"BTC","qty":100}`
       |     (Self-describing, human-readable)
       |
       +---[ Compact Binary (Postcard / Bincode)]-> `[0x2A, 0x03, b'B', b'T', b'C', 0x64]`
       |     (Schema-driven, minimal size)
       |
       +---[ Zero-Copy Archive (`rkyv` / `zerocopy`)]-> Aligned byte buffer cast directly to `&ArchivedOrder`!

Comparing Serialization Architectures in Rust

Architecture Ecosystem Examples Wire Size Deserialization Cost Schema Evolution Support
Self-Describing Text serde_json, toml Large (field names repeated as strings) Medium (UTF-8 + number parsing) High (easy to add/ignore optional fields)
Tagged Binary (TLV) prost (Protobuf), rmp-serde (MessagePack), ciborium (CBOR) Small (numeric field tags or compact maps) Low High (forward & backward compatible via field tags)
Schema-Less Positional Binary bincode, postcard (popular in no_std) Minimal (zero field tags; fields written in order) Very Low Low (reordering or inserting fields breaks old readers)
Zero-Copy / In-Place rkyv, zerocopy, bytemuck Medium (includes alignment padding & relative pointers) $O(1)$ / Near-Zero (validates buffer and casts pointer without allocating!) Low (tied to exact layout representation)

Designing a Trait-Based Codec (Encode and Decode)

How do serialization libraries like serde, bincode, and postcard serialize arbitrary nested structs and collections without runtime reflection? They combine two core traits—a type-driven Encode/Serialize trait and a Decode/Deserialize trait—composed recursively across primitives, slices, and structs:

use std::io::{self, ErrorKind, Read, Write};

pub trait WireEncode {
    fn encode<W: Write>(&self, writer: &mut W) -> io::Result<()>;
}

pub trait WireDecode: Sized {
    fn decode<R: Read>(reader: &mut R) -> io::Result<Self>;
}

impl WireEncode for u32 {
    fn encode<W: Write>(&self, writer: &mut W) -> io::Result<()> {
        writer.write_all(&self.to_le_bytes())
    }
}

impl WireDecode for u32 {
    fn decode<R: Read>(reader: &mut R) -> io::Result<Self> {
        let mut buf = [0u8; 4];
        reader.read_exact(&mut buf)?;
        Ok(u32::from_le_bytes(buf))
    }
}

impl WireEncode for String {
    fn encode<W: Write>(&self, writer: &mut W) -> io::Result<()> {
        let len = u32::try_from(self.len())
            .map_err(|_| io::Error::new(ErrorKind::InvalidInput, "string too long"))?;
        len.encode(writer)?;
        writer.write_all(self.as_bytes())
    }
}

impl WireDecode for String {
    fn decode<R: Read>(reader: &mut R) -> io::Result<Self> {
        let len = u32::decode(reader)? as usize;
        if len > 1024 * 1024 {
            return Err(io::Error::new(ErrorKind::InvalidData, "string exceeds 1 MiB cap"));
        }
        let mut bytes = vec![0u8; len];
        reader.read_exact(&mut bytes)?;
        String::from_utf8(bytes).map_err(|e| io::Error::new(ErrorKind::InvalidData, e))
    }
}

#[derive(Debug, PartialEq, Eq)]
pub struct TelemetrySample {
    pub sensor_id: u32,
    pub metric_name: String,
    pub value: u32,
}

impl WireEncode for TelemetrySample {
    fn encode<W: Write>(&self, writer: &mut W) -> io::Result<()> {
        self.sensor_id.encode(writer)?;
        self.metric_name.encode(writer)?;
        self.value.encode(writer)
    }
}

impl WireDecode for TelemetrySample {
    fn decode<R: Read>(reader: &mut R) -> io::Result<Self> {
        Ok(Self {
            sensor_id: u32::decode(reader)?,
            metric_name: String::decode(reader)?,
            value: u32::decode(reader)?,
        })
    }
}

fn main() -> io::Result<()> {
    let original = TelemetrySample {
        sensor_id: 7,
        metric_name: String::from("cpu_temp_celsius"),
        value: 62,
    };

    let mut buffer = Vec::new();
    original.encode(&mut buffer)?;

    let mut slice_reader = buffer.as_slice();
    let decoded = TelemetrySample::decode(&mut slice_reader)?;

    assert_eq!(original, decoded);
    println!(
        "Serialized into {} bytes and round-tripped: {decoded:?}",
        buffer.len()
    );
    Ok(())
}
Serialized into 28 bytes and round-tripped: TelemetrySample { sensor_id: 7, metric_name: "cpu_temp_celsius", value: 62 }