Skip to content

新しいバックエンドの追加

wirespec に新しいコード生成バックエンドを追加する手順を説明します。C バックエンドと Rust バックエンドがリファレンス実装として使えます。同じ手順で将来のあらゆるターゲット言語(Go、Swift、Zig など)に対応できます。

前提知識

まず アーキテクチャIR パイプライン を読んでください。最も重要なポイント: バックエンドは CodecModule を消費し、AST は消費しない。 バックエンドが動く時点で、名前解決と型検査はすべて完了済みです。

アーキテクチャの概要

.wspec source
  → wirespec-syntax (parse)       → AST
  → wirespec-sema (analyze)       → Semantic IR
  → wirespec-layout (lower)       → Layout IR
  → wirespec-codec (lower)        → Codec IR  ← バックエンドはこれを消費する
  → wirespec-backend-XXX (lower)  → ターゲットコード (.go、.swift など)

ステップ 1: 新しいクレートを作成する

crates/wirespec-backend-xxx/ を作成し、Cargo.toml を記述します:

toml
[package]
name = "wirespec-backend-xxx"
version.workspace = true
edition.workspace = true

[dependencies]
wirespec-backend-api = { path = "../wirespec-backend-api" }
wirespec-codec = { path = "../wirespec-codec" }
wirespec-sema = { path = "../wirespec-sema" }  # Endianness、SemanticVarInt 等に使用

ステップ 2: Backend トレイトを実装する

rust
use wirespec_backend_api::*;
use wirespec_codec::CodecModule;

pub const TARGET_XXX: TargetId = TargetId("xxx");

pub struct XxxBackendOptions {
    // ターゲット固有オプション
}

impl Default for XxxBackendOptions {
    fn default() -> Self { Self {} }
}

pub struct XxxBackend;

impl Backend for XxxBackend {
    type LoweredModule = XxxLoweredModule;

    fn id(&self) -> TargetId { TARGET_XXX }

    fn lower(
        &self,
        module: &CodecModule,
        ctx: &BackendContext,
    ) -> Result<Self::LoweredModule, BackendError> {
        // ターゲットオプションのダウンキャスト
        let _opts = ctx.target_options.downcast_ref::<XxxBackendOptions>()
            .ok_or_else(|| BackendError::UnsupportedOption {
                target: TARGET_XXX,
                option: "target_options".into(),
                reason: "expected XxxBackendOptions".into(),
            })?;

        // CodecModule からコード生成
        let source = emit_source(module, &ctx.module_prefix);
        Ok(XxxLoweredModule { source, prefix: ctx.module_prefix.clone() })
    }

    fn emit(
        &self,
        lowered: &Self::LoweredModule,
        sink: &mut dyn ArtifactSink,
    ) -> Result<BackendOutput, BackendError> {
        sink.write(Artifact {
            target: TARGET_XXX,
            kind: ArtifactKind("xxx-source"),
            module_name: lowered.prefix.clone(),
            module_prefix: lowered.prefix.clone(),
            relative_path: format!("{}.xxx", lowered.prefix).into(),
            contents: lowered.source.as_bytes().to_vec(),
        })?;
        Ok(BackendOutput {
            target: TARGET_XXX,
            artifacts: vec![ArtifactMeta {
                kind: ArtifactKind("xxx-source"),
                relative_path: format!("{}.xxx", lowered.prefix).into(),
                byte_len: lowered.source.len(),
            }],
        })
    }
}

// レジストリディスパッチに必要
impl BackendDyn for XxxBackend {
    fn id(&self) -> TargetId { TARGET_XXX }
    fn lower_and_emit(
        &self, module: &CodecModule, ctx: &BackendContext, sink: &mut dyn ArtifactSink,
    ) -> Result<BackendOutput, BackendError> {
        let lowered = Backend::lower(self, module, ctx)?;
        Backend::emit(self, &lowered, sink)
    }
}

pub struct XxxLoweredModule {
    pub source: String,
    pub prefix: String,
}

ステップ 3: チェックサムバインディングの実装(オプション)

ターゲットがチェックサムをサポートする場合:

rust
use wirespec_backend_api::*;

pub struct XxxChecksumBindings;

impl ChecksumBindingProvider for XxxChecksumBindings {
    fn binding_for(&self, algorithm: &str) -> Result<ChecksumBackendBinding, BackendError> {
        match algorithm {
            "internet" => Ok(ChecksumBackendBinding {
                verify_symbol: Some("xxx_internet_checksum_verify".into()),
                compute_symbol: "xxx_internet_checksum_compute".into(),
                compute_style: ComputeStyle::PatchInPlace,
            }),
            _ => Err(BackendError::MissingChecksumBinding {
                target: TARGET_XXX,
                algorithm: algorithm.to_string(),
            }),
        }
    }
}

ステップ 4: CodecModule を理解する

バックエンドが消費する CodecModule には必要なすべてが含まれています:

フィールド提供する情報
module.packetsフィールド、アイテム、チェックサムプランを持つパケット定義
module.framesバリアントスコープを持つタグ付きユニオン定義
module.capsulesヘッダ + ペイロードバリアントを持つ TLV コンテナ
module.varintsVarInt 定義(プレフィックスマッチおよび継続ビット)
module.consts名前付き定数
module.enumsEnum/flags 定義
module.state_machines状態機械定義

CodecField は以下を持ちます:

  • strategy — パース/シリアライズ方法(Primitive、VarInt、BytesLength、Array、BitGroup など)
  • wire_type — ワイヤレベルの型
  • endianness — バイトオーダー
  • is_optional / condition — 条件付きフィールド情報
  • bytes_spec / array_spec / bitgroup_member — 戦略固有の詳細
  • checksum_algorithm — チェックサムアノテーションがある場合

完全な CodecModule スキーマは crates/wirespec-codec/src/ir.rs を参照してください。

ステップ 5: CLI に登録する

ワークスペースの Cargo.toml にクレートを追加:

toml
[workspace]
members = [
    # ... 既存メンバー ...
    "crates/wirespec-backend-xxx",
]

CLI バイナリに依存を追加しファクトリを登録:

rust
// crates/wirespec-driver/src/bin/wirespec.rs

struct XxxBackendFactory;
impl BackendFactory for XxxBackendFactory {
    fn id(&self) -> TargetId { wirespec_backend_xxx::TARGET_XXX }
    fn create(&self) -> Box<dyn BackendDyn> { Box::new(wirespec_backend_xxx::XxxBackend) }
    fn default_options(&self) -> Box<dyn std::any::Any + Send + Sync> {
        Box::new(wirespec_backend_xxx::XxxBackendOptions::default())
    }
}

fn build_registry() -> BackendRegistry {
    let mut reg = BackendRegistry::new();
    reg.register(Box::new(CBackendFactory));
    reg.register(Box::new(RustBackendFactory));
    reg.register(Box::new(XxxBackendFactory));  // ← この行を追加
    reg
}

これで wirespec compile input.wspec -t xxx が動作します。

ステップ 6: テストを追加する

コード生成の出力をテスト:

rust
fn generate_xxx(src: &str) -> String {
    let ast = wirespec_syntax::parse(src).unwrap();
    let sem = wirespec_sema::analyze(
        &ast,
        wirespec_sema::ComplianceProfile::default(),
        &Default::default(),
    ).unwrap();
    let layout = wirespec_layout::lower(&sem).unwrap();
    let codec = wirespec_codec::lower(&layout).unwrap();
    let backend = XxxBackend;
    let ctx = BackendContext {
        module_name: "test".into(),
        module_prefix: "test".into(),
        source_prefixes: Default::default(),
        compliance_profile: "phase2_extended_current".into(),
        common_options: CommonOptions::default(),
        target_options: Box::new(XxxBackendOptions::default()),
        checksum_bindings: Arc::new(XxxChecksumBindings),
        is_entry_module: true,
    };
    let lowered = Backend::lower(&backend, &codec, &ctx).unwrap();
    lowered.source
}

#[test]
fn codegen_simple_packet() {
    let src = generate_xxx("packet P { x: u8, y: u16 }");
    assert!(src.contains(/* expected pattern */));
}

アーティファクトテストには MemorySink を使用:

rust
let mut sink = MemorySink::new();
backend.lower_and_emit(&codec, &ctx, &mut sink).unwrap();
assert_eq!(sink.artifacts.len(), 1);

テストの実行:

bash
cargo test -p wirespec-backend-xxx

変更不要なクレート

  • wirespec-syntax — パーサ / AST
  • wirespec-sema — セマンティック解析
  • wirespec-layout — レイアウト IR
  • wirespec-codec — コーデック IR
  • wirespec-backend-api — バックエンドトレイト(TargetId、ArtifactKind 等はすべてオープン)
  • wirespec-driver — ドライバライブラリ(CLI バイナリのファクトリ登録のみ必要)

バックエンドがやってはいけないこと

  • AST ノードにアクセスしない。 CodecModule 以下のみ使用してください。
  • 名前解決を再実装しない。 IR に名前がなければ、上流クレート側の問題です。
  • 生成コードでヒープ確保しない。 スタックと呼び出し元バッファのみ許可されています。
  • IR 型を変更しない。 IR クレートは全バックエンド共有です。バックエンド固有の関心事はバックエンドクレートに閉じてください。
  • 生成コードは警告ゼロでコンパイルされること。

リファレンスバックエンド

  • crates/wirespec-backend-c/ — C バックエンド(ヘッダ + ソース分割、ビットグループシフト/マスク、チェックサム検証/計算)
  • crates/wirespec-backend-rust/ — Rust バックエンド(単一 .rs ファイル、ライフタイム追跡、フレーム用 Rust enum)

チェックリスト

新しいバックエンドの PR を出す前に:

  • [ ] crates/wirespec-backend-xxx/ を作成し適切な Cargo.toml を記述
  • [ ] BackendBackendDyn トレイトを実装
  • [ ] 全フィールド戦略を処理(未対応は明示的なエラー)
  • [ ] CLI バイナリに BackendFactory を登録
  • [ ] 全サンプルファイルで wirespec compile input.wspec -t xxx が動作
  • [ ] crates/wirespec-backend-xxx/tests/ にテストを追加
  • [ ] 生成コードがターゲットツールチェーンで警告ゼロでコンパイルされる
  • [ ] cargo test --workspace が通過