Skip to content

ビットフィールド

wirespec は bits[N]bit でサブバイトフィールドを第一級サポートしています。連続するビットフィールドは自動的に 1 回のワイヤ読み取りにグループ化され、コンパイラがフィールドごとのシフト & マスクコードを生成します。手動のビット操作は不要です。

bits[N]

bits[N] は N ビットの符号なし整数フィールドです。N は 1 ~ 32 で、C 構造体では収まる最小の符号なし整数型に格納されます。

NC 型
1--8uint8_t
9--16uint16_t
17--32uint32_t
wire
packet IPv4Header {
    version: bits[4],    # 4 ビット、uint8_t に格納
    ihl: bits[4],        # 4 ビット、uint8_t に格納
    dscp: bits[6],       # 6 ビット、uint8_t に格納
    ecn: bits[2],        # 2 ビット、uint8_t に格納
    total_length: u16,
    # ...
}

bit

bitbits[1] のエイリアスです。プロトコルフラグのような 1 ビットフィールドに自然な記法です。

wire
packet TcpSegment {
    # ...
    cwr: bit,
    ece: bit,
    urg: bit,
    ack: bit,
    psh: bit,
    rst: bit,
    syn: bit,
    fin: bit,
    # ...
}

C では bit フィールドは uint8_t になり、マスク後の値は 0 か 1 です。

ビットグループの自動グループ化

連続する bits[N]/bit フィールドは、wirespec が自動的に 1 回のワイヤ読み取りにまとめます。グループの合計ビット幅で読み取りバイト数が決まります。

  • 合計 8 ビット以下 -> 1 バイト(uint8_t
  • 合計 16 ビット以下 -> 2 バイト(uint16_t
  • 合計 32 ビット以下 -> 4 バイト(uint32_t

非ビットフィールド(u8u16bytes[...] など)が現れると現在のグループが終了し、次の bits[N] で新しいグループが始まります。

例: 4 ビットずつの 2 ニブル

wire
packet IPv4FirstByte {
    version: bits[4],   # \
    ihl: bits[4],       # / -- グループ化: 1 バイト読み取り
    # パケットの残り
}

生成される C(パース):

c
uint8_t _bitgroup_0;
if (ws_cursor_read_u8(cur, &_bitgroup_0) != WIRESPEC_OK) return WIRESPEC_ERR_SHORT_BUFFER;
out->version = (_bitgroup_0 >> 4) & 0x0F;
out->ihl     = (_bitgroup_0     ) & 0x0F;

例: TCP フラグ(8 個の 1 ビットフィールド)

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],   # / -- グループ 0: 1 バイト
    cwr: bit,               # \
    ece: bit,               #  |
    urg: bit,               #  |
    ack: bit,               #  | -- グループ 1: 1 バイト
    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],
}

data_offset/reserved(計 8 ビット)が 1 バイト読み取り、8 個のフラグ(計 8 ビット)がもう 1 バイト読み取りです。フラググループの C コード:

c
uint8_t _bitgroup_1;
if (ws_cursor_read_u8(cur, &_bitgroup_1) != WIRESPEC_OK) return WIRESPEC_ERR_SHORT_BUFFER;
out->cwr = (_bitgroup_1 >> 7) & 0x01;
out->ece = (_bitgroup_1 >> 6) & 0x01;
out->urg = (_bitgroup_1 >> 5) & 0x01;
out->ack = (_bitgroup_1 >> 4) & 0x01;
out->psh = (_bitgroup_1 >> 3) & 0x01;
out->rst = (_bitgroup_1 >> 2) & 0x01;
out->syn = (_bitgroup_1 >> 1) & 0x01;
out->fin = (_bitgroup_1     ) & 0x01;

例: 16 ビットにまたがる混合幅

wire
packet IPv4FlagsFragment {
    flags:           bits[3],    # \
    fragment_offset: bits[13],   # / -- グループ化: 2 バイト (uint16_t)
}

生成される C:

c
uint16_t _bitgroup_0;
if (ws_cursor_read_u16(cur, &_bitgroup_0) != WIRESPEC_OK) return WIRESPEC_ERR_SHORT_BUFFER;
out->flags           = (_bitgroup_0 >> 13) & 0x0007;
out->fragment_offset = (_bitgroup_0      ) & 0x1FFF;

例: 32 ビットグループ

wire
packet ThirtyTwoBits {
    x: bits[4],
    y: bits[12],
    z: bits[16],
}

合計 32 ビットなので、1 回の uint32_t 読み取りになります。

c
uint32_t _bitgroup_0;
if (ws_cursor_read_u32(cur, &_bitgroup_0) != WIRESPEC_OK) return WIRESPEC_ERR_SHORT_BUFFER;
out->x = (_bitgroup_0 >> 28) & 0x0000000F;
out->y = (_bitgroup_0 >> 16) & 0x00000FFF;
out->z = (_bitgroup_0      ) & 0x0000FFFF;

ビッグエンディアンパッキング(デフォルト)

@endian big(デフォルト)では、フィールドは MSB ファーストでパックされます。グループ内の最初のフィールドがワードの最上位ビットを占めます。

version: bits[4], ihl: bits[4] の 1 バイトグループの場合:

ビット:   7  6  5  4  3  2  1  0
          [  version  ][   ihl  ]

最初に宣言した version がバイトの上位側に配置され、抽出時はその右にある残りビット数ぶんの右シフトで取り出します。

リトルエンディアンパッキング

@endian little では、フィールドは LSB ファーストでパックされます。グループ内の最初のフィールドがワードの最下位ビットを占めます。

wire
@endian little

packet LeBits {
    flag_a: bits[3],   # ワイヤバイトのビット [2:0]
    flag_b: bits[5],   # ワイヤバイトのビット [7:3]
}
ビット:   7  6  5  4  3  2  1  0
          [    flag_b    ][ fa ]

LE パッキングの C コード:

c
uint8_t _bitgroup_0;
if (ws_cursor_read_u8(cur, &_bitgroup_0) != WIRESPEC_OK) return WIRESPEC_ERR_SHORT_BUFFER;
out->flag_a = (_bitgroup_0     ) & 0x07;   // ビット [2:0]
out->flag_b = (_bitgroup_0 >> 3) & 0x1F;   // ビット [7:3]

シフト方向が逆になります。最初に宣言したフィールドのシフト量が最小(LSB 側)で、後続フィールドが上位方向にシフトしていきます。

完全な IPv4 ヘッダの例

examples/ip/ipv4.wspec の IPv4 ヘッダには、通常のフィールドで区切られた 3 つの独立したビットグループがあります。

wire
module ip.v4
@endian big

packet IPv4Header {
    version: bits[4],           # \
    ihl: bits[4],               # / -- グループ 0: 1 バイト
    dscp: bits[6],              # \
    ecn: bits[2],               # / -- グループ 1: 1 バイト
    total_length: u16,          # -- 通常フィールド(グループ化なし)
    identification: u16,        # -- 通常フィールド
    flags: bits[3],             # \
    fragment_offset: bits[13],  # / -- グループ 2: 2 バイト (uint16_t)
    ttl: u8,                    # -- 通常フィールド
    protocol: u8,               # -- 通常フィールド
    @checksum(internet)
    header_checksum: u16,       # -- 通常フィールド
    src_addr: u32,              # -- 通常フィールド
    dst_addr: u32,              # -- 通常フィールド
}

ビットフィールドの合間にある u8/u16/u32 フィールドがグループを区切ります。3 つのグループはそれぞれ独立した読み取りです。

式でのビットフィールドの利用

グループから抽出されたビットフィールドは、式内では通常の整数値として扱えます。requireletifbytes[length:] で参照できます。

wire
packet TcpSegment {
    # ...
    data_offset: bits[4],
    # ...
    require data_offset >= 5,
    options: bytes[length: data_offset * 4 - 20],
}

data_offset * 4 - 20 はデータオフセットフィールドからオプション長を算出します。data_offsetoptions のパース前に完全に解決済みです。

ヒント

  • グループ境界は自動。 グループを明示的に宣言する必要はありません。bits[N] フィールドを連続して書けば wirespec がグループ化します。
  • 非ビットフィールドがグループを分断。 別々のバイト読み取りが必要な場合は、間に通常のフィールドを挟んでください。
  • グループは 32 ビットが上限。 合計 32 ビットを超えるビットフィールドの列はコンパイルエラーです。
  • bit エイリアスで可読性向上。 ブールフラグには syn: bits[1] より syn: bit がおすすめです。