Skip to content

QUIC

QUIC は wirespec の機能を幅広く活用する好例です。可変長整数(VarInt)は計算型と bits[N] マッチで実現し、フレームフォーマットではオプションフィールド、導出フィールド、配列、bytes[remaining] など、ほぼすべてのフィールド種類を使います。varint.wspecframes.wspec だけで、言語機能の大部分をカバーできます。

VarInt

QUIC は 2 ビットプレフィックスで 6、14、30、62 ビットの整数をエンコードします。値の大きさに応じて、ワイヤ上では 1、2、4、8 バイトのいずれかを占有します。

wire
module quic.varint

@strict
type VarInt = {
    prefix: bits[2],
    value: match prefix {
        0b00 => bits[6],
        0b01 => bits[14],
        0b10 => bits[30],
        0b11 => bits[62],
    },
}

使われている機能:

機能該当箇所
計算型波括弧付きの type VarInt = { ... }
bits[N] サブバイトフィールドprefix: bits[2]
フィールドに対する matchmatch prefix { 0b00 => ... }
2 進数リテラル0b00, 0b01, 0b10, 0b11
@strict アノテーション非正規エンコーディングをパース時に拒否

動作の仕組み。 prefix は最初のバイトの上位 2 ビットです。match でプレフィックス値に応じた後続ビット数を決定します。0b00 なら同一バイト内の 6 ビット(計 1 バイト)、0b11 なら 7 バイトにまたがる 62 ビット(計 8 バイト)です。

@strict と正規エンコーディング。 RFC 9000 は VarInt を最短エンコーディングで表現することを要求しています。@strict を付けると、より少ないバイトで表現可能な値を検出した場合に WIRESPEC_ERR_NONCANONICAL を返します。@strict がなければ非正規エンコーディングも受け入れます。

コンパイル:

bash
wirespec compile examples/quic/varint.wspec -o build/

生成される C API:

c
wirespec_result_t quic_varint_var_int_parse(
    const uint8_t *buf, size_t len,
    quic_varint_var_int_t *out, size_t *consumed);

wirespec_result_t quic_varint_var_int_serialize(
    const quic_varint_var_int_t *in,
    uint8_t *buf, size_t cap, size_t *written);

生成される構造体は prefixvalue の両フィールドを保持します。シリアライザは値の大きさからプレフィックスを自動的に再構築します。

RFC 9000 付録 A テストベクタ。 wirespec の VarInt テストスイートには RFC 9000 付録 A の全ベクタが含まれており、各エンコーディング幅と境界値を網羅しています。


QUIC フレーム

QUIC フレームは VarInt タグで 10 種類以上のフレームタイプにディスパッチします。各フレームタイプには固有のフィールドレイアウトがあり、多くがオプションフィールド、導出フィールド、長さ区切りのバイト配列を使います。

wire
module quic.frames
@endian big
import quic.varint.VarInt

const MAX_CID_LENGTH: u8 = 20

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: 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: reason_length],
    },

    0x1e => HandshakeDone {},

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

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

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

使われている機能:

機能該当箇所
frame タグ付きユニオンframe QuicFrame = match frame_type: VarInt { ... }
インポートimport quic.varint.VarInt
constconst MAX_CID_LENGTH: u8 = 20
パターン範囲0x02..=0x03, 0x08..=0x0f, 0x1c..=0x1d, 0x30..=0x31
空フレームPadding {}, Ping {}, HandshakeDone {}
配列[AckRange; ack_range_count]
オプションフィールドif COND { T }
ビット条件if frame_type & 0x04 { VarInt }
bytes[length: EXPR]data: bytes[length: length]
bytes[length_or_remaining: EXPR]data: bytes[length_or_remaining: length_raw]
bytes[remaining]Unknown { data: bytes[remaining] }
固定長バイト列reset_token: bytes[16]
導出フィールド(letlet offset: u64 = offset_raw ?? 0
コアレスク演算子(??offset_raw ?? 0
require + constrequire length <= MAX_CID_LENGTH
ワイルドカードブランチ_ => Unknown { ... }

パターン範囲

1 つの frame ブランチで ..=(閉区間)を使い、複数のタグ値をまとめて扱えます。

wire
0x02..=0x03 => Ack { ... }

これは 0x02(ECN なし ACK)と 0x03(ECN あり ACK)の両方にマッチします。ブランチ内では frame_type が実際の値を保持しているため、frame_type == 0x03 で ECN フィールドの有無を判定できます。

オプションフィールド

if COND { T } でフィールドの存在を条件付けます。条件はパース時に評価され、true ならフィールドをパース、false ならスキップします。

wire
ecn_counts: if frame_type == 0x03 { EcnCounts },

C では、オプションフィールドはフラグとのペアになります。

c
bool has_ecn_counts;
quic_frames_ecn_counts_t ecn_counts;

ビット条件も同様です。

wire
offset_raw: if frame_type & 0x04 { VarInt },

Stream フレームタイプの O ビット(0x04)はオフセットの存在を示します。ビットが立っていなければ has_offset_raw は false になり、フィールドはスキップされます。

bytes[length_or_remaining: EXPR]

Stream フレームと Datagram フレームで使われる特殊な bytes spec です。式の型は Option[VarInt] で、L ビット(0x02)が立っていれば値が存在し、なければ null です。

wire
data: bytes[length_or_remaining: length_raw],
  • length_raw が存在する場合: そのバイト数だけ読み取ります。
  • length_raw が null の場合: 現在のスコープの残り全体を消費します。

RFC 9000 の仕様をそのままモデル化しています。L ビットがクリアな Stream フレームは QUIC パケットの末尾まで延びます。

導出フィールド

let フィールドはワイヤフィールドから計算されますが、ワイヤ上には存在しません。

wire
let offset: u64 = offset_raw ?? 0,
let fin: bool = (frame_type & 0x01) != 0,

?? コアレスク演算子はオプションをアンラップします。offset_raw ?? 0 は、値があれば offset_raw を、なければ 0 を返します。下流のコードで has_offset_raw のチェックを省き、常に u64 として扱えます。

let フィールドは C 構造体にもワイヤフィールドと並んで格納されます。

c
uint64_t offset;  /* derived */
bool fin;         /* derived */

マルチモジュールコンパイル

frames.wspecvarint.wspec から VarInt をインポートしています。両方をまとめてコンパイルするには:

bash
wirespec compile examples/quic/frames.wspec \
    -I examples/ \
    -o build/

-I examples/ でリゾルバに quic/varint.wspec の探索パスを指定します。コンパイラがトポロジカルソートを行い、quic_varint.hquic_frames.h より先に生成してインクルード順を保証します。

ディレクトリ内の全ファイルを一括コンパイルすることもできます。

bash
wirespec compile --recursive examples/quic/ -o build/

生成される C API:

c
wirespec_result_t quic_frames_quic_frame_parse(
    const uint8_t *buf, size_t len,
    quic_frames_quic_frame_t *out, size_t *consumed);

wirespec_result_t quic_frames_quic_frame_serialize(
    const quic_frames_quic_frame_t *in,
    uint8_t *buf, size_t cap, size_t *written);

生成されるユニオンは frame_type で識別します。

c
typedef struct {
    quic_varint_var_int_t frame_type;
    union {
        quic_frames_ack_t          ack;
        quic_frames_crypto_t       crypto;
        quic_frames_stream_t       stream;
        quic_frames_datagram_t     datagram;
        quic_frames_unknown_t      unknown;
        /* ... */
    };
} quic_frames_quic_frame_t;

次のステップ

  • クラシックプロトコル -- UDP、TCP、Ethernet、IPv4 の基本
  • BLE -- リトルエンディアン、型エイリアス、列挙型、ATT プロトコル
  • MQTT -- 継続ビット VarInt、TLV カプセル、式ベースのディスパッチ
  • TLS 1.3 -- 列挙型タグ、u24、fill-within 配列