MQTT 3.1.1
MQTT is a publish-subscribe messaging protocol widely used in IoT devices. Its wire format is compact: every control packet starts with a 1-byte fixed header followed by a continuation-bit variable-length integer (the "Remaining Length"), and then a type-specific payload. The type is encoded in the upper 4 bits of the first byte — not as a separate field — which makes it an ideal demonstration of wirespec's expression-based capsule tag.
Protocol Background
MQTT 3.1.1 (OASIS standard) defines 14 control packet types. The most important for a client are CONNECT/CONNACK (session establishment), PUBLISH/PUBACK (message delivery), SUBSCRIBE/SUBACK (topic subscription), PINGREQ/PINGRESP (keepalive), and DISCONNECT. Each packet type has a distinct payload structure; some carry optional fields gated on flag bits embedded in the first header byte.
The "Remaining Length" field uses a variable-length encoding where the MSB of each byte is a continuation flag, and the lower 7 bits carry data — allowing lengths up to 268,435,455 in at most 4 bytes.
Complete wirespec Definition
@endian big
module mqtt
# Continuation-bit VarInt — MQTT Remaining Length (MQTT 3.1.1 §2.2.3)
type MqttLength = varint {
continuation_bit: msb, # MSB = 1 means another byte follows
value_bits: 7, # 7 payload bits per byte
max_bytes: 4, # overflow after 4 bytes → WIRESPEC_ERR_OVERFLOW
byte_order: little, # least-significant group first
}
# UTF-8 string: u16be length prefix + data bytes
packet MqttString {
length: u16,
data: bytes[length],
}
# Binary blob: u16be length prefix + data (no UTF-8 validation)
# Used for Will Message (§3.1.3.3) and Password (§3.1.3.5)
packet MqttBytes {
length: u16,
data: bytes[length],
}
# MQTT Control Packet — Fixed Header + Remaining Length + Body
capsule MqttPacket {
type_and_flags: u8,
remaining_length: MqttLength,
payload: match (type_and_flags >> 4) within remaining_length {
1 => Connect {
protocol_name: MqttString,
protocol_level: u8,
connect_flags: u8,
keep_alive: u16,
client_id: MqttString,
will_topic: if connect_flags & 0x04 { MqttString },
will_message: if connect_flags & 0x04 { MqttBytes },
username: if connect_flags & 0x80 { MqttString },
password: if connect_flags & 0x40 { MqttBytes },
},
2 => ConnAck {
ack_flags: u8,
return_code: u8,
},
3 => Publish {
topic: MqttString,
let qos: u8 = (type_and_flags & 0x06) >> 1,
packet_id: if qos > 0 { u16 },
payload: bytes[remaining],
},
4 => PubAck { packet_id: u16 },
8 => Subscribe {
require type_and_flags & 0x0F == 0x02,
packet_id: u16,
subscriptions: bytes[remaining],
},
9 => SubAck { packet_id: u16, return_codes: bytes[remaining] },
12 => PingReq {},
13 => PingResp {},
14 => Disconnect {},
_ => Unknown { data: bytes[remaining] },
},
}Features Demonstrated
| Feature | Where |
|---|---|
| Continuation-bit VarInt | type MqttLength = varint { ... } |
| Nested packet types | MqttString, MqttBytes as field types |
| Expression-based capsule tag | match (type_and_flags >> 4) within ... |
within byte-length scoping | within remaining_length |
Conditional fields (if) | will_topic: if connect_flags & 0x04 { ... } |
let derived field | let qos: u8 = (type_and_flags & 0x06) >> 1 |
| Dependent conditional | packet_id: if qos > 0 { u16 } |
| Runtime validation | require type_and_flags & 0x0F == 0x02 |
bytes[remaining] | payload: bytes[remaining] |
| Empty branch | PingReq {}, PingResp {}, Disconnect {} |
Feature Notes
Continuation-Bit VarInt
type MqttLength = varint {
continuation_bit: msb,
value_bits: 7,
max_bytes: 4,
byte_order: little,
}The varint { } block defines a continuation-bit variable-length integer — distinct from wirespec's prefix-match VarInt used in QUIC. The parameters control the exact encoding:
| Parameter | Value | Meaning |
|---|---|---|
continuation_bit | msb | Bit 7 of each byte signals "more bytes follow" |
value_bits | 7 | 7 bits of payload per byte |
max_bytes | 4 | After 4 bytes without termination → WIRESPEC_ERR_OVERFLOW |
byte_order | little | Least-significant 7-bit group comes first |
A value of 128 encodes as 0x80 0x01 (two bytes). A value of 16,383 encodes as 0xFF 0x7F. Values up to 268,435,455 (= 2²⁸ − 1) fit in 4 bytes.
In generated C, MqttLength is decoded by a helper that reads bytes in a loop until a non-continuation byte is seen, accumulating the 7-bit groups. The result is stored as uint32_t.
Expression-Based Capsule Tag
capsule MqttPacket {
type_and_flags: u8,
remaining_length: MqttLength,
payload: match (type_and_flags >> 4) within remaining_length {
...
},
}The capsule tag is not a bare field name but a parenthesized expression: (type_and_flags >> 4). wirespec evaluates this expression using only the header fields declared before the within keyword, then dispatches on the result. This lets MQTT's "type nibble" (upper 4 bits of type_and_flags) drive dispatch without introducing a separate field.
Any arithmetic or bitwise expression over prior header fields is valid as a capsule tag.
within Scoping
payload: match (type_and_flags >> 4) within remaining_length {The within remaining_length clause binds the payload parse to exactly remaining_length bytes. A sub-cursor is created from those bytes; parsing each branch operates within that cursor. If a branch consumes fewer bytes than the cursor length, WIRESPEC_ERR_TRAILING_DATA is returned. If a branch tries to read past the cursor boundary, WIRESPEC_ERR_SHORT_BUFFER is returned.
This corresponds directly to how MQTT framing works: the receiver reads remaining_length bytes into a buffer, then parses the type-specific payload from that buffer.
Conditional Fields (if)
will_topic: if connect_flags & 0x04 { MqttString },
will_message: if connect_flags & 0x04 { MqttBytes },
username: if connect_flags & 0x80 { MqttString },
password: if connect_flags & 0x40 { MqttBytes },Each if COND { T } field becomes Option[T] in the IR. In generated C it expands to two fields:
bool has_will_topic;
mqtt_mqtt_string_t will_topic; /* only valid if has_will_topic */At parse time the condition is evaluated; if false the field is skipped and has_will_topic is set to false. At serialize time, if has_will_topic is true the field is written, otherwise it is omitted.
The condition can reference any field declared above it in the same scope — including other if-gated fields via the ?? (coalesce) operator.
let Derived Field
3 => Publish {
topic: MqttString,
let qos: u8 = (type_and_flags & 0x06) >> 1,
packet_id: if qos > 0 { u16 },
payload: bytes[remaining],
},let fields are not on the wire — they carry no bytes — but they appear in the generated struct and can be referenced by subsequent fields and validation clauses. Here qos is derived from the outer type_and_flags header byte (accessible in the branch because it is a prior header field), and then immediately used to gate packet_id.
This pattern avoids redundancy: the QoS level is already encoded in the fixed header bits; there is no need to re-encode it in the payload.
Runtime Validation with require
8 => Subscribe {
require type_and_flags & 0x0F == 0x02,
packet_id: u16,
subscriptions: bytes[remaining],
},MQTT 3.1.1 requires that the lower 4 bits of type_and_flags for a SUBSCRIBE packet must be 0x02. The require clause enforces this at parse time: if the condition is false, parsing returns WIRESPEC_ERR_CONSTRAINT. The check is emitted at the point it appears in the field list, so it can reference any field declared above it.
Compile
wirespec compile examples/mqtt/mqtt.wspec -o build/This generates build/mqtt.h and build/mqtt.c.
Generated C API
wirespec_result_t mqtt_mqtt_packet_parse(
const uint8_t *buf, size_t len,
mqtt_mqtt_packet_t *out, size_t *consumed);
wirespec_result_t mqtt_mqtt_packet_serialize(
const mqtt_mqtt_packet_t *in,
uint8_t *buf, size_t cap, size_t *written);Usage Example
#include "mqtt.h"
void handle_mqtt(const uint8_t *buf, size_t len) {
mqtt_mqtt_packet_t pkt;
size_t consumed;
wirespec_result_t r = mqtt_mqtt_packet_parse(buf, len, &pkt, &consumed);
if (r != WIRESPEC_OK) return;
switch (pkt.kind) {
case MQTT_MQTT_PACKET_CONNECT:
/* pkt.connect.client_id.data is a wirespec_bytes_t */
on_connect(pkt.connect.client_id.data.ptr,
pkt.connect.client_id.data.len);
if (pkt.connect.has_will_topic) {
set_will(pkt.connect.will_topic.data.ptr,
pkt.connect.will_topic.data.len);
}
break;
case MQTT_MQTT_PACKET_PUBLISH:
/* pkt.publish.qos is the derived let field */
on_publish(pkt.publish.topic.data.ptr,
pkt.publish.topic.data.len,
pkt.publish.payload.ptr,
pkt.publish.payload.len,
pkt.publish.qos);
break;
case MQTT_MQTT_PACKET_PINGREQ:
send_pingresp();
break;
default:
break;
}
}What Next
- BLE ATT — little-endian protocols, type aliases, enum fields
- TLS 1.3 — enum tag types,
u24primitive, fill arrays with byte bounds - Classic Protocols — UDP, TCP, Ethernet, IPv4