Bitfields
wirespec provides first-class support for sub-byte fields through bits[N] and bit. Consecutive bit fields are automatically grouped into a single wire read, and the compiler generates shift+mask extraction code for each field. No manual bit manipulation is required.
bits[N]
bits[N] declares an N-bit unsigned integer field. N can range from 1 to 32. The field is stored in the generated C struct as the smallest unsigned integer type that fits:
| N | C type |
|---|---|
| 1–8 | uint8_t |
| 9–16 | uint16_t |
| 17–32 | uint32_t |
packet IPv4Header {
version: bits[4], # 4-bit unsigned, stored as uint8_t
ihl: bits[4], # 4-bit unsigned, stored as uint8_t
dscp: bits[6], # 6-bit unsigned, stored as uint8_t
ecn: bits[2], # 2-bit unsigned, stored as uint8_t
total_length: u16,
# ...
}bit
bit is an alias for bits[1]. It is the natural type for boolean protocol flags:
packet TcpSegment {
# ...
cwr: bit,
ece: bit,
urg: bit,
ack: bit,
psh: bit,
rst: bit,
syn: bit,
fin: bit,
# ...
}In the generated C struct, bit fields become uint8_t (value is 0 or 1 after masking).
BitGroup Auto-Grouping
When wirespec encounters a run of consecutive bits[N] or bit fields, it automatically groups them into a single wire read. The total width of the group determines how many bytes are read:
- Total ≤ 8 bits → read 1 byte (
uint8_t) - Total ≤ 16 bits → read 2 bytes (
uint16_t) - Total ≤ 32 bits → read 4 bytes (
uint32_t)
A non-bit field (any u8, u16, bytes[...], etc.) breaks the current group and starts a new one on the next bits[N] field encountered.
Example: two 4-bit nibbles
packet IPv4FirstByte {
version: bits[4], # \
ihl: bits[4], # / — grouped: 1 byte read
# rest of packet
}Generated C (parse):
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;Example: TCP flags (8 single-bit fields)
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], # / — group 0: 1 byte
cwr: bit, # \
ece: bit, # |
urg: bit, # |
ack: bit, # | — group 1: 1 byte
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],
}The data_offset / reserved run (8 bits total) becomes one byte read. The eight flag fields (8 bits total) become a second byte read. Generated C for the flags group:
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;Example: mixed widths across 16 bits
packet IPv4FlagsFragment {
flags: bits[3], # \
fragment_offset: bits[13], # / — grouped: 2 bytes (uint16_t)
}Generated 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;Example: 32-bit group
packet ThirtyTwoBits {
x: bits[4],
y: bits[12],
z: bits[16],
}Total is 32 bits, so the group uses a single uint32_t read:
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;Big-Endian Packing (default)
With @endian big (the default), fields are packed MSB-first. The first field in the group occupies the most significant bits of the group word.
For a one-byte group containing version: bits[4], ihl: bits[4]:
Bit: 7 6 5 4 3 2 1 0
[ version ][ ihl ]The first declared field (version) sits at the top (MSB) of the byte. Extraction uses a right-shift equal to the number of bits remaining to the right of that field.
Little-Endian Packing
With @endian little, fields are packed LSB-first. The first field in the group occupies the least significant bits of the group word.
@endian little
packet LeBits {
flag_a: bits[3], # bits [2:0] of the wire byte
flag_b: bits[5], # bits [7:3] of the wire byte
}Bit: 7 6 5 4 3 2 1 0
[ flag_b ][ fa ]Generated C for LE packing:
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; // bits [2:0]
out->flag_b = (_bitgroup_0 >> 3) & 0x1F; // bits [7:3]The shift direction reverses: the first-declared field has the smallest shift (sits at LSB), and subsequent fields shift upward.
Full IPv4 Header Example
The complete IPv4 header from examples/ip/ipv4.wspec uses three separate bit groups interleaved with regular fields:
module ip.v4
@endian big
packet IPv4Header {
version: bits[4], # \
ihl: bits[4], # / — group 0: 1 byte
dscp: bits[6], # \
ecn: bits[2], # / — group 1: 1 byte
total_length: u16, # — regular field, not grouped
identification: u16, # — regular field
flags: bits[3], # \
fragment_offset: bits[13], # / — group 2: 2 bytes (uint16_t)
ttl: u8, # — regular field
protocol: u8, # — regular field
@checksum(internet)
header_checksum: u16, # — regular field
src_addr: u32, # — regular field
dst_addr: u32, # — regular field
}Each regular u8/u16/u32 field between the bit-field runs breaks the current group. The three groups are independent reads.
Using Bit Fields in Expressions
Bit fields extracted from groups are ordinary integer values in expressions. You can use them in require, let, if, and bytes[length:]:
packet TcpSegment {
# ...
data_offset: bits[4],
# ...
require data_offset >= 5,
options: bytes[length: data_offset * 4 - 20],
}The data_offset * 4 - 20 expression computes the options length from the data offset field, which is fully resolved before the options field is parsed.
Tips
- Group boundaries are automatic. You never declare groups explicitly — just write the
bits[N]fields consecutively and wirespec groups them. - Non-bit fields break groups. If you need two separate byte reads, place a regular field between them.
- Groups must not exceed 32 bits. A run of bit fields totalling more than 32 bits is a compile error.
- The
bitalias improves readability. Prefersyn: bitoversyn: bits[1]for boolean flags.