Source:
JsonSink.h
JsonSink¶
Writes a document with no fixed size ceiling, in one of three modes, so a module tree of any size serializes correctly.
Functions¶
| Return | Name | Description |
|---|---|---|
void |
jsonEscape inline |
Escape a string for embedding in a literal, without the surrounding quotes, truncating rather than overflowing its output. |
jsonEscape¶
inline
Escape a string for embedding in a literal, without the surrounding quotes, truncating rather than overflowing its output.
JsonSink¶
src/core/util/JsonSink.h:49Public Attributes¶
void va_list ap {
char frag[FRAG_MAX]
va_list ap2
int n = std::vsnprintf(frag, sizeof(frag), fmt, ap)
char * big = static_cast<char*>((static_cast<size_t>(n) + 1))
Public Methods¶
inline explicit JsonSink(platform::TcpConnection & conn)
: Socket mode: a staging buffer flushes to the connection as it fills.
JsonSink() = default
: Buffer mode: bytes collect in a block this owns.
inline JsonSink(char * buf, size_t cap)
: Fixed mode, writing into a caller-owned slice with no allocation: what happens when it fills.
inline ~JsonSink()
: Frees the block, when one was taken and not detached.
JsonSink(const JsonSink &) = delete
: Non-copyable: it owns a heap block and possibly a connection.
JsonSink & operator=(const JsonSink &) = delete
: Non-assignable, for the same reason.
inline void append(const char * s)
: Append a string, growing or flushing as the mode requires.
void appendf(const char * fmt, ...)
: Append a formatted fragment; the common case fits a stack buffer and a longer one is re-formatted so nothing is silently truncated.
va_start(ap, fmt)
va_copy(ap2, ap)
va_end(ap)
inline if()
inline if()
inline if(fixed_)
inline if(big)
va_end(ap2)
inline void writeNumber(double v)
: Write one syntactically correct value: why firmware uses the typed serializers instead.
inline void writeBool(bool v)
: Write a boolean literal.
inline void writeJsonString(const char * s)
: Write a string as a quoted literal, escaping what the standard requires.
inline void flush()
: Push whatever is staged to the connection, in socket mode.
inline const char * data() const
: The collected document and its length, terminated; in fixed mode this is the caller's own buffer.
inline size_t size() const
: How many bytes have been written.
inline char * detach()
: Hand the block to the caller and give up ownership, so a large built document is not copied out; nothing in the other modes.
inline bool overflowed() const
: Whether a write was refused, which makes the document incomplete.
inline int nameIndex() const
: Which single option a palette call wants, the default meaning the whole set: it rides here because the callback takes only a sink, the one channel into the light domain.
inline void requestName(uint8_t index)
: Ask a palette callback for one option's name rather than the whole set.
More info¶
The three modes¶
A socket mode flushes a small staging buffer to a connection as it fills, so a whole response never lives in memory at once. A buffer mode collects into a heap block that grows on demand, for a caller that needs the assembled document and its length up front. A fixed mode writes into a caller-owned slice and raises an overflow flag rather than truncating silently or growing, which the save path wants.
Growth steps down when doubling is refused¶
Doubling is the right default, giving amortized constant-time appends, but both buffers are live across the copy. So growing a large block asks for half again as much CONTIGUOUS memory at once, and a device serving a big document has the free bytes without the block. The allocation then failed, every later append was dropped, and the device shipped a truncated document that looked complete.
A refused doubling therefore steps down toward the minimum rather than giving up: slower to grow, and it fits where doubling cannot. Measured on the bench, both classic boards cut their document at a power of two, losing eight to eleven kilobytes of the tree.
The step is a QUARTER of the current capacity rather than the bare minimum. Backing off to just enough serves one append and grows again on the next character, which copies quadratically and thrashes the fragmented heap that refused the doubling.
Overflow is a flag, never a silent truncation¶
Once tripped, later appends do nothing, so a caller sees one consistent state. A caller that ships the buffer anyway sends a truncated document, indistinguishable from a whole one at the far end. The receiver parses it, throws, and drops the tail.
The fixed mode never allocates, its capacity being the caller's slice on purpose, since an allocation there would succeed even when the slice is too small.
The number writer takes a double, and firmware must not call it¶
Its only caller is the test runner's bridge, which stores numerics that way so it does not lose precision parsing fixtures. A double runs in software emulation on one architecture, far slower than the single-precision type, so production paths use the typed serializers instead.