Text out, without printf and without a heap.
Any time you would reach for snprintf and prefer not to link a formatter, or cannot afford one.
The writers hold no state. Each one takes the position it should write at and returns the position after what it wrote, and so the cursor is the caller's at and nothing is carried between calls. A writer with no room returns cap, which every later writer also returns. An overflow propagates to finish without a flag and finish reports it as a length of zero.
There is no format string anywhere. Nothing parses d at runtime, and so nothing can disagree with the argument you passed.
| job | entries |
|---|---|
| raw text | put, put_n, put_clip, ch |
| unsigned | uint, u32, u32w, u64, u64_clip, hex |
| signed | i64 |
| floating point | g, fixed, sign_bit, is_inf, is_nan |
| escaping | xml, json |
| finishing | finish |
put_n takes the length with the text. A literal costs no scan: pass sizeof "literal" - 1u. put measures what it is given.
verba_scribo/verba_linea.h keeps the buffer, the cursor and an optional flush together in a VerbaLinea, and its mmgr_line_* entries take that line and the value as plain arguments. The header includes mmgr.h alone and defines no table, and therefore a C++ translation unit calls them as they are.
Every write leaves the buffer terminated, one store paid on every call, and a written line whose buffer holds no terminator has been corrupted by something other than the line. A line with no flush behaves as snprintf does: a write that does not fit keeps what fits, terminated in the last byte, leaves the line full, and finish reports zero. mmgr_line_put, mmgr_line_put_n and mmgr_line_put_most answer the bytes of the text the line took, and a caller that sees fewer than the text holds goes on from text + taken, in the same buffer with at set to zero once those bytes are read. A line with a flush hands what it holds to the flush as it fills and goes on, and a text of any length passes through a buffer of any size. A flush answers 1 where it took the bytes and 0 where it refused, which leaves the line full. mmgr_line_put reads its text a room at a time and never past the terminator, and so it needs no bound on the text's length.
| job | entries |
|---|---|
| raw text | mmgr_line_put, mmgr_line_put_n, mmgr_line_put_most, mmgr_line_ch |
| columns | mmgr_line_put_columns, mmgr_line_decimal_columns |
| numbers | mmgr_line_decimal, mmgr_line_decimal_signed, mmgr_line_hex |
| making | mmgr_line_over |
| finishing | mmgr_line_flush, mmgr_line_finish, mmgr_line_ok |
Overflow propagates, and so check once at the end. A writer with no room returns cap and every later writer returns cap too, and so the writes after an overflow are safe no-ops and finish reports zero. See How failure is reported.
xml and json escape, they do not quote. You supply the surrounding quotes.
put_clip truncates deliberately. It is for a field with a maximum width, and it is not the same as running out of room.
Float formatting is exact bit work, not printf. It goes through Fractio: IEEE-754 fields.
A declarative layout. The format is data and not a string.
A record with a fixed shape emitted many times: a log line, a telemetry frame, a fixed-width table. Describe the layout once as an array and fill values in.
Two arrays. The spec says what the record looks like and holds no data. The values are supplied separately and are consumed in order.
A literal is a MMGR_FK_LIT field carrying its own text and length, and so it costs no scan. Every other spec entry is a bare kind taking no argument. There is a one-word macro for some of them, MMGR_STR, MMGR_U32, MMGR_U64, MMGR_I64, MMGR_CH, MMGR_JSON, MMGR_XML, MMGR_END (src/numeros_scribo/numeros_scribo.h:60-85), and the rest are written as the brace form above. MMGR_FK_HEX, MMGR_FK_DEC, MMGR_FK_OCT, MMGR_FK_G and MMGR_FK_FIX have no such macro.
Four entries: build writes a record from a spec, emit writes values with no spec at all, and append and emit_append add to a record already in the buffer.
A format string is parsed at runtime and nothing checks it against the arguments. Here the spec is a const array in flash, the kind is an enum, and every value carries its own kind tag. build compares the two and refuses the record if they disagree.
MMGR_END terminates the spec. Leaving it off runs off the end of the array.
A kind mismatch returns 0 and empties the buffer. So does too few values, or too many. The record is written or it is not; there is no partial record.
emit takes no spec. Each value carries its own kind and width, and so it is the entry to use when the layout is not fixed.
A thin, portable surface over a DMA controller: open a channel, submit a transfer, poll or take a callback, close it.
There is no handle. The channel number identifies the transfer, and so nothing has to be stored between open and close. open and tx_submit return false when the channel is busy or out of range.
A completion callback is optional: point PraetCfg::on_complete at a PraetCallbackCfg and it is called with a mmgr_praet_event describing what finished.
The functions that actually touch a controller are EMBED_WEAK. A board support file overrides one by defining a symbol with the same name: no registration, no function pointer table, no init order to get right.
Without an override they are present and refuse every request, as lets memoriam_praetereo compile and its tests link on a host with no DMA controller at all.
A submitted buffer must stay valid until the transfer completes [BORROWS]. The module does not copy it and cannot tell when the controller is done: poll and the completion callback report. Everywhere else in MMgr a lifetime ends at a mark you control; here it ends when the hardware says so.
Cache coherency is not handled here. On a part with a data cache and a DMA engine that does not snoop it, the clean and invalidate are the board file's job.
The schedule's knobs are read only when the gate is on, and an unset one stops the build. PRAET_CHANNELS and the rest arrive through MMGR_PRAET_KNOBS. See Every compile-time knob.
praet_hw_progress is a fifth hook, with a weak default in praet_ordo.c that reports no movement. A port that leaves it in place still gets the watchdog: a running channel nothing reports on is marked stalled once its keepalive window passes.