MMgr v1.0.0
Zero-heap memory manager in C11. One borrowed buffer, carved by asserted offset.
Loading...
Searching...
No Matches
Spatium: spans

A bounded view over memory the span does not own, with a cursor and a flag that latches.

When to reach for it

  • You are passing a buffer and a length to something and want them to travel together.
  • You are filling a fixed buffer through several steps and want to test once at the end.
  • You want the compiler to stop a buffer you may write from being handed where a read-only one belongs.

What it composes with

spat.from borrows whatever Locus carcerum: the region handed you. Byteio: bytes on a wire writes into that storage and reads back out of it; Verba scribo: the string builder and Cellularum laboro: bounded string work work against the same bytes.

Two types, deliberately

typedef struct {
uint8_t *buf;
size_t cap;
size_t pos;
embed_bool overflow;
typedef struct {
const uint8_t *buf;
size_t len;
size_t pos;
embed_bool err;
A buffer being read: its bytes, how far the reader has gone, and whether it ran out.
Definition spatium.h:63
A buffer being filled: its bytes, how far the writer has gone, and whether it ran out.
Definition spatium.h:47
mmgr_span mmgr_cspan
for filling reading
buf writable const
extent cap len
the flag overflow err

The extent is named differently in each deliberately. const alone would stop the write but let the two be confused wherever a name is read, and so the field names differ too, and a mix-up is a compile error and not a puzzle.

The two flags are not the same kind of thing

overflow is a build failure. What a writer emits and how big its buffer is are both settled before the build, and so the two are either compatible or the program is wrong. There is no runtime condition under which a correct writer overruns a correctly sized span. MMGR_ASSERT says so, nothing in a shipping build, an abort in checks. See How failure is reported.

The flag is what a shipping build does with a wrong program, not a path to design for. It latches so that the first bad append stores nothing and every append after it is a no-op, which keeps a wrong writer off the end of the buffer instead of walking down it. Read it to learn that something is broken, not to decide what to do next.

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 embed_bool and no append returns anything: a short read is a case to handle, an overrun append is a bug to fix.

Either way the flag is sticky. A caller walks a span through several steps and tests once at the end and not after each:

uint8_t buf[64];
mmgr_span w = EMBED_CALL(spat.from, SpatiumCfg, .buf = buf, .cap = sizeof buf);
EMBED_CALL(byteio.raw, OctetusCfg, .w = &w, .src = head, .bytes = sizeof head);
EMBED_CALL(byteio.put_be, OctetusCfg, .w = &w, .val = id, .bytes = 4u);
EMBED_CALL(byteio.raw, OctetusCfg, .w = &w, .src = body, .bytes = n);
/* One test, covering all three */
MMGR_ASSERT(EMBED_CALL(spat.ok, SpatiumCfg, .span = w), "frame does not fit buf");
#define MMGR_ASSERT(cond_, msg_)
Runtime assertion hook, inert here.
Definition mmgr.h:252
Arguments for the byteio calls, where each call reads only the members it needs.
const uint8_t *const src
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
const mmgr_span span
Definition spatium.h:81

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. spat.reset is the only call that clears a flag.

The ten entries

Two build one: from, cfrom. Two ask whether it is still usable: ok, cok. One asks whether it covers any bytes at all: has_storage. One rewinds: reset. Two narrow: after a count of bytes, and first so many. Two turn a filled span back into a readable one: produced for everything written, read for a named prefix of it.

There is no len and no room. The span is the caller's own value, and so s.pos and s.cap - s.pos are already in hand, and a call to fetch them would be a second way to write a member read.

Gotchas

A span travels inside the argument pack, not as a pointer to one. SpatiumCfg holds an mmgr_span in .span and an mmgr_cspan in .cspan. A walk reads a copy and cannot change the span you gave it. reset is the exception and takes .at, a pointer, because it is the single one entry that must change one.

from takes .buf and cfrom takes .cbuf. They are separate members deliberately: a const uint8_t * cannot reach the fill constructor. A buffer you may not write cannot be turned into a span you may write through.

.cap carries the extent for both constructors, and .count the byte count for after, first and read. One member per job and not one per entry.

after and first return a new span; they do not move the one you passed. Assign the result.

A .count of exactly cap is not a failure. It gives an empty span that has not failed: nothing is left, and nothing went wrong either. Past cap is the failure.

produced carries the fill span's overflow across as the read span's err. Reading back a span that overran does not launder the failure.

pos keeps counting past cap once a span has overrun. So the number left behind says how far past the end a wrong writer went, which is worth reading in a post-mortem. It is not a sizing pass: a length you have to measure by overrunning a buffer at run time is a length you already knew when you wrote the code, and the assert fires long before you get to read it.

A span does not know when its target dies. It holds a pointer the caller gave it. Returning a span over temporary-tier storage from a function that gives the cell back is a use-after-free, and no field in the span can tell you.

Spans are values. Copy them, pass them, return them. Nothing in the library holds a pointer to one.

Reference

Generated reference for spatium ยท Who owns what for the lifetime rules