Postcard

serializationRustno-stdembeddedbinary-format

Serialization Library

Postcard

Overview

Postcard is a no-std compatible Rust serialization library designed for embedded systems and memory-constrained environments. While maintaining compatibility with Serde, it encodes data in an extremely compact binary format. Since v1.0.0, wire format stability is guaranteed, making it ideal for communication in embedded systems and IoT devices.

Details

Postcard is a message library designed primarily for use in no-std environments. It's designed with resource efficiency (memory usage, code size, developer time, and CPU time) as the top priority.

Key Features:

  • no-std Support: Works without the standard library, ideal for embedded systems
  • Wire Format Stability: Backward compatibility guaranteed since v1.0.0
  • Compact Encoding: Efficient size through Varint (variable-length integer) encoding
  • Flavors System: Customization capabilities for serialization processing
  • Non-Self-Describing Format: Requires pre-shared schema but more compact as a result

Technical Details:

  • Encoding in little-endian order
  • Integers larger than 8 bits use Varint encoding
  • Safety through maximum encoding length restrictions
  • Canonicalization not enforced, but maximum value limits applied
  • Sponsored by Mozilla Corporation

Design Priorities:

  1. Minimize memory usage
  2. Minimize code size
  3. Optimize developer time
  4. Optimize CPU time

Pros and Cons

Pros

  • Excellent performance in embedded systems
  • Extremely small binary size
  • Full support in no-std environments
  • Long-term wire format stability (since v1.0.0)
  • Integration with Serde ecosystem
  • Flexible customization through Flavors

Cons

  • Not self-describing (requires pre-shared schema)
  • Difficult to debug (binary format)
  • No interoperability with other serialization formats
  • Learning curve (especially Flavors system)

References

Code Examples

Basic Usage

use serde::{Serialize, Deserialize};
use postcard::{to_vec, from_bytes};

#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct SensorData {
    temperature: f32,
    humidity: u8,
    timestamp: u32,
}

fn main() -> Result<(), postcard::Error> {
    let data = SensorData {
        temperature: 23.5,
        humidity: 65,
        timestamp: 1640995200,
    };
    
    // Serialize
    let serialized = to_vec(&data)?;
    println!("Serialized size: {} bytes", serialized.len());
    println!("Data: {:?}", serialized);
    
    // Deserialize
    let deserialized: SensorData = from_bytes(&serialized)?;
    println!("Deserialized: {:?}", deserialized);
    
    assert_eq!(data, deserialized);
    
    Ok(())
}

Usage in no-std Environment

#![no_std]

use serde::{Serialize, Deserialize};
use postcard::{to_slice, from_bytes};

#[derive(Serialize, Deserialize, Debug)]
struct Command {
    id: u16,
    action: Action,
    payload: [u8; 8],
}

#[derive(Serialize, Deserialize, Debug)]
enum Action {
    Read,
    Write,
    Execute,
}

fn process_command(buffer: &mut [u8]) -> Result<(), postcard::Error> {
    let cmd = Command {
        id: 0x1234,
        action: Action::Write,
        payload: [1, 2, 3, 4, 5, 6, 7, 8],
    };
    
    // Serialize to fixed-size buffer
    let used = to_slice(&cmd, buffer)?;
    let serialized = &buffer[..used.len()];
    
    // Deserialize
    let received: Command = from_bytes(serialized)?;
    
    // Process command
    match received.action {
        Action::Read => { /* Read operation */ },
        Action::Write => { /* Write operation */ },
        Action::Execute => { /* Execute operation */ },
    }
    
    Ok(())
}

Using the Flavors System

use serde::{Serialize, Deserialize};
use postcard::{
    ser_flavors::{Cobs, Slice},
    de_flavors::Flavor as DeFlavor,
    serialize_with_flavor,
    deserialize_with_flavor,
};

#[derive(Serialize, Deserialize, Debug)]
struct Message {
    id: u32,
    data: Vec<u8>,
}

fn main() -> Result<(), postcard::Error> {
    let msg = Message {
        id: 42,
        data: vec![0x00, 0xFF, 0x00, 0xFF],
    };
    
    // COBS (Consistent Overhead Byte Stuffing) encoding
    // Useful for byte boundary clarification
    let mut buffer = [0u8; 128];
    let slice_flavor = Slice::new(&mut buffer);
    let cobs_flavor = Cobs::try_new(slice_flavor)?;
    
    let used = serialize_with_flavor(&msg, cobs_flavor)?;
    let encoded = used.finalize();
    
    println!("COBS encoded size: {} bytes", encoded.len());
    
    // Decode
    let mut dec_buffer = [0u8; 128];
    let decoded = postcard::de_flavors::cobs::decode_in_place(&mut dec_buffer, encoded)?;
    let message: Message = from_bytes(decoded)?;
    
    println!("Decoded message: {:?}", message);
    
    Ok(())
}

Communication in Embedded Systems

#![no_std]

use serde::{Serialize, Deserialize};
use postcard::{to_slice, from_bytes};

#[derive(Serialize, Deserialize, Debug)]
struct TelemetryPacket {
    device_id: u16,
    sequence: u32,
    voltage: u16,      // in millivolts
    current: u16,      // in milliamps
    temperature: i16,  // in 0.1 degree units
    status: StatusFlags,
}

#[derive(Serialize, Deserialize, Debug)]
struct StatusFlags {
    power_good: bool,
    overtemp: bool,
    fault: bool,
}

// UART send function (hardware dependent)
fn uart_send(data: &[u8]) {
    // Actual UART send implementation
}

// UART receive function (hardware dependent)
fn uart_receive(buffer: &mut [u8]) -> usize {
    // Actual UART receive implementation
    0 // Return number of bytes received
}

fn send_telemetry(packet: &TelemetryPacket) -> Result<(), postcard::Error> {
    let mut buffer = [0u8; 64];
    let used = to_slice(packet, &mut buffer)?;
    uart_send(used);
    Ok(())
}

fn receive_telemetry(buffer: &mut [u8]) -> Result<TelemetryPacket, postcard::Error> {
    let received_len = uart_receive(buffer);
    let packet = from_bytes(&buffer[..received_len])?;
    Ok(packet)
}

Saving Configuration Structures

use serde::{Serialize, Deserialize};
use postcard::{to_allocvec, from_bytes};

#[derive(Serialize, Deserialize, Debug)]
struct DeviceConfig {
    version: u16,
    network: NetworkConfig,
    sensors: Vec<SensorConfig>,
}

#[derive(Serialize, Deserialize, Debug)]
struct NetworkConfig {
    ssid: heapless::String<32>,
    password: heapless::String<64>,
    channel: u8,
}

#[derive(Serialize, Deserialize, Debug)]
struct SensorConfig {
    id: u8,
    enabled: bool,
    sample_rate: u16,
    threshold: f32,
}

fn save_config_to_flash(config: &DeviceConfig) -> Result<Vec<u8>, postcard::Error> {
    // For environments with alloc support
    let serialized = to_allocvec(config)?;
    
    // Write to flash memory (implementation dependent)
    // flash_write(CONFIG_ADDRESS, &serialized);
    
    Ok(serialized)
}

fn load_config_from_flash(data: &[u8]) -> Result<DeviceConfig, postcard::Error> {
    let config = from_bytes(data)?;
    Ok(config)
}

Implementing Custom Flavors

use serde::Serialize;
use postcard::ser_flavors::{Flavor, Slice};

// Flavor with CRC calculation
struct CrcFlavor<'a> {
    inner: Slice<'a>,
    crc: u16,
}

impl<'a> CrcFlavor<'a> {
    fn new(buffer: &'a mut [u8]) -> Self {
        Self {
            inner: Slice::new(buffer),
            crc: 0xFFFF,
        }
    }
    
    fn update_crc(&mut self, byte: u8) {
        // Simple CRC16 calculation
        self.crc ^= byte as u16;
        for _ in 0..8 {
            if (self.crc & 0x0001) != 0 {
                self.crc = (self.crc >> 1) ^ 0xA001;
            } else {
                self.crc >>= 1;
            }
        }
    }
}

impl<'a> Flavor for CrcFlavor<'a> {
    type Output = (&'a [u8], u16);

    fn try_push(&mut self, byte: u8) -> postcard::Result<()> {
        self.update_crc(byte);
        self.inner.try_push(byte)
    }

    fn finalize(mut self) -> postcard::Result<Self::Output> {
        let data = self.inner.finalize()?;
        Ok((data, self.crc))
    }
}

#[derive(Serialize)]
struct Data {
    value: u32,
}

fn main() -> Result<(), postcard::Error> {
    let data = Data { value: 0x12345678 };
    let mut buffer = [0u8; 32];
    
    let flavor = CrcFlavor::new(&mut buffer);
    let (serialized, crc) = postcard::serialize_with_flavor(&data, flavor)?;
    
    println!("Serialized: {:?}", serialized);
    println!("CRC16: 0x{:04X}", crc);
    
    Ok(())
}