Byte and wire serialization over spans.
Building or parsing a binary frame where the layout is fixed and the byte order is part of the spec.
Six entries over Spatium: spans's two span types. Three append into a span being filled: put for one byte, put_be for a value big end first, raw for a run as it stands. Three read out of one: take_be for a value, rd_str for a length-prefixed run, mpint_fixed for an integer right-aligned into a fixed field.
Fields sit bytes apart, and the span carries the cursor, and so nothing here needs an offset passed in.
The appends answer nothing and the takes all answer. This is the complete failure model. An append that does not fit is a build failure: what a writer emits and how big its buffer is are both fixed before the build, and so it asserts, stores nothing, and latches the span's overflow to keep a wrong program off the end. A short read is a runtime fact about whatever sent the bytes, and so it sets err, leaves the cursor alone and returns EMBED_FALSE for a caller to act on. See How failure is reported.
A value moves a word at a time, not a byte at a time. put_be reverses once through Endian: stated byte order and then stores at the widest step the count allows: eight bytes is one store, seven is three, and only an odd final byte is ever written alone. take_be is the mirror.
Neither access needs to be aligned. They reach Proximus operor: load and store's unaligned entries. A field may start anywhere in the span. The cost does vary with the field width, because the number of stores does.
rd_str copies nothing. It points blob into the span's own bytes, and so what it hands back lives exactly as long as the buffer does. A length that is read but not followed by its payload puts the cursor back where it started: a partial read is not a read.
mpint_fixed writes the field whole instead of appending to it. It zero-fills ahead of the value and leaves the cursor at cap. Leading zero bytes of the integer are skipped before the width is tested. A value carrying a sign byte still fits a field of its own size.
Bit-level output for formats that are not byte-aligned.
The writer is yours to hold and the cfg points at it. init fills it in; every put reads and updates it through writer.
align is how a stream ends, and leaving it out loses the tail. put writes whole bytes only, and bits that do not fill one stay in residue and are never stored on their own. Any stream whose length is not a multiple of eight needs align before its buffer is read. It pads with zeros above the bits it holds, and does nothing when the residue is empty, and so calling it on a stream that happened to end on a byte, or calling it twice, costs nothing.
Bits pack from the low end. A 3-bit put of 0b101 then a 5-bit put of zero produces 0x05, not 0xA0.
The writer holds a partial byte. cnt counts whole bytes written; anything short of a byte sits in residue and goes out when a later put completes it. A byte-level write to the same buffer in between lands in the wrong place.
overflow latches. A put whose completed bytes would run past cap writes nothing, sets the flag and clears the residue, and nothing clears it again, and so one check after a run of puts covers the whole run. As with a span, reaching it means the buffer was sized wrong for a stream whose length was known before the build.
This is a writer only. There is no bit reader; a format that needs one reads bytes through Byteio: bytes on a wire and shifts them itself.
Explicit reads and writes. Two namespaces over three entries each, and the namespace is the byte order:
parva_extremitas is little end first, magna_extremitas is big. The width is an argument instead of part of the entry name, and mmgr_endian_width's values are the byte counts, and so the same number advances your cursor.
There is no host variant, deliberately. 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 appears the first time the other end is a different machine.
EMBED_BIG_ENDIAN exists so the library can take the cheap path when the requested order happens to match the host. It is not there for a caller to ask for whatever the machine does.
Generated reference ยท Width, alignment and byte order
Getting at the parts of a double without <math.h> and without a heap.
to_bits and from_bits are the two directions of the same union, and merge returns a bit pattern, not a double: put it through from_bits to get the value back.
The MMGR_DBL_* constants, masks, shifts, the bias, are exposed too. A caller doing something the entries do not cover is not forced to re-derive them.
Formatting a float without printf, as Verba scribo: the string builder's g and fixed entries do. Classifying a value as infinite or NaN without a library call. Anything that needs the fields instead of the number.
This is double, not float. There are no single-precision entries.
It is bit manipulation and not arithmetic. merge will happily build a NaN or a denormal from the fields you hand it. It masks each one to its width instead of rejecting it. A value too large for its field loses the high bits silently.