Classic Protocols
UDP, TCP, Ethernet, and IPv4 are the foundation of internet networking. They are also excellent wirespec examples: small, well-understood, and each one demonstrates a distinct set of language features. Start here if you are new to wirespec.
UDP
User Datagram Protocol has the simplest possible fixed header: four 16-bit fields, a length constraint, and a variable-length payload whose size is derived from the header.
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],
}Features demonstrated:
| Feature | Where |
|---|---|
packet definition | packet UdpDatagram { ... } |
| Big-endian module default | @endian big |
u16 scalar field | src_port, dst_port, length, checksum |
| Runtime validation | require length >= 8 |
| Length-prefixed bytes | bytes[length: length - 8] |
| Expression arithmetic | length - 8 in the bytes spec |
The require clause is checked at parse time and returns WIRESPEC_ERR_CONSTRAINT if the condition fails. The expression length - 8 subtracts the fixed header size (8 bytes) from the length field to give the payload byte count.
Compile:
wirespec compile examples/net/udp.wspec -o build/Generated C API:
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);The data field becomes a wirespec_bytes_t (pointer + length) — zero-copy, pointing into the original buffer.
TCP
TCP has a more complex header than UDP. The 4-bit data offset field, eight individual flag bits, and a variable-length options region show off wirespec's bits[N] and bit types.
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],
}Features demonstrated:
| Feature | Where |
|---|---|
bits[N] sub-byte fields | data_offset: bits[4], reserved: bits[4] |
bit (single-bit flag) | cwr, ece, urg, ack, psh, rst, syn, fin |
| BitGroup auto-grouping | The 8 flag bits are packed into one byte on the wire |
u32 scalar | seq_num, ack_num |
| Computed length bytes | bytes[length: data_offset * 4 - 20] |
BitGroup auto-grouping. Consecutive bits[N] and bit fields that together fill complete bytes are automatically grouped by the compiler. In the TCP definition, data_offset: bits[4] and reserved: bits[4] are grouped into one byte. The eight flag bits are grouped into a second byte. The compiler emits a single read for each group and uses shifts and masks to extract individual fields — no manual bit manipulation needed.
The data offset field encodes the header length in 32-bit words. The minimum value is 5 (20 bytes). The options field occupies data_offset * 4 - 20 bytes, which is zero when there are no options.
Compile:
wirespec compile examples/net/tcp.wspec -o build/Generated C struct (excerpt):
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
An Ethernet frame has two fixed-length MAC address fields (6 bytes each) and a payload whose size extends to the end of the input buffer. This demonstrates wirespec's zero-copy bytes[remaining].
module net.ethernet
@endian big
packet EthernetFrame {
dst_mac: bytes[6],
src_mac: bytes[6],
ether_type: u16,
payload: bytes[remaining],
}Features demonstrated:
| Feature | Where |
|---|---|
| Fixed-length bytes | bytes[6] — exact byte count, zero-copy |
bytes[remaining] | payload consumes all bytes left in the buffer |
| Simple flat packet | No conditionals or expressions needed |
bytes[remaining] is a terminal field: it must be the last wire field in its scope. The compiler enforces this statically and will reject any definition that places wire fields after it.
Both dst_mac, src_mac, and payload map to wirespec_bytes_t in the generated C — a { const uint8_t *ptr; size_t len; } pair pointing into the original buffer. No copying occurs.
Compile:
wirespec compile examples/net/ethernet.wspec -o build/IPv4
IPv4 uses bits[N] fields of varying widths and adds an @checksum(internet) annotation for automatic RFC 1071 checksum verification and serialization.
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,
}Features demonstrated:
| Feature | Where |
|---|---|
bits[N] with varied widths | 4, 6, 2, 3, 13-bit fields |
| BitGroup auto-grouping | version+ihl → one byte; dscp+ecn → one byte; flags+fragment_offset → two bytes |
@checksum(internet) | Automatic RFC 1071 checksum on header_checksum |
@checksum(internet) behavior:
- On parse: The generated parser computes the one's complement sum over the entire header. If the result is not 0xFFFF, it returns
WIRESPEC_ERR_CHECKSUM. - On serialize: The generated serializer zeros the checksum field, computes the checksum over the completed header, and writes the result back. You never touch the checksum field manually.
This matches the behavior specified in RFC 791 and verified against real IPv4 packets.
Compile:
wirespec compile examples/ip/ipv4.wspec -o build/Parsing with checksum verification:
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) {
/* header checksum failed — drop packet */
}Serializing with automatic checksum:
ip_v4_ipv4_header_t hdr = {
.version = 4,
.ihl = 5,
.protocol = 17, /* UDP */
/* ... other fields ... */
};
uint8_t buf[20];
size_t written;
ip_v4_ipv4_header_serialize(&hdr, buf, sizeof(buf), &written);
/* buf now contains a valid IPv4 header with correct checksum */What Next
- QUIC — variable-length integers, tagged frame unions, optional fields
- BLE — little-endian protocols, type aliases, enums
- Checksums — CRC32, CRC32C, Fletcher-16
- Bit Fields — deeper dive into
bits[N]and BitGroup packing