C API Reference
wirespec generates C11 code with no heap allocation. All memory is stack-allocated or provided by the caller.
Naming Convention
For a module named mod.sub and a type named FooBar, all generated symbols use the prefix mod_sub_foo_bar_:
| Module | Type | C prefix |
|---|---|---|
quic.frames | QuicFrame | quic_frames_quic_frame_ |
quic.varint | VarInt | quic_varint_var_int_ |
ble.att | AttPdu | ble_att_att_pdu_ |
mqtt | MqttPacket | mqtt_mqtt_packet_ |
The module path components and type name are each converted to snake_case and joined with _.
Generated Functions
For every packet, frame, or capsule named Foo in module mod:
/* Parse: reads from buf[0..len], fills *out, sets *consumed to bytes read. */
wirespec_result_t mod_foo_parse(
const uint8_t *buf, size_t len,
mod_foo_t *out, size_t *consumed);
/* Serialize: writes to buf[0..cap], sets *written to bytes written. */
wirespec_result_t mod_foo_serialize(
const mod_foo_t *val,
uint8_t *buf, size_t cap, size_t *written);
/* Returns the exact number of bytes serialize() will write. */
size_t mod_foo_serialized_len(const mod_foo_t *val);Parse contract:
- Returns
WIRESPEC_OKand advances*consumedon success. - Returns an error code and leaves
*outin an unspecified state on failure. - Never reads beyond
buf + len.
Serialize contract:
- Returns
WIRESPEC_OKand sets*writtenon success. - Returns
WIRESPEC_ERR_SHORT_BUFFERifcap < mod_foo_serialized_len(val). - Never writes beyond
buf + cap.
Usage pattern:
#include "quic_frames.h"
#include "wirespec_runtime.h"
uint8_t buf[4096];
size_t len = read_packet(buf, sizeof(buf));
quic_frames_quic_frame_t frame;
size_t consumed;
wirespec_result_t rc = quic_frames_quic_frame_parse(buf, len, &frame, &consumed);
if (rc != WIRESPEC_OK) {
handle_error(rc);
return;
}
/* Round-trip: serialize back */
uint8_t out[4096];
size_t written;
rc = quic_frames_quic_frame_serialize(&frame, out, sizeof(out), &written);Memory Model — Three Tiers
wirespec never allocates heap memory. Fields are mapped to one of three tiers:
| Tier | What | Strategy | C Representation |
|---|---|---|---|
| A | bytes[...] fields | Zero-copy: pointer + length into the input buffer | wirespec_bytes_t |
| B | [scalar; N] arrays | Materialized: memcpy + byte-swap per element | Fixed-size C array |
| C | [composite; N] arrays | Materialized: parse each struct element | Struct array + count field |
Tier A — Zero-Copy Bytes
bytes[N], bytes[length: expr], bytes[remaining], and bytes[length_or_remaining: expr] all map to wirespec_bytes_t:
typedef struct {
const uint8_t *ptr; /* points into the original input buffer */
size_t len;
} wirespec_bytes_t;ptr is a slice into the parse input buffer. The caller must keep the input buffer alive for as long as the parsed struct is used.
Tier B — Scalar Arrays
[u16le; count] and similar scalar arrays are materialized as fixed-size C arrays with a separate count field:
uint16_t items[WIRESPEC_MAX_ARRAY_ELEMENTS]; /* or @max_len capacity */
size_t items_count;Byte-swapping for endianness is performed during parse and serialize.
Tier C — Composite Arrays
[AckRange; count] and similar struct arrays are materialized as fixed-size arrays of the element struct type:
quic_frames_ack_range_t ack_ranges[WIRESPEC_MAX_ARRAY_ELEMENTS];
size_t ack_ranges_count;Array Capacity
The default capacity for all array fields is WIRESPEC_MAX_ARRAY_ELEMENTS (default: 64).
Override globally for a translation unit:
gcc -DWIRESPEC_MAX_ARRAY_ELEMENTS=128 ...Override per field with @max_len(N) in the .wspec source.
If the parsed element count exceeds the field capacity, parse returns WIRESPEC_ERR_CAPACITY.
Optional Field Pattern
if COND { T } optional fields expand to a boolean presence flag plus the value:
/* wire source: ecn_counts: if frame_type == 0x03 { EcnCounts } */
bool has_ecn_counts;
quic_frames_ecn_counts_t ecn_counts;When has_ecn_counts is false, the ecn_counts field is zero-initialized and must not be read as meaningful data.
The ?? coalesce operator in wirespec source becomes a conditional expression in generated C: offset_raw ?? 0 → (has_offset_raw ? offset_raw : 0).
Frame and Capsule Union Pattern
Tagged union types (frame, capsule) generate a tag enum plus a discriminated union:
/* Generated for: frame QuicFrame = match frame_type: VarInt { ... } */
typedef enum {
QUIC_FRAMES_QUIC_FRAME_TAG_PADDING = 0,
QUIC_FRAMES_QUIC_FRAME_TAG_PING = 1,
QUIC_FRAMES_QUIC_FRAME_TAG_ACK = 2,
QUIC_FRAMES_QUIC_FRAME_TAG_CRYPTO = 6,
QUIC_FRAMES_QUIC_FRAME_TAG_STREAM = 8,
QUIC_FRAMES_QUIC_FRAME_TAG_UNKNOWN = -1,
} quic_frames_quic_frame_tag_t;
typedef struct {
quic_frames_quic_frame_tag_t tag;
union {
quic_frames_quic_frame_padding_t padding;
quic_frames_quic_frame_ping_t ping;
quic_frames_quic_frame_ack_t ack;
quic_frames_quic_frame_crypto_t crypto;
quic_frames_quic_frame_stream_t stream;
quic_frames_quic_frame_unknown_t unknown;
} data;
} quic_frames_quic_frame_t;Dispatch on the tag:
switch (frame.tag) {
case QUIC_FRAMES_QUIC_FRAME_TAG_ACK:
process_ack(&frame.data.ack);
break;
case QUIC_FRAMES_QUIC_FRAME_TAG_STREAM:
process_stream(&frame.data.stream);
break;
default:
break;
}State Machine Pattern
State machines generate a tag enum, a data union, and a dispatch function:
/* Generated for: state machine PathState { ... } */
typedef enum {
MPQUIC_PATH_PATH_STATE_TAG_INIT = 0,
MPQUIC_PATH_PATH_STATE_TAG_VALIDATING = 1,
MPQUIC_PATH_PATH_STATE_TAG_ACTIVE = 2,
MPQUIC_PATH_PATH_STATE_TAG_STANDBY = 3,
MPQUIC_PATH_PATH_STATE_TAG_CLOSING = 4,
MPQUIC_PATH_PATH_STATE_TAG_CLOSED = 5,
} mpquic_path_path_state_tag_t;
typedef struct {
mpquic_path_path_state_tag_t tag;
union {
mpquic_path_path_state_init_t init;
mpquic_path_path_state_validating_t validating;
mpquic_path_path_state_active_t active;
mpquic_path_path_state_standby_t standby;
mpquic_path_path_state_closing_t closing;
/* closed has no data */
} data;
} mpquic_path_path_state_t;
/* Dispatch: applies event to state machine, transitions in place. */
wirespec_result_t mpquic_path_path_state_dispatch(
mpquic_path_path_state_t *sm,
mpquic_path_path_event_tag_t event,
mpquic_path_path_event_data_t *event_data);On success, *sm holds the new state. On unhandled event, returns WIRESPEC_ERR_INVALID_STATE.
Runtime API (wirespec_runtime.h)
The runtime is a single header-only file (under 500 LOC) with no external dependencies.
Cursor
typedef struct {
const uint8_t *buf;
size_t pos;
size_t len;
} wirespec_cursor_t;
void wirespec_cursor_init(wirespec_cursor_t *c,
const uint8_t *buf, size_t len);Read Functions
wirespec_result_t wirespec_cursor_read_u8 (wirespec_cursor_t *c, uint8_t *out);
wirespec_result_t wirespec_cursor_read_u16be(wirespec_cursor_t *c, uint16_t *out);
wirespec_result_t wirespec_cursor_read_u16le(wirespec_cursor_t *c, uint16_t *out);
wirespec_result_t wirespec_cursor_read_u32be(wirespec_cursor_t *c, uint32_t *out);
wirespec_result_t wirespec_cursor_read_u32le(wirespec_cursor_t *c, uint32_t *out);
wirespec_result_t wirespec_cursor_read_u64be(wirespec_cursor_t *c, uint64_t *out);
wirespec_result_t wirespec_cursor_read_u64le(wirespec_cursor_t *c, uint64_t *out);
/* Zero-copy: sets out->ptr into the cursor's buffer, advances pos by len. */
wirespec_result_t wirespec_cursor_read_bytes(wirespec_cursor_t *c,
size_t len,
wirespec_bytes_t *out);
/* Creates a sub-cursor for a within EXPR scope of exactly sub_len bytes. */
wirespec_result_t wirespec_cursor_sub(wirespec_cursor_t *parent,
size_t sub_len,
wirespec_cursor_t *sub);Write Functions
wirespec_result_t wirespec_write_u8 (uint8_t *buf, size_t cap, size_t *pos, uint8_t val);
wirespec_result_t wirespec_write_u16be(uint8_t *buf, size_t cap, size_t *pos, uint16_t val);
wirespec_result_t wirespec_write_u16le(uint8_t *buf, size_t cap, size_t *pos, uint16_t val);
wirespec_result_t wirespec_write_u32be(uint8_t *buf, size_t cap, size_t *pos, uint32_t val);
wirespec_result_t wirespec_write_u32le(uint8_t *buf, size_t cap, size_t *pos, uint32_t val);
wirespec_result_t wirespec_write_u64be(uint8_t *buf, size_t cap, size_t *pos, uint64_t val);
wirespec_result_t wirespec_write_u64le(uint8_t *buf, size_t cap, size_t *pos, uint64_t val);
/* Writes bytes->len bytes from bytes->ptr. */
wirespec_result_t wirespec_write_bytes(uint8_t *buf, size_t cap, size_t *pos,
const wirespec_bytes_t *bytes);All read/write functions return WIRESPEC_ERR_SHORT_BUFFER if the cursor/buffer does not have sufficient space.
Including Generated Headers
Generated .h files include wirespec_runtime.h via a relative path. Ensure the runtime directory is on the include path:
gcc -I path/to/runtime -o my_app my_app.c quic_frames.c quic_varint.cThe runtime header is self-contained — no .c file is needed for the runtime itself.
Rust Backend
wirespec also supports a Rust backend. Pass -t rust to generate a .rs file instead of .h/.c:
wirespec compile examples/quic/varint.wspec -t rust -o build/Generated Rust code uses the wirespec-rt crate (located at runtime/rust/wirespec-rt/) which provides Cursor, Writer, and Error types. See the Rust API reference for details.