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 to writer (if given). With no writer the encoder is an in-memory buffer — read it with getvalue().

  • Encoder.over_buffer(buf, offset, flush) — Rust/C/Java-style. Writes into a fixed caller buffer, reserving offset bytes at the front for a lower-layer header, draining via the flush sink 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: object

Encodes 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, reserving offset bytes at the front for a lower-layer header. When the buffer fills, the encoder calls flush with the bytes written so far; flush is expected to drain them and (via buffer_set()) hand back a fresh buffer so encoding continues. With no flush sink a full buffer raises SofaBufferError. Pass sticky=True to latch the first error instead of raising per call (inspect it via error).

Parameters:
  • buffer (bytearray)

  • offset (int)

  • flush (Callable[[bytes], None] | None)

  • sticky (bool)

Return type:

Encoder

buffer_set(buffer, offset=0)[source]

Install a new fixed output buffer mid-stream.

Mirrors C sofab_ostream_buffer_set / Rust buffer_set / Java bufferSet: typically called from inside the flush sink to hand the encoder a fresh buffer so encoding continues without interruption. offset bytes are reserved at the front (e.g. for a framing header).

Parameters:
  • buffer (bytearray)

  • offset (int)

Return type:

None

property error: SofaError | None

The first error recorded in sticky mode, or 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

getvalue()[source]

Return the accumulated bytes (in-memory writer model only).

Return type:

bytes

write_unsigned(field_id, value)[source]

Write an unsigned integer field as a base-128 varint.

value must be in 0..UNSIGNED_MAX (64-bit), else SofaRangeError.

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.

value must be in SIGNED_MIN..SIGNED_MAX (64-bit), else SofaRangeError.

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 no errors=). Python str is a Unicode string type, so per CORELIB_PLAN §6.4 it is always strict: SOFAB_STRICT_UTF8 is a no-op for it and is omitted entirely (documented as always-ON). A str that cannot be encoded as valid UTF-8 — a lone/unpaired surrogate such as '\ud800' — is refused with SofaRangeError (the encode-side InvalidArgument outcome, MESSAGE_SPEC §8 producer-side MUST NOT), never silently replaced. Embedded U+0000 is 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_MAX and every value in 0..UNSIGNED_MAX, else SofaRangeError. 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_MAX and every value in SIGNED_MIN..SIGNED_MAX, else SofaRangeError. 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, else SofaRangeError. A zero-count array emits [header][count=0][fixlen_word] — the fixlen_word is 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, else SofaRangeError. A zero-count array emits [header][count=0][fixlen_word] — the fixlen_word is 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 than sofab.MAX_DEPTH (255), raising SofaRangeError.

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/union field, and an array field whose declared default is the empty collection (MESSAGE_SPEC §2). Where the frame must be visible, close with write_sequence_end_keep() instead.

Raises SofaStateError if 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 SofaStateError if no sequence is currently open.

Return type:

None