Skip to content

言語ツアー

wirespec のワイヤフォーマット機能を、実際に使う順序で一通り解説します。各セクションで機能の説明、実例、生成される C コードを示します。

初めての方は、先にはじめにでコンパイラをセットアップしてください。ステートマシンとモジュールは別ガイドで扱います。

1. プリミティブ型

標準的な符号あり・符号なし整数型を備えています。u8 = 1 バイト、u16 = 2 バイト、以下同様。マルチバイトフィールドのエンディアンはモジュールの @endian 設定に従います(未指定時はビッグエンディアン)。

wire
packet Example {
    a: u8,    # 1 byte, unsigned
    b: u16,   # 2 bytes, unsigned
    c: u32,   # 4 bytes, unsigned
    d: u64,   # 8 bytes, unsigned
    e: i8,    # 1 byte, signed
    f: i16,   # 2 bytes, signed
    g: i32,   # 4 bytes, signed
    h: i64,   # 8 bytes, signed
}

エンディアンネスを明示的に指定するには、サフィックス付きの型を使います:

wire
packet MixedEndian {
    big_val:    u32be,   # 4 bytes, big-endian
    little_val: u32le,   # 4 bytes, little-endian
    also_big:   u16be,
    also_le:    u16le,
}

u24 / u24be / u24le は 24 ビット(3 バイト)符号なし整数です。TLS のメッセージ長など 3 バイト幅のフィールドに使います:

wire
# From examples/tls/tls13.wspec
packet CertificateEntry {
    cert_data_length: u24,
    cert_data: bytes[cert_data_length],
    extensions_length: u16,
    extensions: bytes[extensions_length],
}

C への対応: u8uint8_tu16uint16_tu32uint32_tu64uint64_t。符号あり型は int8_tint16_t 等。u24uint32_t にマップされ、上位バイトはマスクで除去されます。パース関数は適切なバイト数を読み、必要に応じてバイトスワップします。

2. バイト型

固定長、長さプレフィックス付き、スコープ消費の 4 バリアントがあります。

固定長バイト

bytes[N] はちょうど N バイトを読み書きします。コピーではなくゼロコピーのビューを返します。

wire
# From examples/net/ethernet.wspec
packet EthernetFrame {
    dst_mac: bytes[6],
    src_mac: bytes[6],
    ether_type: u16,
    payload: bytes[remaining],
}
wire
# From examples/tls/tls13.wspec — 32-byte TLS random nonce
packet ClientHello {
    legacy_version: u16,
    random: bytes[32],
    ...
}

長さプレフィックス付きバイト

bytes[length: EXPR] は EXPR(先行フィールドや式)で指定されたバイト数だけ読み込みます。

wire
# From examples/mqtt/mqtt.wspec
packet MqttString {
    length: u16,
    data: bytes[length],
}

先行フィールドを使った算術式も書けます:

wire
# From examples/net/udp.wspec
packet UdpDatagram {
    src_port: u16,
    dst_port: u16,
    length: u16,
    checksum: u16,
    require length >= 8,
    data: bytes[length: length - 8],  # payload = total - 8-byte header
}

残りバイト

bytes[remaining] は現在のスコープの残りバイトをすべて消費します。スコープ内の最後のワイヤフィールドである必要があります。

wire
# From examples/ble/att.wspec
frame AttPdu = match opcode: u8 {
    0x0b => ReadRsp  { value: bytes[remaining] },
    0x12 => WriteReq { handle: AttHandle, value: bytes[remaining] },
    ...
}

オプション長バイト

bytes[length_or_remaining: EXPR] は長さフィールドがオプショナルな場合に使います。EXPR が Some なら指定バイト数だけ読み込み、null ならスコープの残り全バイトを消費します。EXPR の型は Option[T](T は整数型)でなければなりません。

wire
# From examples/quic/frames.wspec — Stream frame
0x08..=0x0f => Stream {
    stream_id:  VarInt,
    offset_raw: if frame_type & 0x04 { VarInt },
    length_raw: if frame_type & 0x02 { VarInt },
    data: bytes[length_or_remaining: length_raw],
    ...
},

C への対応: すべてのバイト型バリアントは wirespec_bytes_t になります。const uint8_t *ptrsize_t len を持つゼロコピー構造体で、メモリ割り当てはなく、ポインタは元の入力バッファを指します。

c
typedef struct { const uint8_t *ptr; size_t len; } wirespec_bytes_t;

3. エンディアンネス

マルチバイト整数フィールドにはエンディアン指定が必要です。モジュールレベルのデフォルトを設定し、フィールド単位でオーバーライドできます。

モジュールレベルのデフォルト

.wspec ファイルの先頭で @endian big または @endian little を指定します:

wire
# From examples/ble/att.wspec — BLE uses little-endian
@endian little
module ble.att

type AttHandle = u16le
type Uuid16    = u16le
wire
# QUIC and most network protocols use big-endian
@endian big
module quic.frames

フィールドごとのオーバーライド

明示的なエンディアン型(u16leu32be 等)のフィールドは、モジュールデフォルトに関わらず常にその型のエンディアンを使います。1 つの構造体内でエンディアンを混在させられます。

wire
packet MixedEndian {
    net_field:   u32,      # big-endian (from @endian big)
    le_field:    u32le,    # always little-endian
    another_net: u16,      # big-endian again
}

優先ルール: フィールドの明示的な型指定は常にモジュールデフォルトより優先されます。

4. 定数、列挙型、フラグ

定数

const でコンパイル時定数を定義します。型はプリミティブ整数型に限ります。

wire
# From examples/quic/frames.wspec
const MAX_CID_LENGTH: u8 = 20

packet LengthPrefixedCid {
    length: u8,
    value: bytes[length],
    require length <= MAX_CID_LENGTH,
}

列挙型

enum は名前と整数値の対応を定義します。: の後の型(基底型)がワイヤ上のタグ型になります。

wire
# From examples/tls/tls13.wspec
enum ContentType: u8 {
    ChangeCipherSpec = 20,
    Alert            = 21,
    Handshake        = 22,
    ApplicationData  = 23,
}

enum HandshakeType: u8 {
    ClientHello         = 1,
    ServerHello         = 2,
    Certificate         = 11,
    CertificateVerify   = 15,
    Finished            = 20,
}

enum はカプセルの match タグとして直接使えます:

wire
# From examples/tls/tls13.wspec
capsule TlsRecord {
    content_type: ContentType,
    legacy_version: u16,
    length: u16,
    payload: match content_type within length {
        22 => Handshake { data: bytes[remaining] },
        21 => Alert { level: u8, description: u8 },
        ...
    },
}

フラグ(ビットマスク)

flags は enum に似ていますが、値をビット OR で組み合わせる前提のビットマスクです。基底型は符号なし整数でなければなりません。

wire
flags PacketFlags: u8 {
    KeyPhase = 0x04,
    SpinBit  = 0x20,
    FixedBit = 0x40,
}

C への対応: enumflags も C の enum typedef を生成します。const#define マクロになります。

5. パケット(構造体)

packet は固定構造を定義します。名前付きフィールドが宣言順にパースされます。プロトコルヘッダの基本的な構成要素です。

wire
# From examples/net/udp.wspec
module net.udp
@endian big

packet UdpDatagram {
    src_port:  u16,
    dst_port:  u16,
    length:    u16,
    checksum:  u16,
    require length >= 8,
    data: bytes[length: length - 8],
}
wire
# From examples/net/tcp.wspec
packet TcpSegment {
    src_port:  u16,
    dst_port:  u16,
    seq_num:   u32,
    ack_num:   u32,
    data_offset: bits[4],
    reserved:    bits[4],
    cwr: bit, ece: bit, urg: bit, ack: bit,
    psh: bit, rst: bit, syn: bit, fin: bit,
    window:         u16,
    checksum:       u16,
    urgent_pointer: u16,
    require data_offset >= 5,
    options: bytes[length: data_offset * 4 - 20],
}

C への対応: packet は typedef struct になり、3 つの関数が生成されます:

c
wirespec_result_t udp_datagram_parse(
    const uint8_t *buf, size_t len,
    udp_datagram_t *out, size_t *consumed);

wirespec_result_t udp_datagram_serialize(
    const udp_datagram_t *in,
    uint8_t *buf, size_t cap, size_t *written);

size_t udp_datagram_serialized_len(const udp_datagram_t *in);

構造体にはスタック割り当ての値のみが入ります。wirespec は malloc を呼びません。

6. フレーム(タグ付きユニオン)

frame はタグ付きユニオンです。先頭フィールドがディスクリミネータとなり、その値で本体の構造が決まります。1 つのオペコード/タイプバイトから複数のレイアウトに分岐する場合に使います。

wire
# From examples/quic/frames.wspec (abbreviated)
frame QuicFrame = match frame_type: VarInt {
    0x00 => Padding {},
    0x01 => Ping {},

    0x06 => Crypto {
        offset: VarInt,
        length: VarInt,
        data:   bytes[length],
    },

    0x08..=0x0f => Stream {
        stream_id:  VarInt,
        offset_raw: if frame_type & 0x04 { VarInt },
        length_raw: if frame_type & 0x02 { VarInt },
        data:       bytes[length_or_remaining: length_raw],
        let offset: u64 = offset_raw ?? 0,
        let fin:   bool = (frame_type & 0x01) != 0,
    },

    _ => Unknown { data: bytes[remaining] },
}

パターン範囲

..= で閉区間のパターン範囲を書けます:

  • 0x02..=0x03 — タグ値 2 または 3 にマッチ
  • 0x08..=0x0f — 8 から 15(両端含む)にマッチ
  • _ — キャッチオール(網羅性のために必須)

具体値 > パターン範囲 > _ の優先順位です。

ブランチフィールドでのタグ参照

タグフィールド(上の例では frame_type)はすべてのブランチ内で参照可能です。if frame_type == 0x03 { EcnCounts } のように正確な値で分岐できます。

C への対応: frame は tag フィールド付きの構造体でラップされた C union になります。各バリアントはネストされた構造体です。パース関数はタグを読み取り、対応するブランチへディスパッチしてフィールドを埋めます。

7. カプセル(within を使った TLV)

capsule は Type-Length-Value コンテナです。固定ヘッダ(長さフィールド含む)の後、ペイロードが長さで区切られたサブスコープ内でパースされます。不正・未知のペイロードが割り当て範囲を超えて読み込むことはありません。

wire
# From examples/mqtt/mqtt.wspec
capsule MqttPacket {
    type_and_flags: u8,
    remaining_length: MqttLength,
    payload: match (type_and_flags >> 4) within remaining_length {
        1 => Connect {
            protocol_name: MqttString,
            protocol_level: u8,
            connect_flags: u8,
            keep_alive: u16,
            client_id: MqttString,
            will_topic:   if connect_flags & 0x04 { MqttString },
            will_message: if connect_flags & 0x04 { MqttBytes },
            username: if connect_flags & 0x80 { MqttString },
            password: if connect_flags & 0x40 { MqttBytes },
        },
        3 => Publish {
            topic: MqttString,
            let qos: u8 = (type_and_flags & 0x06) >> 1,
            packet_id: if qos > 0 { u16 },
            payload: bytes[remaining],
        },
        _ => Unknown { data: bytes[remaining] },
    },
}

within remaining_lengthremaining_length バイトに制限されたサブカーソルを作ります。サブスコープ内で:

  • 消費不足(バイトが余った)→ WIRESPEC_ERR_TRAILING_DATA
  • 消費超過(範囲外を読もうとした)→ WIRESPEC_ERR_SHORT_BUFFER

式タグ

capsule(や frame)のタグにはフィールド名だけでなく任意の式も使えます:

wire
payload: match (type_and_flags >> 4) within remaining_length { ... }

(type_and_flags >> 4) でフラグバイトの上位ニブルからパケット型を抽出しています。タグ式で参照するフィールドはすべて within より前に宣言されている必要があります。

C への対応: capsule は frame と同じ構造体/union パターンを生成し、長さ境界を強制するサブカーソルが追加されます。

TLS 拡張の例

wire
# From examples/tls/tls13.wspec
capsule Extension {
    extension_type: u16,
    length: u16,
    payload: match extension_type within length {
        0x002b => SupportedVersions  { data: bytes[remaining] },
        0x000d => SignatureAlgorithms { data: bytes[remaining] },
        0x0033 => KeyShare            { data: bytes[remaining] },
        0x0000 => ServerName          { data: bytes[remaining] },
        _ => Unknown { data: bytes[remaining] },
    },
}

8. 式言語

式はフィールド条件、導出フィールド、制約、配列サイズで使います。演算子の優先順位(低い順):

レベル演算子備考
コアレス??Option[T] ?? T — デフォルト値でアンラップ
論理 ORor
論理 ANDand
比較== != < <= > >=
ビット OR|
ビット XOR^
ビット AND&比較より強く結合
シフト<< >>
加減算+ -
乗除算* / %
単項! -
後置.field[idx][start..end]

重要: wirespec ではビット演算子が比較演算子より強く結合します(C とは逆)。a & mask == 0 は常に (a & mask) == 0 と解釈されます。C だと a & (mask == 0) になる定番のバグを回避できます。

?? コアレス演算子

?? でオプショナル値をデフォルト値付きでアンラップします:

wire
0x08..=0x0f => Stream {
    offset_raw: if frame_type & 0x04 { VarInt },
    ...
    let offset: u64 = offset_raw ?? 0,
}

offset_raw が存在する(0x04 ビットがセット)場合はその値を、存在しない場合は 0 を返します。

フィールド値の算術演算

数値が期待される場所なら、先行フィールドを使った式を自由に書けます:

wire
data: bytes[length: length - 8],           # subtract header overhead
cipher_suites: [u16; cipher_suites_length / 2],  # count in elements, not bytes
options: bytes[length: data_offset * 4 - 20],    # multiply and subtract

9. オプションフィールド

if COND { T } で条件付きフィールドを定義します。COND が真のときだけワイヤ上に存在し、型システムでは Option[T] として扱われます。

wire
# From examples/quic/frames.wspec — ECN counts only in ACK type 0x03
0x02..=0x03 => Ack {
    largest_ack:    VarInt,
    ack_range_count: VarInt,
    first_ack_range: VarInt,
    ack_ranges: [AckRange; ack_range_count],
    ecn_counts: if frame_type == 0x03 { EcnCounts },
},
wire
# From examples/mqtt/mqtt.wspec — QoS-dependent packet ID
3 => Publish {
    topic:  MqttString,
    let qos: u8 = (type_and_flags & 0x06) >> 1,
    packet_id: if qos > 0 { u16 },
    payload: bytes[remaining],
},

条件にはビットマスクテストなど先行フィールドの任意の式を使えます:

wire
# Present when a specific flag bit is set
offset_raw: if frame_type & 0x04 { VarInt },

C への対応: 存在フラグと値のペアになります:

c
bool has_ecn_counts;
ecn_counts_t ecn_counts;

オプションフィールドの値を式中で使うには、同じ条件でガードするか ?? でデフォルト値を与える必要があります。

10. 導出フィールド(let

let フィールドは他のフィールドから値を計算します。ワイヤ上には存在せずバイトも消費しませんが、C 構造体のメンバになり、後続の式や制約で参照できます。

wire
# From examples/quic/frames.wspec
0x08..=0x0f => Stream {
    stream_id:  VarInt,
    offset_raw: if frame_type & 0x04 { VarInt },
    length_raw: if frame_type & 0x02 { VarInt },
    data: bytes[length_or_remaining: length_raw],
    let offset: u64 = offset_raw ?? 0,
    let fin:   bool = (frame_type & 0x01) != 0,
},
wire
# From examples/mqtt/mqtt.wspec
let qos: u8 = (type_and_flags & 0x06) >> 1,

bool はセマンティック型で、ワイヤフィールドにはなれませんが let の型として使えます。導出フィールドは先行するワイヤフィールドと let フィールドを参照できます。

C への対応: let フィールドはパース時に値が設定される通常の構造体メンバになります。シリアライズ時は格納値を使わず式を再計算します。

11. 制約(requirestatic_assert

require による実行時制約

require EXPR は実行時チェックです。パース中にこの式が偽なら WIRESPEC_ERR_CONSTRAINT を返します。

wire
# From examples/net/udp.wspec
require length >= 8,

# From examples/quic/frames.wspec
require length <= MAX_CID_LENGTH,

# From examples/net/tcp.wspec
require data_offset >= 5,

# From examples/mqtt/mqtt.wspec — check flag bits
require type_and_flags & 0x0F == 0x02,

require は packet、frame ブランチ、capsule 本体のどこにでも配置可能です。先行するワイヤフィールドや let フィールドを参照できます。

static_assert によるコンパイル時アサーション

static_assert EXPR はコンパイル時チェックです。定数間の関係を保証するのに便利です:

wire
const MAX_CID_LENGTH: u8 = 20
static_assert MAX_CID_LENGTH <= 255

失敗するとコード生成前にコンパイルエラーになります。

12. 配列

個数指定配列

[T; count]T 型の要素をちょうど count 個読み込みます。count は先行する整数フィールドで指定します:

wire
# From examples/quic/frames.wspec
ack_ranges: [AckRange; ack_range_count],

# From examples/tls/tls13.wspec — count derived from byte length / element size
cipher_suites: [u16; cipher_suites_length / 2],

fill 配列

[T; fill] はスコープの残りを消費し、T を読めるだけ読みます:

wire
# A list that fills the rest of the packet
entries: [Entry; fill],

長さ境界付き fill

[T; fill] within EXPR はちょうど EXPR バイトを消費するまで要素を読みます。TLS の拡張リストなどで典型的なパターンです:

wire
# From examples/tls/tls13.wspec
extensions: [Extension; fill] within extensions_length,

# From examples/tls/tls13.wspec
certificate_list: [CertificateEntry; fill] within certificate_list_length,

extensions_length バイトのサブスコープを作り、使い切るまで Extension レコードを読みます。

配列のキャパシティ

C への対応: 配列はスタック上に固定キャパシティで確保されます。デフォルトは wirespec_runtime.hWIRESPEC_MAX_ARRAY_ELEMENTS(64 要素)で、-DWIRESPEC_MAX_ARRAY_ELEMENTS=N でグローバルに変更可能です。

フィールド単位でキャパシティを変えるには @max_len を使います:

wire
packet Foo {
    count: u16,
    items: [Item; count],           # uses default capacity (64)

    @max_len(1024)
    large_items: [Item; count],     # uses capacity 1024
}

ワイヤ上の実際のカウントがキャパシティを超えると WIRESPEC_ERR_CAPACITY が返ります。

生成される構造体:

c
typedef struct {
    uint16_t count;
    item_t items[64];
    item_t large_items[1024];
} foo_t;

13. 型エイリアスと計算型

シンプルなエイリアス

type Name = TypeExpr は型エイリアスです。新しいワイヤレイアウトは生まれず、純粋に名前を付けるだけです。

wire
# From examples/ble/att.wspec
type AttHandle = u16le
type Uuid16    = u16le

計算型(依存レコード)

右辺が { ... } ブロックなら計算型になります。先行フィールドの値によってフィールドの型が決まる依存レコードです。

wire
# From examples/quic/varint.wspec — QUIC Variable-Length Integer
@strict
type VarInt = {
    prefix: bits[2],
    value: match prefix {
        0b00 => bits[6],
        0b01 => bits[14],
        0b10 => bits[30],
        0b11 => bits[62],
    },
}

まず prefix(2 ビット)を読み、その値に応じて value のビット幅が決まります。計算型は通常の型として扱えるので、型が求められる場所ならどこでも使えます。

@strict を付けると、パース時に非正規エンコードを拒否します。たとえば 1 バイトで表現できる値を 2 バイト形式でエンコードした場合に WIRESPEC_ERR_NONCANONICAL を返します。

14. 継続ビット VarInt

各バイトの MSB(または LSB)を継続フラグとし、残りのビットがデータを運ぶ可変長整数方式です。MQTT、Protocol Buffers、LEB128 がこのパターンを使います。

wire
# From examples/mqtt/mqtt.wspec — MQTT Remaining Length
type MqttLength = varint {
    continuation_bit: msb,   # MSB=1 means more bytes follow
    value_bits: 7,           # 7 data bits per byte
    max_bytes: 4,            # maximum encoded length: 4 bytes (268,435,455)
    byte_order: little,      # lower-order groups come first
}

varint キーワードで継続ビット VarInt 型を定義します。パラメータ:

パラメータ説明
continuation_bitmsb または lsb継続フラグのビット位置
value_bits整数バイトあたりのデータビット数
max_bytes整数オーバーフロー前の最大エンコードバイト数
byte_orderlittle または big下位グループが先かどうか

max_bytes バイト読んでもまだ継続ビットがセットされていれば WIRESPEC_ERR_OVERFLOW を返します。

C への対応: 最大値に収まる最小の uintN_t にデコードされます。MqttLength(最大 4 バイト、28 有効ビット)なら uint32_t です。

15. bits[N] と BitGroup パッキング

bits[N] は N ビットを符号なし整数として読みます。bitbits[1] の省略形です。複数の小さなフィールドを 1 バイトや 1 ワードにパックするヘッダで使います。

wire
# From examples/ip/ipv4.wspec
packet IPv4Header {
    version:         bits[4],
    ihl:             bits[4],
    dscp:            bits[6],
    ecn:             bits[2],
    total_length:    u16,
    identification:  u16,
    flags:           bits[3],
    fragment_offset: bits[13],
    ttl:             u8,
    protocol:        u8,
    ...
}
wire
# From examples/net/tcp.wspec
packet TcpSegment {
    ...
    data_offset: bits[4],
    reserved:    bits[4],
    cwr: bit,
    ece: bit,
    urg: bit,
    ack: bit,
    psh: bit,
    rst: bit,
    syn: bit,
    fin: bit,
    window: u16,
    ...
}

BitGroup 自動グループ化

連続する bits[N] / bit フィールドは自動的にグループ化され、1 回の読み込みになります。コンパイラは:

  1. ビット幅の合計から読み込むバイト数を決定(整数バイトでなければならない)
  2. そのバイトを 1 回で読む
  3. シフト+マスクで各フィールドを抽出

ビッグエンディアンでは最初に宣言したフィールドが MSB 側、リトルエンディアン(@endian little)では LSB 側に配置されます。

C への対応: 各フィールドは収まる最小の型(uint8_tuint16_tuint32_t)になります。パース関数のコード例:

c
uint8_t _byte0 = buf[pos];
out->version = (_byte0 >> 4) & 0x0f;
out->ihl     = (_byte0 >> 0) & 0x0f;

ビットグループの合計がバイト境界に揃っていない場合はコンパイルエラーになります。

16. @checksum アノテーション

@checksum を付けたフィールドは、パース時に自動検証、シリアライズ時に自動計算されます。

wire
# From examples/ip/ipv4.wspec
packet IPv4Header {
    version: bits[4],
    ihl:     bits[4],
    ...
    @checksum(internet)
    header_checksum: u16,
    src_addr: u32,
    dst_addr: u32,
}

パース時: 構造体全体のバイトに対してチェックサムを計算・検証します。不一致なら WIRESPEC_ERR_CHECKSUM を返します。

シリアライズ時: チェックサムフィールドをゼロクリアしてからシリアライズし、計算結果を書き戻します。

対応アルゴリズム

アルゴリズムフィールド型規格
internetu16RFC 1071 1 の補数和(IPv4、UDP、TCP)
crc32u32IEEE 802.3 CRC-32
crc32cu32CRC-32C(Castagnoli)、SCTP、iSCSI で使用
fletcher16u16RFC 1146 Fletcher-16
wire
# From examples/checksum/crc32_test.wspec
packet Crc32Packet {
    id:     u16,
    length: u16,
    require length >= 8,
    data: bytes[length: length - 8],
    @checksum(crc32)
    checksum: u32,
}

1 つの packet または frame ブランチに付けられる @checksum は最大 1 つです。フィールド型はアルゴリズムが要求する型と一致しなければなりません。

まとめ

ここまでの機能をほぼ網羅した実例として、examples/quic/frames.wspec の QUIC フレーム定義を示します:

wire
module quic.frames
@endian big

import quic.varint.VarInt

const MAX_CID_LENGTH: u8 = 20

packet AckRange  { gap: VarInt, ack_range: VarInt }
packet EcnCounts { ect0: VarInt, ect1: VarInt, ecn_ce: VarInt }

packet LengthPrefixedCid {
    length: u8,
    value: bytes[length],
    require length <= MAX_CID_LENGTH,
}

frame QuicFrame = match frame_type: VarInt {
    0x00 => Padding {},
    0x01 => Ping {},

    0x02..=0x03 => Ack {
        largest_ack:     VarInt,
        ack_delay:       VarInt,
        ack_range_count: VarInt,
        first_ack_range: VarInt,
        ack_ranges: [AckRange; ack_range_count],
        ecn_counts: if frame_type == 0x03 { EcnCounts },
    },

    0x06 => Crypto {
        offset: VarInt,
        length: VarInt,
        data:   bytes[length],
    },

    0x08..=0x0f => Stream {
        stream_id:  VarInt,
        offset_raw: if frame_type & 0x04 { VarInt },
        length_raw: if frame_type & 0x02 { VarInt },
        data:       bytes[length_or_remaining: length_raw],
        let offset: u64  = offset_raw ?? 0,
        let fin:   bool  = (frame_type & 0x01) != 0,
    },

    0x18 => NewConnectionId {
        sequence:      VarInt,
        retire_prior:  VarInt,
        cid_length:    u8,
        cid:           bytes[cid_length],
        reset_token:   bytes[16],
    },

    0x1c..=0x1d => ConnectionClose {
        error_code:          VarInt,
        offending_frame_type: if frame_type == 0x1c { VarInt },
        reason_length:       VarInt,
        reason_phrase:       bytes[reason_length],
    },

    0x30..=0x31 => Datagram {
        length: if frame_type & 0x01 { VarInt },
        data:   bytes[length_or_remaining: length],
    },

    _ => Unknown { data: bytes[remaining] },
}

この定義だけで import、const、packet、frame、パターン範囲、オプションフィールド、導出フィールド、配列、bytes[length]bytes[remaining]bytes[length_or_remaining]、ビットマスク条件、??require をカバーしています。

次のステップ

  • モジュール・インポート — 定義を複数の .wspec ファイルに分割: モジュール
  • ステートマシン — 型付き遷移によるプロトコルセッションロジック: ステートマシン
  • リファレンス — 完全な文法と C API: リファレンス