How one borrowed buffer becomes everything else in the library.
There is exactly one source of storage: a buffer the caller already owns. MMgr never asks the system for memory and never gives any back, because it was never holding any.
Five things, in the order a byte meets them. The first four are one path:
A pool is declared at compile time; a cellblock is dressed over it; the cellblock hands out a cell and returns its first byte; a span bounds that cell; and the text, scan and copy calls work against the span. Rings are the fifth, and sit alongside that line instead of on it.
ParsMemoriaeInternae and ParsMemoriaeExternum declare the storage and its alignment. LocusCarcerum is a declaration too and never a call: it emits each cellblock's state and the entries bound to it, and nothing runs at startup. From then on a cellblock hands out storage from two ends of its pool's bytes:
They grow toward each other out of one gap, and both allocations test it. carcer_grow compares the header plus the payload against the gap and returns NULL without moving a boundary when it will not fit, and both persistent_buf_alloc and temporary_buf_alloc can answer no and every call site has to test the return. buf_available reports the gap still between them. It is not a release; it answers how much is left.
persistent_buf_release takes the cell itself, void (*persistent_buf_release)(void *prisoner, const CellOwner *owner), because the block's own word carries its size, and the release does not have to be told. A block freed mid-chain stays in the chain until a release reaches the boundary: the bottom end does not recover on every free. That is the trade the whole library is built on: giving up free-anything-anytime makes the footprint decidable.
Interim is the same algorithm as persist with the arithmetic mirrored: its base is the pool's size, its boundary the top, and it grows down.
A take bumps the top down. A release frees the cell's header and, where the cell stood at the top, trims the top back up over the free cells there. A take the gap cannot meet walks the tier alone: every run of free cells merged into one, the fitting run nearest the pool's size taken, the free run at the top returned to the gap. One release serves both tiers, since a cell's offset says which tier it lies in.
Nothing is reallocated and nothing moves: buf still points at readable memory after its release. It is dead all the same, and the library cannot tell you that you kept it.
A cell is what a pool hands out. Every one is taken with the pool's own persistent_buf_alloc and given back with its own persistent_buf_release. There is one release and never two. What differs is which guard the pool was declared under:
| MMGR_MINIMUM_SECURITY | MMGR_MAXIMUM_SECURITY | |
|---|---|---|
| gives the bytes back | yes | yes |
| clears them first | no | yes (locus_carcerum.h:140) |
| costs | a chain walk | a chain walk and a pass over the bytes |
The guarantee is in the declaration, never in a flag or a second call. A caller cannot ask for a wipe and not get one, and cannot reach for an unwiped release on a pool that promised one. The extent cleared is the block's own, read from its header. A caller cannot under-wipe a cell by naming fewer bytes than it holds.
Two guards exist instead of one that always clears, because the clear costs a pass over the bytes and most cells hold nothing worth paying it for. Which storage is which is settled by declaring two pools and handing secrets to the maximum-security cellblock. Each one's extent is the count in its own pool declaration, and no build knob sizes it.
Bytes are cleared on release, not on hand-out. An allocation does not return zeroed storage: a cell released from a minimum-security cellblock and handed out again carries what the last prisoner left. Declaring the sensitive pool under MMGR_MAXIMUM_SECURITY buys the zeroing.
mmgr_zero_buf clears an address and a count in place without giving anything back. Its stores are volatile machine-width stores: a plain store there is a dead store the optimizer is entitled to drop, and a byte loop would pay eight times the stores for the same guarantee. Byte edges cover a length or an address that is not a whole number of words.
A span is a pointer, an extent, a position and a sticky flag. spat.from borrows; it does not allocate, and the span dies with the buffer it was given.
There are two, and they are different types by design:
| mmgr_span | mmgr_cspan | |
|---|---|---|
| for | filling | reading |
| buf | writable | const |
| extent | cap | len |
| the flag | overflow | err |
Naming the extent differently in each stops one being handed where the other belongs without the compiler saying so.
The flag is sticky, and the stickiness is the gain of a span over a bare pointer and length. A caller may append a whole message through several calls and test once at the end in place of after each. spat.reset alone clears the flag.
The two flags do not mean the same kind of thing. overflow is a build failure: what a writer emits and how big its buffer is are both fixed before the build: there is no runtime condition under which a correct writer overruns a correctly sized span. It asserts, nothing in a shipping build, an abort in checks, and the latch is what a shipping build does with a wrong program, keeping it from running off the end in place of offering it somewhere to go.
err is a runtime fact: a read span runs out because whatever sent the bytes sent fewer, and nothing was built wrong. For that reason every take answers and no append returns anything: a short read is a case to handle, an overrun append is a bug to fix. A take that reaches past the end leaves the cursor where it was. A caller that keeps reading after a failure still knows where it is.
A read is a buffer, how far it may go, and where it is. Those are the members Cellularum laboro: bounded string work names src, cap and at. A struct holding the three added a second name and no information.
The cellblock is shared by every context that reaches it, on atomic words and with no lock, and memoria_anularis is concurrent in one shape: single producer, single consumer. Exactly one producer advances head and exactly one consumer advances tail: ordering is all that is needed, and every atomic access goes through the module's own MMGR_ATOMIC_LOAD and MMGR_ATOMIC_STORE, acquire and release, and no entry on those two takes a lock or a read-modify-write.
It offers three things over the same bytes: a byte ring, a segment view that hands out whole segments by index instead of copying them, and loculi, numbered holds that record a region to keep out of, for a reader walking bytes in place.
The ring's size is yours to pick. cap is the bytes in the buffer you hand mmgr_anular_init, and any non-zero power of two is accepted, because the ring wraps by masking. nsegs is a power of two at most cap.
What the word width bounds is the loculi and never the segments: their free and busy masks are one machine word each, and MMGR_RING_LOCULI_MAX is EMBED_WORD_BITS, 64 on a 64-bit build, 16 on a 16-bit one. MMGR_RING_LOCULI is a build knob under that ceiling, and a static assert names it if it is set higher. A build with no use for the loculus view sets it to 0 and gets the keepout storage back.
The caller declares the ring as an mmgr_ring and supplies the bytes. Everything else, the two cursors, the segment counters, the masks and the keepout records, lives inside that storage and is declared nowhere a consumer can reach.
| Thing | Allocates | Frees | Lifetime |
|---|---|---|---|
| caller's buffer | the caller | the caller | outlives everything below |
| pool | nothing | nothing | the region's |
| persist take | a block from the middle | persistent_buf_release by address | as long as it likes |
| interim take | a block from the middle | persistent_buf_release by address | one call |
| span | nothing | nothing | its target's |
| ring segment | a counter step | seg_release | until released |
| loculus | a bit in a mask | loculus_drop | until dropped |
Read the third column. Nothing in MMgr reaches an allocator: every take comes out of a region the caller declared, and every free either returns a block to that region's own chain, moves a boundary, or clears a bit.
The persistent end is the exception to "nothing is ever really freed": a release there frees the block, and a block freed at the end comes off it, along with every freed block below it in turn. A block freed under a busy one waits in the tier until that one goes. The interim end is not: nothing is released one at a time, and the whole run comes back at once.
Buys. The footprint is a compile-time number. There is no fragmentation, because there is no general free. There is no allocation failure at an arbitrary point, because every take is against a region whose size you chose. Worst-case timing is a pointer bump.
Costs. You must size it yourself, up front. Get it wrong and the take returns NULL: the region fails closed in place of letting the two ends run into each other, but no allocator quietly finds more. The counters exist for exactly this: run the real workload under the checks environment, read them, then size the region.
mmgr_series_cellularum_persistent_end(pool) is how far the bottom has reached and mmgr_series_cellularum_temporary_top(pool) how far the top has, both read off the one atomic word that holds the two boundaries. buf_available reports the gap between them.
For the peak in place of the current value, turn on MMGR_ENABLE_HW_MEM_CAPACITY_CB. Each end then keeps the largest it has seen in its own field, persistent_hw records the persistent end, temporary_hw records the bytes taken from the top, one per end: neither is a maximum over the other. It is off by default, and there is no entry that returns either: read the field.
See Why there is no malloc for the argument, Who owns what for the lifetime rules in detail, and Every compile-time knob for the knobs that set the sizes.