Wire Encoding and Runtime Codec
Two layers
Section titled “Two layers”Each call has two layers of data:
- Frame: a fixed header routes requests, responses, streams, and DartFn messages.
- Payload: generated code writes arguments or results in declaration order.
The flow is:
Dart generated API │ arguments / result ▼payload (ByteWriter / ByteReader) │ plus the 24-byte fixed header ▼wire frame │ ▼C++ dispatch / SessionYou normally do not write ByteWriter or ByteReader directly. Codegen creates
the C++ dispatch and Dart codec; application code only provides annotated C++ APIs.
Frame header
Section titled “Frame header”The fixed header is 24 bytes, encoded in little-endian order:
| Offset | Size | Field | Purpose |
|---|---|---|---|
| 0 | 4 | magic |
0x31424344 (DCB1) |
| 4 | 2 | version |
currently 1 |
| 6 | 1 | msg_type |
request, response, stream, or DartFn |
| 7 | 1 | flags |
reserved, currently 0 |
| 8 | 8 | request_id |
RPC, stream, or DartFn reply correlation ID |
| 16 | 4 | method_id |
generated API method number |
| 20 | 4 | payload_len |
payload size in bytes |
| 24 | N | payload |
argument or result data |
See Wire Protocol for message types
and compatibility constraints. method_id, field order, and protocol version
are compatibility contracts and must not be changed casually.
Primitive payload encoding
Section titled “Primitive payload encoding”Payloads have no self-describing field names or type tags. The sender and receiver must use the same generated code and read/write in the same order:
| Wire value | Size / layout |
|---|---|
bool |
1 byte, 0 or 1 |
u8 |
1 byte |
i32 / u32 |
4 bytes, little-endian |
i64 |
8 bytes, little-endian |
f32 / f64 |
IEEE 754 floating point |
string |
u32 byte length + UTF-8 bytes, without NUL |
DateTime |
i64 Unix microsecond timestamp, interpreted as UTC |
Int128 / UInt128 |
u32 length + decimal ASCII string |
internal u64 |
frame IDs, object handles, pointer addresses; not a public codegen primitive |
The public C++ to wire-value mapping is defined by Type Mapping.
The fact that the low-level codec can carry i16, u16, or u64 does not make
those values public codegen API types.
Composite values
Section titled “Composite values”std::vector<T>: au32element count, followed by each element.std::vector<uint8_t>: the sameu32count, followed by the raw bytes.std::array<T, N>: exactlyNelements, with no count prefix; generated Dart code checks the length.std::optional<T>: au8presence tag;0isNone, and1is followed byT.std::map/std::unordered_map: au32count, followed by each key/value.std::set/std::unordered_set: au32count, followed by each element.std::pair/std::tuple: positional values, with no length or field names.enum class: the enum value encoded asi32.BRIDGE_DATA_CLASS: recursively encoded in C++ field declaration order, with no field names.BRIDGE_OPAQUE: anu64object handle, not object fields.std::uint8_t*/const std::uint8_t*: only theu64native address; pointed-to bytes and length are not included. Pass the length as a separate argument.
Ordered and unordered map/set types have the same payload shape. Iteration order of an unordered container is not part of the wire protocol semantics.
Rules for hand-written codecs
Section titled “Rules for hand-written codecs”Direct codec use is for hand-written wire dispatch, test fixtures, or the runtime itself. Writes and reads must be paired exactly:
dcb::ByteWriter writer;writer.i32(42);writer.str("Hi");
const auto& raw = writer.raw();dcb::ByteReader reader(raw.data(), raw.size());const auto number = reader.i32(); // 42const auto text = reader.str(); // "Hi"In a normal project, generated wire_dispatch.cpp and Dart *.g.dart perform
this work. Do not wrap business functions in another custom Future/Stream or
manually assemble payloads. Async functions use stdexec::task<T>, streams use
dcb::StreamSink<T>, and the generator keeps the types and encoding aligned.
Payload example
Section titled “Payload example”These values are encoded in order:
int32 42 → 2A 00 00 00string "Hi" → 02 00 00 00 48 69optional<int32>(42) → 01 2A 00 00 00For an API argument std::vector<uint8_t>{10, 20}, the payload fragment is:
02 00 00 00 0A 14std::array<int32_t, 2>{10, 20} has no 02 00 00 00 count prefix because the
array length is already part of the C++ type.
Error frames
Section titled “Error frames”C++ exceptions do not cross FFI. At the wire boundary they become a
responseErr frame:
code i32 error code (generated dispatch currently uses 1)message string length-prefixed error messageDart turns the error frame into an exception; callers never need to parse a C++ exception object.
Validation and lifetime
Section titled “Validation and lifetime”parseFrame/ByteReadervalidate magic, protocol version, payload length, and read boundaries.- The wire protocol is for Dart and bundled C++ in one process, not for parsing untrusted network input.
uint8_t*transmits only an address. Dart-owned input memory must remain valid until a synchronous call returns or an asyncFuturecompletes; C++ must validate the address and length itself.- Ownership and validity of returned pointers are part of the C++ API contract. The bridge does not copy, free, or add bounds checks automatically.
Further reading
Section titled “Further reading”- Type Mapping: C++ types, Dart types, and codegen rules
- Wire Protocol: frame fields and message types
- stdexec Usage Guide: sender/task patterns for async code
- Code Generation: generate Dart/C++ bindings from C++ headers