MMgr v1.0.0
Zero-heap memory manager in C11. One borrowed buffer, carved by asserted offset.
Loading...
Searching...
No Matches
The dispatch table idiom

Why a call reads EMBED_CALL(spat.from, SpatiumCfg, .buf = p, .cap = n) and what that costs.

Two forms, one function

Every module declares plain C functions with a long, prefixed name, and also gathers them into a static const struct of function pointers named for a short Latin stem.

Every entry takes exactly one argument: a pointer to that module's config struct. EMBED_CALL builds the struct as a compound literal and passes its address, and so the members are named at the call site and anything left out is zero:

mmgr_span s = EMBED_CALL(spat.from, SpatiumCfg, .buf = p, .cap = n);
Arguments for every spat call, where each call reads only the members it needs.
Definition spatium.h:80
uint8_t *const buf
Definition spatium.h:84
A buffer being filled: its bytes, how far the writer has gone, and whether it ran out.
Definition spatium.h:47

the same call as

mmgr_span s = mmgr_spat_from(&(SpatiumCfg){.buf = p, .cap = n});
mmgr_span mmgr_spat_from(const SpatiumCfg *args)
Builds a fill span over args->buf, with pos at 0 and overflow clear.

Both forms are public and both are documented. The table is what call sites use, because at a call site the module is context you already have and repeating it is noise:

if (EMBED_CALL(lane.has_zero, ScrutLaneCfg, .word = w) &&
!EMBED_CALL(cellul.eq, CatenaFinitaCfg, .src = a, .other = b, .cap = n, .ci = EMBED_FALSE))
{
EMBED_CALL(memor.cpy, MemoriaCfg, .dst = dst, .src = src, .bytes = n);
}
Arguments for the string calls, where each call reads only the members it needs.
const char *const src
Arguments for the memor calls.
void *const dst
Arguments for the lane calls.
const embed_word word

The library keeps the free functions because they are what a debugger, a linker map and a nm listing show you, and a namespace built out of macros would show nothing.

Naming the members also means an entry cannot be called with its arguments in the wrong order, and an entry that grows a member does not silently change the meaning of existing call sites.

How it is declared

Three pieces, in every module header:

typedef struct
{
mmgr_span (*from)(const SpatiumCfg *c);
mmgr_cspan (*cfrom)(const SpatiumCfg *c);
EMBED_TABLE_LAYOUT(SpatiumNs, from, cfrom);
EMBED_TABLE_STORAGE SpatiumNs spat EMBED_UNUSED = {.from = mmgr_spat_from, .cfrom = mmgr_spat_cfrom};
mmgr_cspan mmgr_spat_cfrom(const SpatiumCfg *args)
Builds a read span over args->cbuf, with pos at 0 and err clear.
Type of the spat dispatch table.
Definition spatium.h:100
A buffer being read: its bytes, how far the reader has gone, and whether it ran out.
Definition spatium.h:63

EMBED_TABLE_STORAGE is static const. EMBED_UNUSED lets an unreferenced table drop out of a translation unit that does not use it.

EMBED_TABLE_LAYOUT is the interesting part

The table is addressed by offset. A positional initializer mis-wires silently when a member is inserted, removed or moved: the code still compiles, and spat.from calls something else.

EMBED_TABLE_LAYOUT pins it. It expands to a chain of _Static_asserts checking that each named member sits at its own loculus, in the order given, and that sizeof the struct is exactly that many pointers. A member added and not listed, a member reordered, or padding appearing between them all fail at the declaration, at compile time.

That has one consequence for this documentation: the order of the members is the layout, and so the reference must not sort them alphabetically. SORT_MEMBER_DOCS is NO in docs/Doxyfile for that reason, and it is not a cosmetic setting: sorting MemoriaOperorNs would show loculus 0 as chr when it is cpy.

What the indirection costs

Nothing, when const is respected.

Measured: gcc devirtualizes a call through a static const <Mod>Ns down to the inlined body, identical instructions to calling the entry directly, and does not devirtualize the same call through a non-const table, which becomes a real call with an 88-byte frame. clang devirtualizes both. So the const in EMBED_TABLE_STORAGE is essential: it makes the two compilers agree.

Across module boundaries where the entry is not EMBED_INLINE, link-time optimization is what closes the gap. See Doing eight bytes at a time for the measurement.

Modules with more than one table

verbum_scrutor has three: lane, mask and word. The entries were one table until the count passed what EMBED_TABLE_LAYOUT accepts, and splitting them by what they operate on, a lane of a word, a mask of lanes, a whole word, reads better than one table of everything did.

endian is the other shape: two tables, parva_extremitas and magna_extremitas, over the same EndianNs type. The byte order is chosen by which table you call through and not by an argument.

The Latin, decoded lists every stem and the module it belongs to.