Postcard
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:
- Minimize memory usage
- Minimize code size
- Optimize developer time
- 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
- GitHub Repository: https://github.com/jamesmunns/postcard
- Documentation: https://docs.rs/postcard/
- Wire Format Specification: https://postcard.jamesmunns.com/wire-format
- v1.0 Release Blog: https://jamesmunns.com/blog/postcard-1-0/
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(())
}