sofab.encoder module¶
SofaBuffers encoder (OStream equivalent).
Two construction models, mirroring the ecosystem:
Encoder(writer)/Encoder()— Go-style. Bytes accumulate in an internal buffer;flush()drains them towriter(if given). With no writer the encoder is an in-memory buffer — read it withgetvalue().Encoder.over_buffer(buf, offset, flush)— Rust/C/Java-style. Writes into a fixed caller buffer, reservingoffsetbytes at the front for a lower-layer header, draining via theflushsink when full.
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 grows on demand, so the hold-back reaches
the full sofab.MAX_DEPTH and every depth is canonical (CORELIB_PLAN §6 —
only a heap-free profile may bound the run and frame eagerly beyond the bound).
It is allocated on the first hold-back, so an encoder that never opens a sequence
never pays for it.
- 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).- 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).- Parameters:
buffer (bytearray)
offset (int)
- Return type:
None
- bytes_used()[source]¶
Bytes written to the current buffer since construction/last flush.
- Return type:
int
- flush()[source]¶
Drain buffered bytes to the writer / flush sink; return the count.
- Return type:
int
- write_unsigned(field_id, value)[source]¶
Write an unsigned integer field as a base-128 varint.
valuemust be in0..UNSIGNED_MAX(64-bit), elseSofaRangeError.- Parameters:
field_id (int)
value (int)
- Return type:
None
- write_signed(field_id, value)[source]¶
Write a signed integer field, ZigZag-encoded into a varint.
valuemust be inSIGNED_MIN..SIGNED_MAX(64-bit), elseSofaRangeError.- Parameters:
field_id (int)
value (int)
- Return type:
None
- write_bool(field_id, value)[source]¶
Write a boolean as an unsigned field (
1/0).- Parameters:
field_id (int)
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 (int)
value (float)
- 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 (int)
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 withSofaRangeError(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 (int)
text (str)
- Return type:
None
- write_bytes(field_id, data)[source]¶
Write a raw byte blob as a fixlen field (BLOB subtype).
- Parameters:
field_id (int)
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 value in0..UNSIGNED_MAX, elseSofaRangeError. A zero-count array is a valid, fully-specified empty array on the wire ([header][count=0]).- Parameters:
field_id (int)
values (Iterable[int])
- 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 value inSIGNED_MIN..SIGNED_MAX, elseSofaRangeError. A zero-count array is a valid, fully-specified empty array ([header][count=0]).- Parameters:
field_id (int)
values (Iterable[int])
- 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, elseSofaRangeError. 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 (int)
values (Iterable[float])
- 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, elseSofaRangeError. 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 (int)
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), raisingSofaRangeError.- Parameters:
field_id (int)
- 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
SofaStateErrorif 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
SofaStateErrorif no sequence is currently open.- Return type:
None