ビットフィールド
wirespec は bits[N] と bit でサブバイトフィールドを第一級サポートしています。連続するビットフィールドは自動的に 1 回のワイヤ読み取りにグループ化され、コンパイラがフィールドごとのシフト & マスクコードを生成します。手動のビット操作は不要です。
bits[N]
bits[N] は N ビットの符号なし整数フィールドです。N は 1 ~ 32 で、C 構造体では収まる最小の符号なし整数型に格納されます。
| N | C 型 |
|---|---|
| 1--8 | uint8_t |
| 9--16 | uint16_t |
| 17--32 | uint32_t |
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
bit は bits[1] のエイリアスです。プロトコルフラグのような 1 ビットフィールドに自然な記法です。
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)
非ビットフィールド(u8、u16、bytes[...] など)が現れると現在のグループが終了し、次の bits[N] で新しいグループが始まります。
例: 4 ビットずつの 2 ニブル
packet IPv4FirstByte {
version: bits[4], # \
ihl: bits[4], # / -- グループ化: 1 バイト読み取り
# パケットの残り
}生成される 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 ビットフィールド)
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 コード:
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 ビットにまたがる混合幅
packet IPv4FlagsFragment {
flags: bits[3], # \
fragment_offset: bits[13], # / -- グループ化: 2 バイト (uint16_t)
}生成される 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 ビットグループ
packet ThirtyTwoBits {
x: bits[4],
y: bits[12],
z: bits[16],
}合計 32 ビットなので、1 回の uint32_t 読み取りになります。
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 ファーストでパックされます。グループ内の最初のフィールドがワードの最下位ビットを占めます。
@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 コード:
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 つの独立したビットグループがあります。
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 つのグループはそれぞれ独立した読み取りです。
式でのビットフィールドの利用
グループから抽出されたビットフィールドは、式内では通常の整数値として扱えます。require、let、if、bytes[length:] で参照できます。
packet TcpSegment {
# ...
data_offset: bits[4],
# ...
require data_offset >= 5,
options: bytes[length: data_offset * 4 - 20],
}式 data_offset * 4 - 20 はデータオフセットフィールドからオプション長を算出します。data_offset は options のパース前に完全に解決済みです。
ヒント
- グループ境界は自動。 グループを明示的に宣言する必要はありません。
bits[N]フィールドを連続して書けば wirespec がグループ化します。 - 非ビットフィールドがグループを分断。 別々のバイト読み取りが必要な場合は、間に通常のフィールドを挟んでください。
- グループは 32 ビットが上限。 合計 32 ビットを超えるビットフィールドの列はコンパイルエラーです。
bitエイリアスで可読性向上。 ブールフラグにはsyn: bits[1]よりsyn: bitがおすすめです。