MMgr v1.0.0
Zero-heap memory manager in C11. One borrowed buffer, carved by asserted offset.
Loading...
Searching...
No Matches
Width, alignment and byte order

The three machine properties this library refuses to assume.

The two types everything is built on

embed_word embed_iword

They are separate by design. A word is what the scanner loads; an index is what addresses a region. On a 64-bit host they are 64 and 32 bits, and code that conflates them works there and breaks on the first machine where they differ. idx16 is the environment that exists to reach that case.

embed_types.h carries static asserts policing the combination: that an index can address the largest region the configuration allows, that the word is at least as wide as the index, that the fixed-width types are the widths they claim. They fire at compile time, in the build that got it wrong, naming the pairing.

Enums must keep their declared width

An enum that silently becomes an int moves every field after it in every struct containing one, and this library addresses borrows by offset. Every offset computed from such a struct is then wrong.

So every enum a struct holds is declared through EMBED_ENUM_BYTE, which gives it one byte on every compiler: the enum itself where the compiler can pin it to a byte, with : unsigned char or __attribute__((packed)), and its constants with an unsigned char name where it cannot, as on MSVC's C. A probe at the source asserts the width:

EMBED_STATIC_ASSERT(sizeof(EmbedEnumProbe) == 1,
"EMBED_ENUM_BYTE gave an enum more than one byte here, so no enum keeps its declared width ...");

It fails only where a compiler accepts the packed attribute and ignores it. On TI toolchains, pass --small_enum.

Three access strategies, not one

proximus_operor exposes three families that look interchangeable and are not:

infix strategy when
proxim unaligned the address may be anything
aequus aligned you know the alignment holds
migro may alias the pointer may alias another live pointer of a different type

Merging them is a miscompile the compiler cannot report. An aligned load emitted for an address that is not aligned is a fault on some machines and a silently wrong value on others; a load without the may-alias marking lets the optimizer reorder it against a store it genuinely conflicts with.

This is also why tools/dev_env/names.tsv keeps three infixes for one module and does not collapse them onto the stem: collapsing rawmemcpy's three onto one merged seven symbol pairs, including the aligned and unaligned load.

Byte order is stated, never inherited

endian.wr32be(p, v); /* big-endian, on every machine */
uint32_t v = endian.rd32le(p);

There is no endian.wr32host(). A wire format has a byte order; the host's order is an implementation detail of the host, and code that writes "native" order to a wire has a bug that only appears when the other end is different.

EMBED_BIG_ENDIAN derives from __BYTE_ORDER__ and exists so the library can pick the cheap path when the requested order happens to match the host, not so callers can ask for "whatever this machine does".

Alignment of an allocation

uint8_t *p = prison.work.persistent_buf_alloc(256, reader);

An allocation does not name an alignment, because there is only one. Every cell comes back aligned to MMGR_CARCER_ALIGN, the larger of sizeof(embed_word) and the alignment of the atomic size word, derived from the two types instead of written as a number. Every cell carries an atomic size word at its header and at its foot, and a grain narrower than that word would put the foot off its alignment. At a 16-bit or 32-bit word the grain is the atomic word's; at a 64-bit word the two are the same.

locus_carcerum.h asserts that MMGR_CARCER_ALIGN is a power of two, since an offset rounds by masking and masking lands on a multiple only for a power of two, and that MMGR_ALIGN_BYTES, the alignment a pool declaration puts on its storage, is a multiple of it. mmgr.h asserts that MMGR_ALIGN_BYTES is a power of two. All three stop the build.

The width costs bytes twice over. A request rounds up to a whole word before it is carved, and every cell carries three words beside its payload, each rounded to the same alignment: its size ahead of it, and behind it its owner and its tail, the cell's own offset. On a 64-bit build that is 24 bytes per cell; on a 16-bit build it is 6. A cellblock full of small cells pays that per cell, and in a 4 KB pool the width is worth thinking about for that reason.

What to do with all this

Build all five environments. The default build already does:

cmake -S . -B build -DMMGR_BUILD_TESTS=ON && cmake --build build && ctest --test-dir build

word16 reaches the scan tails four times sooner than host. idx16 reaches the width asserts. Between them they cover the two mistakes this page is about. See The environment matrix.

Note
The generated reference on this site is produced at one configuration, 64-bit word, 32-bit index, because Doxygen has to resolve embed_word to a single concrete type. The other environments differ only in these typedefs.