Skip to content

クラシックプロトコル

UDP、TCP、Ethernet、IPv4 はインターネットの基盤となるプロトコルです。いずれも小さくて仕様が明確であり、wirespec の機能を端的に示す良い題材でもあります。wirespec を初めて使うなら、まずここから読むのがおすすめです。

UDP

User Datagram Protocol は最もシンプルな固定ヘッダを持ちます。4 つの 16 ビットフィールド、長さの制約、そしてヘッダ値から算出されるサイズの可変長ペイロードで構成されます。

wire
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],
}

使われている機能:

機能該当箇所
packet 定義packet UdpDatagram { ... }
ビッグエンディアンのモジュールデフォルト@endian big
u16 スカラーフィールドsrc_port, dst_port, length, checksum
実行時バリデーションrequire length >= 8
長さプレフィックス付きバイト列bytes[length: length - 8]
式中の算術演算bytes spec 内の length - 8

require 節はパース時に評価され、条件を満たさなければ WIRESPEC_ERR_CONSTRAINT を返します。式 length - 8 は、length フィールドから固定ヘッダサイズ(8 バイト)を差し引いてペイロードのバイト数を求めます。

コンパイル:

bash
wirespec compile examples/net/udp.wspec -o build/

生成される C API:

c
wirespec_result_t net_udp_udp_datagram_parse(
    const uint8_t *buf, size_t len,
    net_udp_udp_datagram_t *out, size_t *consumed);

wirespec_result_t net_udp_udp_datagram_serialize(
    const net_udp_udp_datagram_t *in,
    uint8_t *buf, size_t cap, size_t *written);

data フィールドは wirespec_bytes_t(ポインタ + 長さ)になり、元のバッファをゼロコピーで参照します。


TCP

TCP は UDP より複雑なヘッダ構造を持ちます。4 ビットのデータオフセット、8 個の個別フラグビット、可変長のオプション領域があり、wirespec の bits[N] 型と bit 型が活躍します。

wire
module net.tcp
@endian big

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

使われている機能:

機能該当箇所
bits[N] サブバイトフィールドdata_offset: bits[4], reserved: bits[4]
bit(1 ビットフラグ)cwr, ece, urg, ack, psh, rst, syn, fin
ビットグループの自動グループ化8 個のフラグビットがワイヤ上で 1 バイトにパック
u32 スカラーseq_num, ack_num
計算されたバイト長bytes[length: data_offset * 4 - 20]

ビットグループの自動グループ化。 バイト境界に揃う連続した bits[N]/bit フィールドは、コンパイラが自動的にグループ化します。TCP の定義では、data_offset: bits[4]reserved: bits[4] で 1 バイト、8 個のフラグで 1 バイトにまとまります。コンパイラはグループごとに 1 回の読み取りを生成し、シフト & マスクで個別フィールドを抽出します。手動のビット操作は不要です。

データオフセットフィールドはヘッダ長を 32 ビットワード単位でエンコードしており、最小値は 5(= 20 バイト)です。オプション領域は data_offset * 4 - 20 バイトを占め、オプションがなければゼロバイトになります。

コンパイル:

bash
wirespec compile examples/net/tcp.wspec -o build/

生成される C 構造体(抜粋):

c
typedef struct {
    uint16_t src_port;
    uint16_t dst_port;
    uint32_t seq_num;
    uint32_t ack_num;
    uint8_t  data_offset;   /* bits[4] */
    uint8_t  reserved;      /* bits[4] */
    uint8_t  cwr;           /* bit */
    uint8_t  ece;
    uint8_t  urg;
    uint8_t  ack;
    uint8_t  psh;
    uint8_t  rst;
    uint8_t  syn;
    uint8_t  fin;
    uint16_t window;
    uint16_t checksum;
    uint16_t urgent_pointer;
    wirespec_bytes_t options;
} net_tcp_segment_t;

Ethernet

Ethernet フレームは 6 バイト固定長の MAC アドレスフィールドが 2 つと、入力バッファの末尾まで広がるペイロードで構成されます。ゼロコピーの bytes[remaining] を使う好例です。

wire
module net.ethernet
@endian big

packet EthernetFrame {
    dst_mac: bytes[6],
    src_mac: bytes[6],
    ether_type: u16,
    payload: bytes[remaining],
}

使われている機能:

機能該当箇所
固定長バイト列bytes[6] -- 指定バイト数をゼロコピーで参照
bytes[remaining]payload がバッファの残り全体を消費
シンプルなフラットパケット条件分岐や複雑な式は不要

bytes[remaining] はターミナルフィールドです。スコープ内の最後のワイヤフィールドでなければならず、コンパイラがこれを静的に検証します。後続にワイヤフィールドがあるとコンパイルエラーになります。

dst_macsrc_macpayload はいずれも C では wirespec_bytes_t{ const uint8_t *ptr; size_t len; })にマップされ、元のバッファを直接指します。コピーは発生しません。

コンパイル:

bash
wirespec compile examples/net/ethernet.wspec -o build/

IPv4

IPv4 はさまざまなビット幅の bits[N] フィールドを使い、@checksum(internet) アノテーションで RFC 1071 チェックサムの自動検証・自動計算を実現します。

wire
module ip.v4
@endian big

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,
    @checksum(internet)
    header_checksum: u16,
    src_addr: u32,
    dst_addr: u32,
}

使われている機能:

機能該当箇所
各種ビット幅の bits[N]4, 6, 2, 3, 13 ビットフィールド
ビットグループの自動グループ化version+ihl -> 1 バイト、dscp+ecn -> 1 バイト、flags+fragment_offset -> 2 バイト
@checksum(internet)header_checksum に RFC 1071 チェックサムを自動適用

@checksum(internet) の動作:

  • パース時: ヘッダ全体の 1 の補数和を計算し、結果が 0xFFFF でなければ WIRESPEC_ERR_CHECKSUM を返します。
  • シリアライズ時: チェックサムフィールドをゼロクリアしてからヘッダ全体のチェックサムを計算し、結果を書き戻します。手動でのフィールド設定は不要です。

RFC 791 の規定に準拠しており、実際の IPv4 パケットで検証済みです。

コンパイル:

bash
wirespec compile examples/ip/ipv4.wspec -o build/

チェックサム検証付きパース:

c
ip_v4_ipv4_header_t hdr;
size_t consumed;
wirespec_result_t r = ip_v4_ipv4_header_parse(buf, len, &hdr, &consumed);
if (r == WIRESPEC_ERR_CHECKSUM) {
    /* ヘッダチェックサム不正 -- パケット破棄 */
}

自動チェックサム付きシリアライズ:

c
ip_v4_ipv4_header_t hdr = {
    .version  = 4,
    .ihl      = 5,
    .protocol = 17,  /* UDP */
    /* ... 他のフィールド ... */
};
uint8_t buf[20];
size_t written;
ip_v4_ipv4_header_serialize(&hdr, buf, sizeof(buf), &written);
/* buf には正しいチェックサムが設定された有効な IPv4 ヘッダが格納される */

次のステップ

  • QUIC -- 可変長整数、タグ付きフレームユニオン、オプションフィールド
  • BLE -- リトルエンディアン、型エイリアス、列挙型
  • チェックサム -- CRC32、CRC32C、Fletcher-16
  • ビットフィールド -- bits[N] とビットグループパッキングの詳細