Skip to content

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

wire
@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

FeatureWhere
Continuation-bit VarInttype MqttLength = varint { ... }
Nested packet typesMqttString, MqttBytes as field types
Expression-based capsule tagmatch (type_and_flags >> 4) within ...
within byte-length scopingwithin remaining_length
Conditional fields (if)will_topic: if connect_flags & 0x04 { ... }
let derived fieldlet qos: u8 = (type_and_flags & 0x06) >> 1
Dependent conditionalpacket_id: if qos > 0 { u16 }
Runtime validationrequire type_and_flags & 0x0F == 0x02
bytes[remaining]payload: bytes[remaining]
Empty branchPingReq {}, PingResp {}, Disconnect {}

Feature Notes

Continuation-Bit VarInt

wire
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:

ParameterValueMeaning
continuation_bitmsbBit 7 of each byte signals "more bytes follow"
value_bits77 bits of payload per byte
max_bytes4After 4 bytes without termination → WIRESPEC_ERR_OVERFLOW
byte_orderlittleLeast-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

wire
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

wire
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)

wire
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:

c
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

wire
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

wire
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

bash
wirespec compile examples/mqtt/mqtt.wspec -o build/

This generates build/mqtt.h and build/mqtt.c.

Generated C API

c
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

c
#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, u24 primitive, fill arrays with byte bounds
  • Classic Protocols — UDP, TCP, Ethernet, IPv4