ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
protobuf.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file protobuf.h
6 * @brief The Protocol Buffers wire format (PROTOCORE_ENABLE_PROTOBUF): a streaming encoder and a
7 * cursor decoder over caller buffers, zero heap.
8 *
9 * The governing specification is Google's Protocol Buffers "Encoding" document
10 * (https://protobuf.dev/programming-guides/encoding/). It is not an IETF document and no RFC
11 * number applies to it. Every normative term below is that document's.
12 *
13 * "Message Structure": a message is a sequence of records, and each record is "the field number, a
14 * wire type and a payload". The tag "is encoded as a varint formed from the field number and the
15 * wire type via the formula `(field_number << 3) | wire_type`".
16 *
17 * "Base 128 Varints": "Each byte in the varint has a continuation bit that indicates if the byte
18 * that follows it is part of the varint. This is the most significant bit (MSB) of the byte." The
19 * lower seven bits are payload, appended in little-endian order, and an unsigned 64-bit value takes
20 * "anywhere between one and ten bytes".
21 *
22 * The wire types, from the same document's table:
23 *
24 * ID Name Used For
25 * 0 VARINT int32, int64, uint32, uint64, sint32, sint64, bool, enum
26 * 1 I64 fixed64, sfixed64, double
27 * 2 LEN string, bytes, embedded messages, packed repeated fields
28 * 3 SGROUP group start (deprecated)
29 * 4 EGROUP group end (deprecated)
30 * 5 I32 fixed32, sfixed32, float
31 *
32 * "Length-Delimited Records": "The LEN wire type has a dynamic length, specified by a varint
33 * immediately after the tag, which is followed by the payload as usual."
34 *
35 * "Groups": "Groups are a deprecated feature that should not be used." SGROUP and EGROUP records
36 * are rejected by the decoder here, as are the two IDs the table does not name.
37 *
38 * sint32 and sint64 carry ZigZag: `(n << 1) ^ (n >> 31)` and `(n << 1) ^ (n >> 63)`.
39 *
40 * An encoder row appends one record at a time into a caller buffer and fails closed on overflow;
41 * an embedded message is encoded into a second row's buffer and added with @ref ProtobufNs::write_bytes.
42 * A decoder row is a cursor: it decodes the record at its own offset and reports where that offset
43 * landed. Rows nest, which is what an embedded message needs, so @ref ProtobufNs::slot names one.
44 *
45 * The module exports one symbol, @ref Protobuf. Everything in protobuf.c has internal linkage.
46 *
47 * @author Douglas Quigg (dstroy0)
48 * @date 2026
49 */
50
51#ifndef PROTOCORE_PROTOBUF_H
52#define PROTOCORE_PROTOBUF_H
53
54#include "protocore_config.h"
55
57
58#if PROTOCORE_ENABLE_PROTOBUF
59
60// Wire type IDs, from the "Encoding" document's wire type table.
61#define PROTOCORE_PROTOBUF_WT_VARINT 0 ///< int32, int64, uint32, uint64, sint32, sint64, bool, enum
62#define PROTOCORE_PROTOBUF_WT_I64 1 ///< fixed64, sfixed64, double
63#define PROTOCORE_PROTOBUF_WT_LEN 2 ///< string, bytes, embedded messages, packed repeated fields
64#define PROTOCORE_PROTOBUF_WT_SGROUP 3 ///< group start, deprecated, rejected by a decode
65#define PROTOCORE_PROTOBUF_WT_EGROUP 4 ///< group end, deprecated, rejected by a decode
66#define PROTOCORE_PROTOBUF_WT_I32 5 ///< fixed32, sfixed32, float
67
68/** @brief One varint holds at most ten octets, the width an unsigned 64-bit value reaches. */
69#define PROTOCORE_PROTOBUF_VARINT_MAX 10
70
71#ifndef PROTOCORE_PROTOBUF_SLOTS
72/** @brief Encoder rows and decoder rows, each. An embedded message holds a second row open. */
73#define PROTOCORE_PROTOBUF_SLOTS 4
74#endif
75
76/** @brief The caller buffer one encoder row appends into. */
77typedef struct
78{
79 uint8_t *buf; ///< the octets an encode writes over
80 size_t cap; ///< how many of them there are
81} ProtobufWriterArgs;
82
83/** @brief The tag an encode stamps: `(field_number << 3) | wire_type`. */
84typedef struct
85{
86 uint32_t field_number; ///< the record's field number, the tag's high bits
87 uint8_t wire_type; ///< the record's wire type, the tag's low three bits
88} ProtobufTagArgs;
89
90/** @brief The payload a record carries, one member per wire type the encoders and decoders take. */
91typedef struct
92{
93 uint64_t u64; ///< VARINT payload, I64 bits, or the ZigZag source a decode converts
94 int64_t i64; ///< signed VARINT payload: two's complement for int64, the ZigZag source for sint64
95 uint32_t u32; ///< I32 bits: a fixed32 payload, or the bits a decode converts
96 float f32; ///< float payload, encoded as I32
97 double f64; ///< double payload, encoded as I64
98 proto_bool flag; ///< bool payload, encoded as the VARINT 0 or 1
99 const uint8_t *data; ///< LEN payload octets
100 size_t len; ///< how many of them there are
101 const char *text; ///< NUL-terminated LEN payload, bounded by the row's capacity
102} ProtobufValueArgs;
103
104/** @brief The encoded octets one decoder row walks. */
105typedef struct
106{
107 const uint8_t *buf; ///< the message octets
108 size_t len; ///< how many are buffered
109 size_t pos; ///< the offset an open seats the cursor at, clamped to @c len
110} ProtobufSourceArgs;
111
112/** @brief One decoded record. For LEN, @c data points INTO the source octets and nothing is copied. */
113typedef struct
114{
115 uint32_t field_number; ///< the tag's high bits
116 uint8_t wire_type; ///< the tag's low three bits
117 uint64_t value; ///< the VARINT payload, or the raw little-endian bits of an I32 / I64 payload
118 const uint8_t *data; ///< the LEN payload
119 size_t len; ///< the length prefix that preceded it
120} ProtobufRecord;
121
122/**
123 * @brief The Protocol Buffers wire format: the record encoder and the record decoder.
124 *
125 * A caller sets the members a call takes, invokes it through ::Protobuf, and reads the outcome off
126 * the same handle.
127 *
128 * @var ProtobufNs::slot the row a call acts on; a @c write_ names an encoder row, a @c read_ a decoder row
129 * @var ProtobufNs::writer the caller buffer an encoder row appends into
130 * @var ProtobufNs::tag the field number and wire type a tag carries
131 * @var ProtobufNs::value the payload one record carries
132 * @var ProtobufNs::source the encoded octets a decoder row walks
133 * @var ProtobufNs::ok a call's true/false outcome
134 * @var ProtobufNs::n the encoded octet count a finish reports, or the offset a decode left its cursor at
135 * @var ProtobufNs::u64 the varint a read decoded
136 * @var ProtobufNs::i64 the sint64 a ZigZag decode produced
137 * @var ProtobufNs::i32 the sint32 a ZigZag decode produced
138 * @var ProtobufNs::f32 the float an I32 bit pattern names
139 * @var ProtobufNs::f64 the double an I64 bit pattern names
140 * @var ProtobufNs::record the record a read decoded
141 * @var ProtobufNs::writer_open seat an encoder row on @c writer and empty it
142 * @var ProtobufNs::write_varint append @c value.u64 as a Base 128 varint, no tag
143 * @var ProtobufNs::write_tag append the tag `(tag.field_number << 3) | tag.wire_type`
144 * @var ProtobufNs::write_uint64 append a VARINT record carrying @c value.u64
145 * @var ProtobufNs::write_int64 append a VARINT record carrying @c value.i64 in two's complement
146 * @var ProtobufNs::write_sint64 append a VARINT record carrying @c value.i64 in ZigZag
147 * @var ProtobufNs::write_bool append a VARINT record carrying @c value.flag as 0 or 1
148 * @var ProtobufNs::write_fixed32 append an I32 record carrying @c value.u32
149 * @var ProtobufNs::write_fixed64 append an I64 record carrying @c value.u64
150 * @var ProtobufNs::write_float append an I32 record carrying the bits of @c value.f32
151 * @var ProtobufNs::write_double append an I64 record carrying the bits of @c value.f64
152 * @var ProtobufNs::write_bytes append a LEN record carrying @c value.data for @c value.len
153 * @var ProtobufNs::write_string append a LEN record carrying @c value.text up to its NUL
154 * @var ProtobufNs::writer_finish report the encoded octet count in @c n, or 0 if any append overflowed
155 * @var ProtobufNs::reader_open seat a decoder row on @c source at @c source.pos
156 * @var ProtobufNs::read_varint decode the Base 128 varint at the cursor into @c u64 and advance it
157 * @var ProtobufNs::read_record decode the record at the cursor into @c record and advance past it
158 * @var ProtobufNs::zigzag64 convert the ZigZag varint @c value.u64 to the sint64 @c i64
159 * @var ProtobufNs::zigzag32 convert the ZigZag varint @c value.u32 to the sint32 @c i32
160 * @var ProtobufNs::float_bits convert the I32 bit pattern @c value.u32 to the float @c f32
161 * @var ProtobufNs::double_bits convert the I64 bit pattern @c value.u64 to the double @c f64
162 */
163typedef struct
164{
165 uint8_t slot; ///< the encoder or decoder row every call names
166 ProtobufWriterArgs writer; ///< where an encode lands
167 ProtobufTagArgs tag; ///< what a tag says
168 ProtobufValueArgs value; ///< what a payload carries
169 ProtobufSourceArgs source; ///< what a decode walks
170 proto_bool ok;
171 size_t n;
172 uint64_t u64;
173 int64_t i64;
174 int32_t i32;
175 float f32;
176 double f64;
177 ProtobufRecord record;
178} ProtobufVars;
179
180/** @brief The operands and the outcome. */
181extern ProtobufVars ProtobufV;
182
183/** @brief The entries. */
184typedef struct
185{
186 void (*const writer_open)(uint8_t *work);
187 void (*const write_varint)(uint8_t *work);
188 void (*const write_tag)(uint8_t *work);
189 void (*const write_uint64)(uint8_t *work);
190 void (*const write_int64)(uint8_t *work);
191 void (*const write_sint64)(uint8_t *work);
192 void (*const write_bool)(uint8_t *work);
193 void (*const write_fixed32)(uint8_t *work);
194 void (*const write_fixed64)(uint8_t *work);
195 void (*const write_float)(uint8_t *work);
196 void (*const write_double)(uint8_t *work);
197 void (*const write_bytes)(uint8_t *work);
198 void (*const write_string)(uint8_t *work);
199 void (*const writer_finish)(uint8_t *work);
200 void (*const reader_open)(uint8_t *work);
201 void (*const read_varint)(uint8_t *work);
202 void (*const read_record)(uint8_t *work);
203 void (*const zigzag64)(uint8_t *work);
204 void (*const zigzag32)(uint8_t *work);
205 void (*const float_bits)(uint8_t *work);
206 void (*const double_bits)(uint8_t *work);
207} ProtobufNs;
208
209// What the table binds, defined once in the .c and taking one parameter each: everything
210// else an entry needs is an operand in ProtobufV or a region of the borrow at a fixed offset.
211void protocore_protobuf_writer_open(uint8_t *work);
212void protocore_protobuf_write_varint(uint8_t *work);
213void protocore_protobuf_write_tag(uint8_t *work);
214void protocore_protobuf_write_uint64(uint8_t *work);
215void protocore_protobuf_write_int64(uint8_t *work);
216void protocore_protobuf_write_sint64(uint8_t *work);
217void protocore_protobuf_write_bool(uint8_t *work);
218void protocore_protobuf_write_fixed32(uint8_t *work);
219void protocore_protobuf_write_fixed64(uint8_t *work);
220void protocore_protobuf_write_float(uint8_t *work);
221void protocore_protobuf_write_double(uint8_t *work);
222void protocore_protobuf_write_bytes(uint8_t *work);
223void protocore_protobuf_write_string(uint8_t *work);
224void protocore_protobuf_writer_finish(uint8_t *work);
225void protocore_protobuf_reader_open(uint8_t *work);
226void protocore_protobuf_read_varint(uint8_t *work);
227void protocore_protobuf_read_record(uint8_t *work);
228void protocore_protobuf_zigzag64(uint8_t *work);
229void protocore_protobuf_zigzag32(uint8_t *work);
230void protocore_protobuf_float_bits(uint8_t *work);
231void protocore_protobuf_double_bits(uint8_t *work);
232
233// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
234// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
235// `Protobuf.writer_open(work)` resolves to a named function and becomes a DIRECT call. An extern table
236// leaves the call indirect and the symbol live at every level, -O2 -flto included.
237static const ProtobufNs Protobuf __attribute__((unused)) = {
238 .writer_open = protocore_protobuf_writer_open,
239 .write_varint = protocore_protobuf_write_varint,
240 .write_tag = protocore_protobuf_write_tag,
241 .write_uint64 = protocore_protobuf_write_uint64,
242 .write_int64 = protocore_protobuf_write_int64,
243 .write_sint64 = protocore_protobuf_write_sint64,
244 .write_bool = protocore_protobuf_write_bool,
245 .write_fixed32 = protocore_protobuf_write_fixed32,
246 .write_fixed64 = protocore_protobuf_write_fixed64,
247 .write_float = protocore_protobuf_write_float,
248 .write_double = protocore_protobuf_write_double,
249 .write_bytes = protocore_protobuf_write_bytes,
250 .write_string = protocore_protobuf_write_string,
251 .writer_finish = protocore_protobuf_writer_finish,
252 .reader_open = protocore_protobuf_reader_open,
253 .read_varint = protocore_protobuf_read_varint,
254 .read_record = protocore_protobuf_read_record,
255 .zigzag64 = protocore_protobuf_zigzag64,
256 .zigzag32 = protocore_protobuf_zigzag32,
257 .float_bits = protocore_protobuf_float_bits,
258 .double_bits = protocore_protobuf_double_bits,
259};
260
261/**
262 * @brief The PROTOCORE_PROTOBUF_BORROW bytes this module's state lives in.
263 *
264 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
265 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
266 * walks, so the state lasts the life of the program.
267 *
268 * @return the span.
269 */
270uint8_t *protocore_protobuf_span(void);
271
272#endif // PROTOCORE_ENABLE_PROTOBUF
273
275
276#endif // PROTOCORE_PROTOBUF_H
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97