QUIC
QUIC は wirespec の機能を幅広く活用する好例です。可変長整数(VarInt)は計算型と bits[N] マッチで実現し、フレームフォーマットではオプションフィールド、導出フィールド、配列、bytes[remaining] など、ほぼすべてのフィールド種類を使います。varint.wspec と frames.wspec だけで、言語機能の大部分をカバーできます。
VarInt
QUIC は 2 ビットプレフィックスで 6、14、30、62 ビットの整数をエンコードします。値の大きさに応じて、ワイヤ上では 1、2、4、8 バイトのいずれかを占有します。
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] |
フィールドに対する match | match 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 がなければ非正規エンコーディングも受け入れます。
コンパイル:
wirespec compile examples/quic/varint.wspec -o build/生成される C API:
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);生成される構造体は prefix と value の両フィールドを保持します。シリアライザは値の大きさからプレフィックスを自動的に再構築します。
RFC 9000 付録 A テストベクタ。 wirespec の VarInt テストスイートには RFC 9000 付録 A の全ベクタが含まれており、各エンコーディング幅と境界値を網羅しています。
QUIC フレーム
QUIC フレームは VarInt タグで 10 種類以上のフレームタイプにディスパッチします。各フレームタイプには固有のフィールドレイアウトがあり、多くがオプションフィールド、導出フィールド、長さ区切りのバイト配列を使います。
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 |
const | const 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] |
導出フィールド(let) | let offset: u64 = offset_raw ?? 0 |
コアレスク演算子(??) | offset_raw ?? 0 |
require + const | require length <= MAX_CID_LENGTH |
| ワイルドカードブランチ | _ => Unknown { ... } |
パターン範囲
1 つの frame ブランチで ..=(閉区間)を使い、複数のタグ値をまとめて扱えます。
0x02..=0x03 => Ack { ... }これは 0x02(ECN なし ACK)と 0x03(ECN あり ACK)の両方にマッチします。ブランチ内では frame_type が実際の値を保持しているため、frame_type == 0x03 で ECN フィールドの有無を判定できます。
オプションフィールド
if COND { T } でフィールドの存在を条件付けます。条件はパース時に評価され、true ならフィールドをパース、false ならスキップします。
ecn_counts: if frame_type == 0x03 { EcnCounts },C では、オプションフィールドはフラグとのペアになります。
bool has_ecn_counts;
quic_frames_ecn_counts_t ecn_counts;ビット条件も同様です。
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 です。
data: bytes[length_or_remaining: length_raw],length_rawが存在する場合: そのバイト数だけ読み取ります。length_rawが null の場合: 現在のスコープの残り全体を消費します。
RFC 9000 の仕様をそのままモデル化しています。L ビットがクリアな Stream フレームは QUIC パケットの末尾まで延びます。
導出フィールド
let フィールドはワイヤフィールドから計算されますが、ワイヤ上には存在しません。
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 構造体にもワイヤフィールドと並んで格納されます。
uint64_t offset; /* derived */
bool fin; /* derived */マルチモジュールコンパイル
frames.wspec は varint.wspec から VarInt をインポートしています。両方をまとめてコンパイルするには:
wirespec compile examples/quic/frames.wspec \
-I examples/ \
-o build/-I examples/ でリゾルバに quic/varint.wspec の探索パスを指定します。コンパイラがトポロジカルソートを行い、quic_varint.h を quic_frames.h より先に生成してインクルード順を保証します。
ディレクトリ内の全ファイルを一括コンパイルすることもできます。
wirespec compile --recursive examples/quic/ -o build/生成される C API:
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 で識別します。
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 配列