新しいバックエンドの追加
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.varints | VarInt 定義(プレフィックスマッチおよび継続ビット) |
module.consts | 名前付き定数 |
module.enums | Enum/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— パーサ / ASTwirespec-sema— セマンティック解析wirespec-layout— レイアウト IRwirespec-codec— コーデック IRwirespec-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を記述 - [ ]
BackendとBackendDynトレイトを実装 - [ ] 全フィールド戦略を処理(未対応は明示的なエラー)
- [ ] CLI バイナリに
BackendFactoryを登録 - [ ] 全サンプルファイルで
wirespec compile input.wspec -t xxxが動作 - [ ]
crates/wirespec-backend-xxx/tests/にテストを追加 - [ ] 生成コードがターゲットツールチェーンで警告ゼロでコンパイルされる
- [ ]
cargo test --workspaceが通過