ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
frame.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file frame.h
6 * @brief Declarative frame builder: a frame is a static table of typed fields, built by one engine.
7 *
8 * The problem this solves. Replacing printf-style formatting with hand-written pc_sb append
9 * sequences trades one call for a dozen, and every one of those dozens is new code with its own
10 * chance of a wrong order, a missing separator, or a forgotten overflow test. Across the ~160
11 * formatting sites in this library that is well over a thousand new lines, none of which any test
12 * covers individually. A frame SPEC moves that structure into data: the shape of the frame is a
13 * `static const pc_field[]` in rodata, the engine that walks it is one function, and the engine is
14 * what gets tested. Adding a frame after that adds a table, not logic.
15 *
16 * Why not a format string. A format string encodes the same table, but re-parses it from text on
17 * every call - scanning characters, decoding widths, dispatching per conversion - to rediscover
18 * what was known when the code was written. The spec is pre-decoded: the engine reads an opcode
19 * and a width from a struct and jumps. Nothing is parsed at runtime, and no float formatter is
20 * linked in unless a frame actually declares a float field.
21 *
22 * **Contract.**
23 * - Returns the number of bytes written (excluding the NUL) on success.
24 * - Returns 0 if the frame does not fit, and writes `out[0] = '\0'`. There is no truncation:
25 * a partial frame is a protocol violation, and a caller that ignores the return still finds a
26 * valid empty C string rather than half a header or stale bytes from a previous build.
27 * - `out` is always NUL-terminated on both paths, so a caller may always read it as a string.
28 * - A NULL `PC_FK_STR` argument renders as empty, never as a crash or "(null)".
29 *
30 * **Arguments** are passed variadically in spec order, one per field that declares one (PC_FK_LIT
31 * and PC_FK_END take none). They are read at their default-promoted types, so a `uint8_t` passed
32 * to PC_FK_U32 arrives as `unsigned` and a `float` passed to PC_FK_G arrives as `double`, which is
33 * what the engine expects. The compiler cannot check that arity, but a spec's field count and its
34 * call sites are visible in the same translation unit, so it is checkable offline - that gate lands
35 * with the rollout, and until it does a mismatched spec is caught only by its test.
36 *
37 * @author Douglas Quigg (dstroy0)
38 * @date 2026
39 */
40
41#ifndef PROTOCORE_FRAME_H
42#define PROTOCORE_FRAME_H
43
45#include <stdarg.h>
46#include <stddef.h>
47#include <stdint.h>
48
49// PC_ALLOW_UNSCOPED_ENUM: the value IS the opcode byte written to the wire and compared as an
50// integer, so an enum class would put a static_cast at every use to satisfy the lint without
51// making anything safer.
52/** @brief Field kinds. The value is an opcode, so this is deliberately a plain byte enum. */
53enum pc_fk : uint8_t
54{
55 PC_FK_END = 0, ///< terminator; takes no argument
56 PC_FK_LIT, ///< literal text from `lit`; takes no argument
57 PC_FK_STR, ///< const char * (NULL renders as empty)
58 PC_FK_U32, ///< uint32_t, plain decimal
59 PC_FK_U64, ///< uint64_t, plain decimal
60 PC_FK_I64, ///< int64_t, signed decimal
61 PC_FK_DEC, ///< uint32_t, decimal zero-padded to `width`
62 PC_FK_HEX, ///< uint64_t, lowercase hex zero-padded to `width`
63 PC_FK_OCT, ///< uint64_t, octal zero-padded to `width`
64 PC_FK_G, ///< double, printf %.<width>g (width 0 means 6)
65 PC_FK_FIX, ///< double, printf %.<width>f
66 PC_FK_CH, ///< char
67 PC_FK_JSON, ///< const char *, emitted as a quoted JSON string literal
68 PC_FK_XML, ///< const char *, XML-escaped
69};
70
71/**
72 * @brief One field of a frame. Frames are `static const pc_field[]`, so they live in rodata.
73 *
74 * @c len carries a literal's length, written out in the spec and verified by
75 * ci_tooling/check/check_frame_specs.py. A spec is fixed when the code is written, so having the
76 * engine re-scan each literal for its NUL at runtime is the same waste as re-parsing a format
77 * string: measured at +54% on a response frame and +184% on a literal-only one.
78 */
80{
81 uint8_t kind; ///< a pc_fk
82 uint8_t width; ///< min digits (DEC/HEX/OCT), significant digits (G), decimals (FIX)
83 uint16_t len; ///< PC_FK_LIT: byte length of @c lit; gated by check_frame_specs.py
84 const char *lit; ///< PC_FK_LIT only
85};
86
87// Spec constructors. These read as the frame they describe:
88// static const pc_field RESP[] = {{PC_FK_LIT, 0, 9, "HTTP/1.1 "}, PC_U32, {PC_FK_LIT, 0, 1, " "}, PC_STR, PC_END};
89// Field order is {kind, width, len, lit}. A valued field that needs no width or literal takes an
90// object-like macro; a field carrying a width or a literal is written as a plain aggregate, because
91// a macro that took the width or the string as a parameter would be a function-like macro
92// (AUTOSAR A16-0-1). The literal's length is therefore spelled out rather than computed:
93//
94// static const pc_field RESP[] = {
95// {PC_FK_LIT, 0, 9, "HTTP/1.1 "}, // 9 == the literal's length
96// PC_U32,
97// {PC_FK_HEX, 8, 0, nullptr}, // 8 == zero-pad width
98// PC_END,
99// };
100//
101// Hand-counting is not trusted: ci_tooling/check/check_frame_specs.py fails the build when any
102// PC_FK_LIT field's len disagrees with its literal, and --fix rewrites it.
103#define PC_STR {PC_FK_STR, 0, 0, nullptr}
104#define PC_U32 {PC_FK_U32, 0, 0, nullptr}
105#define PC_U64 {PC_FK_U64, 0, 0, nullptr}
106#define PC_I64 {PC_FK_I64, 0, 0, nullptr}
107#define PC_CH {PC_FK_CH, 0, 0, nullptr}
108#define PC_JSON {PC_FK_JSON, 0, 0, nullptr}
109#define PC_XML {PC_FK_XML, 0, 0, nullptr}
110#define PC_END {PC_FK_END, 0, 0, nullptr}
111
112/**
113 * @brief Build @p spec into @p out (capacity @p cap), taking one variadic argument per valued field.
114 * @return bytes written, or 0 if the frame did not fit (in which case @p out is set empty).
115 */
116size_t pc_frame_build(char *out, size_t cap, const pc_field *spec, ...);
117
118/** @brief va_list form, for a caller that already has one. */
119size_t pc_frame_vbuild(char *out, size_t cap, const pc_field *spec, va_list ap);
120
121/**
122 * @brief Append @p spec to the NUL-terminated contents already in @p out.
123 *
124 * The append idiom this library uses for header and cookie accumulation: on overflow the buffer is
125 * rewound to its previous length, so a frame is added whole or not at all and a half-written line
126 * never reaches the wire.
127 *
128 * @return the new total length, or 0 if the frame did not fit (previous contents preserved).
129 */
130size_t pc_frame_append(char *out, size_t cap, const pc_field *spec, ...);
131
132#endif // PROTOCORE_FRAME_H
size_t pc_frame_build(char *out, size_t cap, const pc_field *spec,...)
Build spec into out (capacity cap), taking one variadic argument per valued field.
Definition frame.cpp:100
size_t pc_frame_append(char *out, size_t cap, const pc_field *spec,...)
Append spec to the NUL-terminated contents already in out.
Definition frame.cpp:109
size_t pc_frame_vbuild(char *out, size_t cap, const pc_field *spec, va_list ap)
va_list form, for a caller that already has one.
Definition frame.cpp:25
pc_fk
Field kinds. The value is an opcode, so this is deliberately a plain byte enum.
Definition frame.h:54
@ PC_FK_G
double, printf %.<width>g (width 0 means 6)
Definition frame.h:64
@ PC_FK_OCT
uint64_t, octal zero-padded to width
Definition frame.h:63
@ PC_FK_JSON
const char *, emitted as a quoted JSON string literal
Definition frame.h:67
@ PC_FK_LIT
literal text from lit; takes no argument
Definition frame.h:56
@ PC_FK_CH
char
Definition frame.h:66
@ PC_FK_HEX
uint64_t, lowercase hex zero-padded to width
Definition frame.h:62
@ PC_FK_FIX
double, printf %.<width>f
Definition frame.h:65
@ PC_FK_I64
int64_t, signed decimal
Definition frame.h:60
@ PC_FK_DEC
uint32_t, decimal zero-padded to width
Definition frame.h:61
@ PC_FK_U32
uint32_t, plain decimal
Definition frame.h:58
@ PC_FK_U64
uint64_t, plain decimal
Definition frame.h:59
@ PC_FK_END
terminator; takes no argument
Definition frame.h:55
@ PC_FK_STR
const char * (NULL renders as empty)
Definition frame.h:57
@ PC_FK_XML
const char *, XML-escaped
Definition frame.h:68
Bounded no-heap string builder that fails closed on overflow (one shared copy).
One field of a frame. Frames are static const pc_field[], so they live in rodata.
Definition frame.h:80
const char * lit
PC_FK_LIT only.
Definition frame.h:84
uint8_t width
min digits (DEC/HEX/OCT), significant digits (G), decimals (FIX)
Definition frame.h:82
uint8_t kind
a pc_fk
Definition frame.h:81
uint16_t len
PC_FK_LIT: byte length of lit; gated by check_frame_specs.py.
Definition frame.h:83