Postcard

シリアライゼーションRustno-std組み込みバイナリ形式

シリアライゼーションライブラリ

Postcard

概要

Postcardは、組み込みシステムやメモリ制約のある環境向けに設計されたno-std対応のRustシリアライゼーションライブラリです。Serdeと互換性を持ちながら、極めてコンパクトなバイナリ形式でデータをエンコードします。v1.0.0以降、ワイヤーフォーマットの安定性が保証され、組み込みシステムやIoTデバイスでの通信に最適です。

詳細

Postcardは、主にno-std環境での使用を前提として設計されたメッセージライブラリです。リソース効率(メモリ使用量、コードサイズ、開発時間、CPU時間)を最優先に設計されています。

主な特徴:

  • no-std対応: 標準ライブラリなしで動作、組み込みシステムに最適
  • ワイヤーフォーマットの安定性: v1.0.0以降、後方互換性を保証
  • コンパクトなエンコーディング: Varint(可変長整数)による効率的なサイズ
  • Flavorsシステム: シリアライゼーション処理のカスタマイズ機能
  • 非自己記述形式: スキーマの事前共有が必要だが、その分コンパクト

技術的詳細:

  • リトルエンディアン順序でのエンコード
  • 8ビット以上の整数はVarintエンコーディング
  • 最大エンコード長の制限による安全性
  • 正規化は強制されないが、最大値制限は適用
  • Mozillaによるスポンサーシップ

設計優先順位:

  1. メモリ使用量の最小化
  2. コードサイズの最小化
  3. 開発時間の効率化
  4. CPU時間の最適化

メリット・デメリット

メリット

  • 組み込みシステムでの優れたパフォーマンス
  • 極めて小さなバイナリサイズ
  • no-std環境での完全なサポート
  • ワイヤーフォーマットの長期安定性(v1.0.0以降)
  • Serdeエコシステムとの統合
  • Flavorsによる柔軟なカスタマイズ

デメリット

  • 自己記述形式ではない(スキーマの事前共有が必要)
  • デバッグが困難(バイナリ形式)
  • 他のシリアライゼーション形式との相互運用性なし
  • 学習曲線(特にFlavorsシステム)

参考ページ

書き方の例

基本的な使用方法

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,
    };
    
    // シリアライズ
    let serialized = to_vec(&data)?;
    println!("Serialized size: {} bytes", serialized.len());
    println!("Data: {:?}", serialized);
    
    // デシリアライズ
    let deserialized: SensorData = from_bytes(&serialized)?;
    println!("Deserialized: {:?}", deserialized);
    
    assert_eq!(data, deserialized);
    
    Ok(())
}

no-std環境での使用

#![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],
    };
    
    // 固定サイズバッファにシリアライズ
    let used = to_slice(&cmd, buffer)?;
    let serialized = &buffer[..used.len()];
    
    // デシリアライズ
    let received: Command = from_bytes(serialized)?;
    
    // コマンド処理
    match received.action {
        Action::Read => { /* 読み取り処理 */ },
        Action::Write => { /* 書き込み処理 */ },
        Action::Execute => { /* 実行処理 */ },
    }
    
    Ok(())
}

Flavorsシステムの使用

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)エンコーディング
    // バイト境界の明確化に有用
    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());
    
    // デコード
    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(())
}

組み込みシステムでの通信

#![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,      // ミリボルト単位
    current: u16,      // ミリアンペア単位
    temperature: i16,  // 0.1度単位
    status: StatusFlags,
}

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

// UART送信関数(ハードウェア依存)
fn uart_send(data: &[u8]) {
    // 実際のUART送信実装
}

// UART受信関数(ハードウェア依存)
fn uart_receive(buffer: &mut [u8]) -> usize {
    // 実際のUART受信実装
    0 // 受信バイト数を返す
}

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)
}

設定構造の保存

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> {
    // allocを使用できる環境の場合
    let serialized = to_allocvec(config)?;
    
    // フラッシュメモリへの書き込み(実装依存)
    // flash_write(CONFI)G_ADDRESS, &serialized);
    
    Ok(serialized)
}

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

カスタムFlavorの実装

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

// CRC計算付きFlavor
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) {
        // 簡易CRC16計算
        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(())
}