sofab.encoder module¶
SofaBuffers encoder (OStream equivalent).
One buffer-ownership model (CORELIB_PLAN §5.1): the encoder writes into a fixed output buffer and drains it through a flush sink when that buffer fills. It never grows or reallocates the buffer it writes into — “what was handed over is what gets written”. Three construction shapes express that one model:
Encoder.over_buffer(buf, offset, flush)— the primitive, and the only one that takes a caller-supplied buffer. Writes intobuf, reservingoffsetbytes at the front for a lower-layer header, draining via theflushsink when full; without a sink a full buffer reportsSofaBufferError, which is the shape a caller sizes from a generatedMAX_SIZE.Encoder(writer)— the same primitive over a scratch buffer of_SCRATCH_SIZEbytes installed with a sink that forwards towriter.write. This is §5.1’s “unbounded schema” shape, so a message of any size streams out through bounded memory as it is written, rather than accumulating untilEncoder.flush().Encoder()— the same again, with the sink appending into the result the encoder hands back fromEncoder.getvalue(). What grows there is the message being returned, not a buffer the encoder writes into.
The scratch buffer of the latter two is one allocation, made once at
construction and never resized; §5.1 puts even that in the generated layer,
which knows the schema — so generated code that can bound its message should
prefer over_buffer with a MAX_SIZE-sized buffer and no sink.
Sequences are framed lazily: Encoder.write_sequence_begin_lazy() holds
the header back until the sequence receives content, so a sequence-typed field
whose value equals its declared default is omitted rather than emitted as an
empty begin/end frame (MESSAGE_SPEC §2). The closer picks the outcome —
Encoder.write_sequence_end() drops a contentless sequence,
Encoder.write_sequence_end_keep() forces the frame out (wrapper-array
elements, explicit empty arrays). Held-back ids are encoder state, never buffer
content, so a flush cannot split a pending run by construction: a pending header
occupies no buffer space, and the buffer only fills through a write — which
commits the whole run before its first byte goes out. A tiny output buffer
therefore produces exactly the one-shot bytes.
The run itself has no fixed window: it is sofab.MAX_DEPTH slots wide, so
the hold-back reaches the full depth and every depth is canonical (CORELIB_PLAN
§6.0.1 — only a heap-free profile may bound the run and frame eagerly beyond the
bound). It is sized at construction and never grows, which §6.6 requires of every
piece of the codec’s bounded working state.
- class sofab.encoder.Encoder(writer=None, *, sticky=False)[source]¶
Bases:
objectEncodes SofaBuffers fields to a byte stream.
- Parameters:
writer (Writer | None)
sticky (bool)
- classmethod over_buffer(buffer, offset=0, flush=None, *, sticky=False)[source]¶
Create an encoder that writes into a fixed caller-owned buffer.
Rust/C/Java-style construction: bytes are written directly into
buffer, reservingoffsetbytes at the front for a lower-layer header. When the buffer fills, the encoder callsflushwith the bytes written so far;flushis expected to drain them and (viabuffer_set()) hand back a fresh buffer so encoding continues. With noflushsink a full buffer raisesSofaBufferError. Passsticky=Trueto latch the first error instead of raising per call (inspect it viaerror).With a
flushsink the buffer must leave at leastMIN_OUTPUT_BUFFERbytes pastoffset; without one there is no minimum. Seebuffer_set(), which enforces both.- Parameters:
buffer (bytearray)
offset (int)
flush (Callable[[bytes], None] | None)
sticky (bool)
- Return type:
- buffer_set(buffer, offset=0)[source]¶
Install a new fixed output buffer mid-stream.
Mirrors C
sofab_ostream_buffer_set/ Rustbuffer_set/ JavabufferSet: typically called from inside the flush sink to hand the encoder a fresh buffer so encoding continues without interruption.offsetbytes are reserved at the front (e.g. for a framing header).The offset belongs to this installation, not to the buffer (CORELIB_PLAN §5.1): the cursor starts at
offsetand the offset is then consumed, so a later flush the sink returns from without installing anything resumes at 0. Re-installing — even the same buffer — is what re-arms the reservation, which is how a sink gets fresh header room in every flushed unit rather than only in the first.MIN_OUTPUT_BUFFERbinds here, and only for a buffer that is installed with a flush sink:len(buffer) - offsetmust be at least that many bytes, checked at installation and at every mid-stream set, so an unusable buffer is refused where it is handed over rather than partway through a message. Without a sink no flush can occur and no minimum applies — the buffer holds the message or reportsSofaBufferError— which is what keeps a caller sizing from a generatedMAX_SIZEexact, down to a zero-byte remainder.- Parameters:
buffer (bytearray)
offset (int)
- Return type:
None
- bytes_used()[source]¶
Bytes standing in the output buffer, i.e. written since it was installed and not yet drained.
The buffer is fixed, so this never exceeds its size — for the convenience models,
_SCRATCH_SIZE. It is not the length of the message: bytes already drained to the writer/sink are no longer here.- Return type:
int
- flush()[source]¶
Drain buffered bytes to the writer / flush sink; return the count.
- Return type:
int
- getvalue()[source]¶
Return the encoded message (in-memory model only).
Only
Encoder()retains one: with a writer the bytes have already been handed over, and withover_buffer()they are in the caller’s buffer — returning the undrained tail of either would be partial output dressed up as a whole message (CORELIB_PLAN §5.1), so both raiseSofaArgumentError.- Return type:
bytes
- write_unsigned(field_id, value)[source]¶
Write an unsigned integer field as a base-128 varint.
valuemust be an integer in0..UNSIGNED_MAX(64-bit), elseSofaArgumentError. “Integer” is Python’s own rule — anything with__index__(int,bool,IntEnum, NumPy integers). Afloatis refused rather than truncated,3.0included; writeint(x)if that is what you mean.- Parameters:
field_id (SupportsIndex)
value (SupportsIndex)
- Return type:
None
- write_signed(field_id, value)[source]¶
Write a signed integer field, ZigZag-encoded into a varint.
valuemust be an integer inSIGNED_MIN..SIGNED_MAX(64-bit), elseSofaArgumentError— seewrite_unsigned()for what counts as an integer.- Parameters:
field_id (SupportsIndex)
value (SupportsIndex)
- Return type:
None
- write_bool(field_id, value)[source]¶
Write a boolean as an unsigned field (
1/0).- Parameters:
field_id (SupportsIndex)
value (bool)
- Return type:
None
- write_float32(field_id, value)[source]¶
Write a 32-bit IEEE-754 float as a little-endian fixlen field.
- Parameters:
field_id (SupportsIndex)
value (float)
- Return type:
None
- write_float32_bits(field_id, bits)[source]¶
Write a 32-bit float from its raw wire bits, verbatim.
bitsis an unsigned 32-bit integer — the little-endian payload as it lay on the wire, which is whatsofab.Visitor.on_float32_bits()hands back. The four bytes go out unchanged: no float is constructed and no conversion happens, so nothing can quiet a signaling NaN on the way.This is CORELIB_PLAN §6.5’s other half. A double-only target “MUST NOT re-encode an
fp32from the widened value”, because the IEEE widening a Pythonfloatperforms sets the quiet bit and destroys a signaling NaN’s payload. A transcoder, a round-trip or any re-encode pairs this withon_float32_bits; a producer that has a value rather than bytes writeswrite_float32()as before.Out of
0..0xFFFFFFFFisSofaArgumentError(§6.3).- Parameters:
field_id (SupportsIndex)
bits (SupportsIndex)
- Return type:
None
- write_float64(field_id, value)[source]¶
Write a 64-bit IEEE-754 float as a little-endian fixlen field.
- Parameters:
field_id (SupportsIndex)
value (float)
- Return type:
None
- write_string(field_id, text)[source]¶
Write a UTF-8 string as a fixlen field (STRING subtype).
Encoding is strict UTF-8 (
str.encode("utf-8")with noerrors=). Pythonstris a Unicode string type, so per CORELIB_PLAN §6.4 it is always strict:SOFAB_STRICT_UTF8is a no-op for it and is omitted entirely (documented as always-ON). Astrthat cannot be encoded as valid UTF-8 — a lone/unpaired surrogate such as'\ud800'— is refused withSofaArgumentError(the encode-sideInvalidArgumentoutcome, MESSAGE_SPEC §8 producer-side MUST NOT), never silently replaced. EmbeddedU+0000is valid UTF-8 and round-trips unchanged.- Parameters:
field_id (SupportsIndex)
text (str)
- Return type:
None
- write_bytes(field_id, data)[source]¶
Write a raw byte blob as a fixlen field (BLOB subtype).
A blob longer than
sofab.FIXLEN_MAXis refused withSofaArgumentError(see_write_fixlen()) — on the declared length, before the copy, so an oversized payload is never duplicated just to be rejected.- Parameters:
field_id (SupportsIndex)
data (bytes | bytearray | memoryview)
- Return type:
None
- write_unsigned_array(field_id, values)[source]¶
Write an array of unsigned integers, each as a varint.
The element count must be
0..ARRAY_MAXand every element an integer in0..UNSIGNED_MAX, elseSofaArgumentError(seewrite_unsigned()for what counts as an integer). A zero-count array is a valid, fully-specified empty array on the wire ([header][count=0]).- Parameters:
field_id (SupportsIndex)
values (Iterable[SupportsIndex])
- Return type:
None
- write_signed_array(field_id, values)[source]¶
Write an array of signed integers, each ZigZag-encoded into a varint.
The element count must be
0..ARRAY_MAXand every element an integer inSIGNED_MIN..SIGNED_MAX, elseSofaArgumentError(seewrite_unsigned()for what counts as an integer). A zero-count array is a valid, fully-specified empty array ([header][count=0]).- Parameters:
field_id (SupportsIndex)
values (Iterable[SupportsIndex])
- Return type:
None
- write_bool_array(field_id, values)[source]¶
Write an array of booleans, each as the varint
1or0.The array half of
write_bool(), and canonical for the same reason (§4.4): an element is tested for truth andtruegoes out as1, so a re-encode of a tolerantly decoded array is the canonical form of it. On the wire this is an array of unsigned — booleans have no wire type of their own — so a zero-count array is the same empty arraywrite_unsigned_array()writes.Any object may be an element: it is tested for truth exactly as
ifwould test it, which is whatwrite_bool()does for a scalar. The test runs per element, inside the loop rather than up front, so a__bool__that raises leaves the same partial write the other array writers leave — the native engine reaches that state too, and the two have to be indistinguishable.- Parameters:
field_id (SupportsIndex)
values (Iterable[object])
- Return type:
None
- write_float32_array(field_id, values)[source]¶
Write an array of 32-bit floats as a packed little-endian fixlen array.
The element count must be
0..ARRAY_MAX, elseSofaArgumentError. A zero-count array emits[header][count=0][fixlen_word]— thefixlen_wordis always present (so empty fp32/fp64 arrays stay distinguishable) but there is no payload (§4.8).- Parameters:
field_id (SupportsIndex)
values (Iterable[float])
- Return type:
None
- write_float32_array_bits(field_id, payload, count=None)[source]¶
Write an
fp32array from its raw wire bytes, verbatim.payloadis any bytes-like object holding the array’s little-endian payload —4 * countbytes, which is whatsofab.Visitor.on_float32_array_bits()hands back. Passcountto state it, or leave it out and it islen(payload) // 4; a length that is not a multiple of four isSofaArgumentError.The array half of
write_float32_bits(), and for the same reason: §6.5’s bit-exactness requirement is stated over “everyfp32position … and each element of anfp32array”.- Parameters:
field_id (SupportsIndex)
payload (Any)
count (int | None)
- Return type:
None
- write_float64_array(field_id, values)[source]¶
Write an array of 64-bit floats as a packed little-endian fixlen array.
The element count must be
0..ARRAY_MAX, elseSofaArgumentError. A zero-count array emits[header][count=0][fixlen_word]— thefixlen_wordis always present (so empty fp32/fp64 arrays stay distinguishable) but there is no payload (§4.8).- Parameters:
field_id (SupportsIndex)
values (Iterable[float])
- Return type:
None
- write_sequence_begin_lazy(field_id)[source]¶
Open a nested sequence (sub-message) under
field_id, holding its header back until the sequence turns out to have content.MESSAGE_SPEC §2 omits a sequence-typed field whose value equals its declared default, and “not one child was written” is exactly that condition — evaluated per child field, recursively, for free, because the message layer already omits every child equal to its own default. A sequence closed with nothing in it therefore emits nothing instead of a two-byte empty frame, and an all-default message becomes the empty byte string. The predicate is never a byte image of the object, so in-memory padding cannot influence it.
This is the only way to open a sequence. How it closes decides whether a contentless one survives:
write_sequence_end()drops it,write_sequence_end_keep()forces the frame out.Must be balanced by a later
write_sequence_end()/write_sequence_end_keep(). Refuses to open a sequence nested deeper thansofab.MAX_DEPTH(255), raisingSofaArgumentError.- Parameters:
field_id (SupportsIndex)
- Return type:
None
- write_sequence_end()[source]¶
Close the innermost open sequence, letting it vanish if it received no content.
Use it wherever absence encodes the same value as an empty frame: a
struct/unionfield, and an array field whose declareddefaultis the empty collection (MESSAGE_SPEC §2). Where the frame must be visible, close withwrite_sequence_end_keep()instead.Raises
SofaArgumentErrorif no sequence is currently open.- Return type:
None
- write_sequence_end_keep()[source]¶
Close the innermost open sequence, keeping its frame even when it received no content.
Behaves like a write: it first emits any held-back headers — this frame’s and every enclosing one’s — and then the end marker, so an empty sequence reaches the wire as
begin+end.Required wherever the frame carries information beyond its contents:
a wrapper-array element (
struct/union/nested row): element presence is what carries a dynamic array’s length — highest present id + 1 (MESSAGE_SPEC §5.1) — so dropping an all-default element would change the decoded length, not just the bytes;an array field already known to differ from a non-empty declared default: absence would reconstruct that default, so the empty frame is the only encoding of “explicitly empty” (§2, §3).
The two failure directions are not symmetric, which is why this is the safe choice when in doubt: using it where
write_sequence_end()would do costs one non-canonical empty frame that every decoder normalizes away, while the reverse silently changes an array’s length.Raises
SofaArgumentErrorif no sequence is currently open.- Return type:
None