MMgr v1.0.0
Zero-heap memory manager in C11. One borrowed buffer, carved by asserted offset.
Loading...
Searching...
No Matches
Verba scribo: the string builder

Text out, without printf and without a heap.

When to reach for it

Any time you would reach for snprintf and prefer not to link a formatter, or cannot afford one.

Worked example

size_t at = 0;
at = EMBED_CALL(verba.put, VerbaCfg, .out = buf, .cap = sizeof buf, .at = at, .text = "id=");
at = EMBED_CALL(verba.uint, VerbaCfg, .out = buf, .cap = sizeof buf, .at = at, .val = id);
at = EMBED_CALL(verba.put, VerbaCfg, .out = buf, .cap = sizeof buf, .at = at, .text = " rate=");
at = EMBED_CALL(verba.fixed, VerbaCfg, .out = buf, .cap = sizeof buf, .at = at, .real = rate,
.decimals = 2);
at = EMBED_CALL(verba.ch, VerbaCfg, .out = buf, .cap = sizeof buf, .at = at, .ch = '\n');
const size_t len = EMBED_CALL(verba.finish, VerbaCfg, .out = buf, .cap = sizeof buf, .at = at);
if (len == 0u) {
}

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.

The entries, by job

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.

A line that carries its own cursor

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.

char out[128];
VerbaLinea line = {out, sizeof out, 0u, NULL, NULL, 0u};
mmgr_line_put(&line, "id=");
mmgr_line_decimal(&line, id, 1u);
mmgr_line_put(&line, " addr=");
mmgr_line_hex(&line, addr, 8u);
const size_t len = mmgr_line_finish(&line);
A line written into out, at bytes so far, and handed to flush as it fills.
Definition verba_linea.h:53
void mmgr_line_hex(VerbaLinea *line, uint64_t value, unsigned int digits_least)
Writes value in lower case base sixteen, padded on the left with zeros to digits_least digits.
size_t mmgr_line_put(VerbaLinea *line, const char *text)
Writes a terminated text.
void mmgr_line_decimal(VerbaLinea *line, uint64_t value, unsigned int digits_least)
Writes value in base ten, padded on the left with zeros to digits_least digits.
size_t mmgr_line_finish(VerbaLinea *line)
Stores the terminator, the one store a line no write has reached still needs.

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

Gotchas

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.

Generated reference


Numeros scribo: the field formatter

A declarative layout. The format is data and not a string.

When to reach for it

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.

Worked example

Two arrays. The spec says what the record looks like and holds no data. The values are supplied separately and are consumed in order.

static const mmgr_field row[] = {
{MMGR_FK_LIT, 0, 3, "id="},
{MMGR_FK_LIT, 0, 5, " hex="},
{MMGR_FK_HEX, 0u, 0u, NULL},
};
const mmgr_fval vals[] = { MMGR_VU32(id), MMGR_VHEX(flags) };
EMBED_CALL(numer.build, NumerosCfg, .out = buf, .cap = sizeof buf,
.spec = row, .vals = vals, .nvals = 2u);
#define MMGR_END
Expands to an mmgr_field of kind MMGR_FK_END, with width 0, bytes 0 and literal NULL.
@ MMGR_FK_LIT
@ MMGR_FK_HEX
#define MMGR_VU32(value_)
Expands to an mmgr_fval of kind MMGR_FK_U32, with value_ in as.u32 and width 0.
#define MMGR_VHEX(value_)
Expands to an mmgr_fval of kind MMGR_FK_HEX, with value_ in as.u64 and width 0.
#define MMGR_U32
Expands to an mmgr_field of kind MMGR_FK_U32, with width 0, bytes 0 and literal NULL.
Arguments for the four numer calls.
char *const out
One field of a spec.
One value, tagged with the kind that selects which arm of as holds it.

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.

Why a spec array and not a format string

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.

Gotchas

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.

Generated reference


Memoriam praetereo: transfer submission

Note
Compiled only when MMGR_ENABLE_DMA is set, which defaults off. With it off the module is not in the library and its suite is skipped with a message instead of passing empty.

What it is

A thin, portable surface over a DMA controller: open a channel, submit a transfer, poll or take a callback, close it.

/* peripheral is a plain uint8_t the port assigns a meaning to; the library carries no
enum of peripherals, because which ones exist is the part's business and not its own. */
const PraetCfg ch = {
.channel = 0,
.peripheral = PORT_UART0,
};
if (EMBED_CALL(praet.open, PraetCfg, .channel = ch.channel, .peripheral = ch.peripheral))
{
EMBED_CALL(praet.tx_submit, PraetTransferCfg, .channel = 0, .buf = buf, .len = len);
EMBED_CALL(praet.poll, PraetCfg, .channel = 0);
EMBED_CALL(praet.close, PraetTransferCfg, .channel = 0);
}
Arguments for opening a channel, and for polling one.
const uint8_t channel
const uint8_t peripheral
Arguments for submitting a transfer, and for closing a channel.
const uint8_t channel

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 hardware hooks are weak

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.

Gotchas

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.

Generated reference