sofab package¶
Submodules¶
- sofab.binding module
KIND_TAGEntryBindingBinding.entriesBinding.closedBinding.words_requiredBinding.objects_requiredBinding.tree_words_requiredBinding.tree_objects_requiredBinding.freeze()Binding.unsigned()Binding.signed()Binding.boolean()Binding.float32()Binding.float64()Binding.string()Binding.bytes()Binding.string_into()Binding.blob_into()Binding.unsigned_array()Binding.signed_array()Binding.boolean_array()Binding.float32_array()Binding.float64_array()Binding.sequence()
- sofab.collectors module
- sofab.decoder module
- sofab.encoder module
EncoderEncoder.over_buffer()Encoder.buffer_set()Encoder.errorEncoder.bytes_used()Encoder.flush()Encoder.getvalue()Encoder.write_unsigned()Encoder.write_signed()Encoder.write_bool()Encoder.write_float32()Encoder.write_float32_bits()Encoder.write_float64()Encoder.write_string()Encoder.write_bytes()Encoder.write_unsigned_array()Encoder.write_signed_array()Encoder.write_bool_array()Encoder.write_float32_array()Encoder.write_float32_array_bits()Encoder.write_float64_array()Encoder.write_sequence_begin_lazy()Encoder.write_sequence_end()Encoder.write_sequence_end_keep()
- sofab.types module
- sofab.visitor module
VisitorVisitor.destinations()Visitor.on_field()Visitor.on_schema_bound()Visitor.on_sequence_begin()Visitor.on_sequence_end()Visitor.on_array_begin()Visitor.on_blob_begin()Visitor.on_string_begin()Visitor.on_float_array_begin()Visitor.on_unsigned()Visitor.on_signed()Visitor.on_float32()Visitor.on_float32_bits()Visitor.on_float64()Visitor.on_string()Visitor.on_bytes()Visitor.on_unsigned_array()Visitor.on_signed_array()Visitor.on_float32_array()Visitor.on_float32_array_bits()Visitor.on_float64_array()
Module contents¶
SofaBuffers — runtime for the SofaBuffers binary wire format.
Byte-for-byte compatible with the C/C++/Rust/Go/Java/C# core libraries. Import
the Encoder and Decoder and the wire-format types from here.
Encoder / Decoder resolve to the compiled native accelerator
(sofab._speedups, built by Cython) when it is available, and to the
pure-Python implementations otherwise — the two are byte-for-byte interchangeable
(see IMPL).
- class sofab.Encoder¶
Bases:
objectNative encoder — see
sofab.encoder.Encoderfor the full contract.One buffer-ownership model, byte-identical to the pure-Python encoder: the encoder writes into a fixed buffer and drains it through a flush sink, and never grows a buffer (CORELIB_PLAN S5.1).
Encoder.over_buffer(buffer, offset, flush)— the primitive: writes into a caller-ownedbytearray, draining throughflushwhen it fills.Encoder(writer=None, sticky=False)— the same over a scratch buffer of_SCRATCH_SIZEbytes installed with a sink, which forwards towriter.writeor, with no writer, appends into the resultgetvalue()hands back.
- buffer_set()¶
- bytes_used()¶
- error¶
- flush()¶
- getvalue()¶
- classmethod over_buffer()¶
- write_bool()¶
- write_bool_array()¶
- write_bytes()¶
- write_float32()¶
- write_float32_array()¶
- write_float32_array_bits()¶
- write_float32_bits()¶
- write_float64()¶
- write_float64_array()¶
- write_sequence_begin_lazy()¶
- write_sequence_end()¶
- write_sequence_end_keep()¶
- write_signed()¶
- write_signed_array()¶
- write_string()¶
- write_unsigned()¶
- write_unsigned_array()¶
- class sofab.Decoder¶
Bases:
objectNative push decoder — see
sofab.decoder.Decoderfor the contract.Bytes go in through
feedand fields come out at avisitoror abinding. Incoming bytes are held in one contiguous buffer and parsed by advancing a C cursor with direct pointer indexing; a construct that runs off the end suspends and resumes from its first byte on the nextfeed, so the same path serves a whole message and a reader that dribbles one byte at a time.- error¶
- feed()¶
Consume
dataand report the outcome for the bytes so far (§5.2).See
sofab.decoder.Decoder.feed()for the contract; this is the same call with the loop in C.
- reset()¶
Forget the stream and start a new message, keeping the compiled binding and its destinations. See the pure engine for the contract.
- class sofab.Visitor[source]¶
Bases:
objectBase visitor: override the hooks for the fields you handle.
Every hook is keyed by the wire type the decoder recovered.
field_idis the decoded field id. Unhandled hooks default to a no-op, which still consumes the value (so unknown fields are skipped safely).- destinations()[source]¶
The slots this handler wants its fields written into, or
None.Asked once, when the
sofab.Decoderis built, and never again — so nothing the wire says can change the answer, which is what §6.6 asks of a decode’s storage. Return(binding, words, objects): asofab.Bindingmapping field ids to slots, a writable 8-byte-aligned buffer for the scalar and array slots, and a list forstring/blobslots (orNonewhen the table names none).A field the table names is written straight into its slot and no typed hook fires for it — the same bargain
on_array_begin(),on_string_begin()andon_blob_begin()strike per field, made once for the whole message. A field the table does not name reaches this visitor’s hooks exactly as it would have without a table, and thecount/maxlena table entry declares is answered from the table rather than fromon_schema_bound().This is not a second decode surface (§5.3.1). The decoder still drives, the walk is the same walk, and every rule — the receiver cap, the schema bound, the §7.3 tag test, the UTF-8 check, the declared element width, the resume transaction — has one implementation that runs for a mapped field and an unmapped one alike. The table says where a value goes; it never says how it is decoded.
Decoder(binding=…, words=…, objects=…)is the constructor shorthand for a handler that declares exactly this and nothing else.- Return type:
tuple[Binding, Any, list[Any] | None] | None
- on_field(field)[source]¶
Called for every non-sequence field before its value is decoded. Return
Falseto skip the value entirely; any other return proceeds to decode it and dispatch to the typed hook below.- Parameters:
field (Field)
- Return type:
bool | None
- on_schema_bound(field_id, n, wtype, subtype)[source]¶
The count or length the schema declares for this field, or
-1.Asked once, at the count/length header — after
on_field(), before a payload byte is read or any storage is written — for astring, ablobor an array this handler has accepted, and for nothing else. A scalar carries neither a count nor a length, so none is asked for, and a field the handler skipped is never asked (§6.7.2).nis what the wire announced: the byte length for astringorblob, the element count for an array.wtype/subtypeare the tag the wire carried, and they are here so that a handler can apply MESSAGE_SPEC §7.3 to its own declaration before answering. This is the only hook that spans more than one kind —on_string_begin()fires for astringand nothing else,on_array_begin()for an integer array and nothing else, so the decoder has already matched the tag for them. Here it has not, and an id the schema bounds can arrive under a tag the schema never declared for it. §7.3 says such a field is skipped like an unknown id, so a handler must answer-1for a tag it did not declare — a bound answered for someone else’s field is a bound applied to a length that was never the handler’s, and theINVALIDthat follows contradicts §7.3. A table entry (destinations()) gets the same test run for it by the decoder, which is why the two routes agree.subtypeis the fixlen subtype for astring, abloband a fixlen array, andNonefor an integer array, which carries none. Both are enum members recovered by index, not built, so overriding this hook still costs no allocation per field.Returning
n >= 0:a wire count/length above it is
INVALID—sofab.SofaDecodeError(MESSAGE_SPEC §7.1): the message contradicts the schema;the receiver-side
max_dyn_*cap stops applying to the field (§6.2.1) — a schema bound is a statement about validity, a receiver limit about capacity.
Returning
-1(the default) leaves the field to the receiver caps, unchanged.A handler that declares destinations (
destinations()) answers this from its table for every field the table names; this hook is what the rest go through, and both reach the same rule in the same place.def on_schema_bound(self, field_id, n, wtype, subtype): if (field_id == 0 and wtype is WireType.FIXLEN and subtype is FixlenSubtype.STRING): return 32 # the schema's maxlen for this field return -1 # not the field the schema declared
- Parameters:
field_id (int)
n (int)
wtype (WireType)
subtype (FixlenSubtype | None)
- Return type:
int
- on_sequence_begin(field_id)[source]¶
A nested sequence is opening; nothing inside it has been decoded.
Three answers:
Falseskip the whole sub-tree — its end marker is consumed and
on_sequence_end()is not called.- another
Visitor descend into it: every field of that sub-tree goes to the visitor returned, its
on_sequence_end()fires when the scope closes, and this visitor resumes afterwards. That is how a hand-written object hands a nested message to the object that models it. Generated code is flat and does not descend; it grows a wrapper array’s list with the helpers insofab.collectors.- anything else
decode the sub-tree into this same visitor, as a flat event stream.
A sub-tree opens a fresh id scope (§4.9), so the ids inside it mean what the nested schema says, not what the enclosing one does.
- Parameters:
field_id (int)
- Return type:
bool | TypeAliasForwardRef(‘sofab.Visitor’) | None
- on_array_begin(field_id, wtype, count)[source]¶
An integer array’s header has been read; no element has been decoded.
This is the only place a handler can say anything about the array’s elements, because the typed hook below receives them already decoded. Return
Noneto take the default — a list, handed toon_unsigned_array()/on_signed_array()— or a(dst, elem_min, elem_max)triple:dstSomewhere to put the elements, or
Noneto keep the list. A writable buffer of at leastcountslots: anarrayof the right typecode, amemoryviewover one, or any object supporting the buffer protocol. The decoder writes into it and does not call the typed hook — the handler already has the values where it wanted them, and none of them was ever a Python object. A buffer too short issofab.SofaArgumentError; the decoder never grows one (CORELIB_PLAN §6.6).elem_min/elem_maxThe element width the schema declares, or
Nonefor an open side. The decoder applies it at each element, so a value outside it is INVALID whether the array completes or is truncated behind it (§7.1), which is also §5.2’s INVALID-over-INCOMPLETE for free. A handler cannot do this itself: by the time it holds the list, an array that never arrived is indistinguishable from one that did.
Called again for the same array if a chunk boundary suspends the read, so return the same answer each time; the decoder restarts the array from its first element and fills
dstfrom the beginning.Not called for float arrays, which carry no declared width to state. Their destination hook is
on_float_array_begin().A configured
max_dyn_array_countdoes not gate this hook, on the same reasoning ason_blob_begin(): it is asked first and toldcount, and adstit hands back is storage it sized itself. The cap governs the list the decoder would otherwise build — theNoneanswer — not a destination of the handler’s own.- Parameters:
field_id (int)
wtype (WireType)
count (int)
- Return type:
tuple[Any, int | None, int | None] | None
- on_blob_begin(field_id, size)[source]¶
A blob’s length has been read; no payload byte has been copied yet.
Return
Noneto take the default — abytes, handed toon_bytes(). Return a writable, contiguous buffer of at leastsizebytes and the decoder copies the payload straight into it and does not callon_bytes(). One too short issofab.SofaArgumentError; the decoder never grows one (CORELIB_PLAN §6.6), and the refusal comes at the length word, before a byte is written.This is §6.6.3’s second shape for an aggregate: a callback carrying a whole blob obliges the codec to build one, and the only size available to build it from is the wire’s. A megabyte blob costs a megabyte allocation per message that way; into a destination it costs none.
Called again for the same blob if a chunk boundary suspends the copy, so return the same answer each time; the decoder restarts the payload from its first byte.
The string twin is
on_string_begin().A configured
max_dyn_blob_lendoes not gate this hook. It is asked first, and asked whatever the announced size is. The limit is there to stop the sender dictating the receiver’s allocation (§6.2.1), and a handler that hands back a buffer has sized that buffer itself — there is no allocation of the decoder’s left to prevent. It is toldsizebefore a byte is copied precisely so that a receiver unwilling to take that many can refuse it here, which is its call to make. ReturnNoneand the cap applies again, because then thebytesis the decoder’s to build and the wire is its only size.- Parameters:
field_id (int)
size (int)
- Return type:
Any
- on_string_begin(field_id, size)[source]¶
A string’s byte length has been read; no payload byte has been copied yet, and none has been validated.
Return
Noneto take the default — astr, handed toon_string(). Return a writable, contiguous buffer of at leastsizebytes and the decoder validates the payload as UTF-8, copies the wire bytes straight into it, and does not callon_string(). One too short issofab.SofaArgumentError; the decoder never grows one (CORELIB_PLAN §6.6), and the refusal comes at the length word, before a byte is written.sizeis the wire byte length, which is what a schemamaxlenbounds (MESSAGE_SPEC §1) — not a character count. What lands in the buffer is UTF-8, so a target that wants Python text decodes it itself; what this saves is thestrthe decoder would otherwise have had to build, sized by the wire.This is §6.6.3’s second shape for the third aggregate, and it is the sharpest of the three: with a caller
reassembly=buffer and aBinding, a 1 MiB string still cost a 1 MiB allocation inside the codec, because there was no third opt-out to take.The payload is still validated (§6.7.2: a field the handler reads is materialized and validated). Validation walks the bytes — §6.4.3’s
utf8_validprimitive — so nothing the wire sizes is built to check them. Invalid UTF-8 isINVALIDand the destination is left untouched.Called again for the same string if a chunk boundary suspends the copy, so return the same answer each time; the decoder restarts the payload from its first byte.
A configured
max_dyn_string_lendoes not gate this hook, on the same reasoning ason_blob_begin().- Parameters:
field_id (int)
size (int)
- Return type:
Any
- on_float_array_begin(field_id, subtype, count)[source]¶
A fixlen (
fp32/fp64) array’s count has been read; no element has been decoded.Return
Noneto take the default — alist, handed toon_float32_array()/on_float64_array()— or a writable buffer of at leastcount8-byte slots (anarray("d"), amemoryviewover one, a NumPyfloat64array). The decoder widens each element into it and does not call the typed hook. A buffer too short issofab.SofaArgumentError; the decoder never grows one (CORELIB_PLAN §6.6).subtypeissofab.FixlenSubtype.FP32orFP64, so one hook serves both and a handler that only wants one returnsNonefor the other.Slots are 8 bytes for both subtypes because a Python
floatis a double and that is what the values become. A consumer that needs anfp32’s wire bits intact takeson_float32_array_bits()instead (§6.5).Called again for the same array if a chunk boundary suspends the read, so return the same answer each time.
A configured
max_dyn_array_countdoes not gate this hook, on the same reasoning ason_blob_begin().- Parameters:
field_id (int)
subtype (FixlenSubtype)
count (int)
- Return type:
Any
- on_unsigned(field_id, value)[source]¶
Handle a decoded unsigned-integer field.
- Parameters:
field_id (int)
value (int)
- Return type:
None
- on_signed(field_id, value)[source]¶
Handle a decoded signed-integer field.
- Parameters:
field_id (int)
value (int)
- Return type:
None
- on_float32(field_id, value)[source]¶
Handle a decoded 32-bit float field.
The value is a Python
float— a Cdouble— because Python has no other float. CORELIB_PLAN §6.5 permits that for a value consumer; a consumer that has to reproduce the wire bytes takeson_float32_bits()instead.- Parameters:
field_id (int)
value (float)
- Return type:
None
- on_float32_bits(field_id, bits)[source]¶
The raw wire bits of a 32-bit float field, as an
int.Override this and the decoder calls it instead of
on_float32()for every scalarfp32.bitsis the little-endian payload read as an unsigned 32-bit integer, exactly as it lay on the wire, andsofab.Encoder.write_float32_bits()puts it back verbatim.This is CORELIB_PLAN §6.5’s required channel for a double-only target, which Python is. IEEE widening
fp32to a double sets the quiet bit, so a signaling NaN’s payload is destroyed the instant the value passes through the wider float — and no later code can recover it. A port on such a target therefore “MUST provide a raw-wire-bytes path for bit-exact consumers (transcode, round-trip, any re-encode) that re-emits those bytes verbatim” and “MUST NOT re-encode anfp32from the widened value”.(This port also preserves an sNaN through the widened
float, by doing the conversion on the bit pattern by hand rather than letting the hardware quiet it — soon_float32()is bit-exact here today. That is a property of this implementation on this platform; the raw channel is the guarantee.)The array twin is
on_float32_array_bits().- Parameters:
field_id (int)
bits (int)
- Return type:
None
- on_float64(field_id, value)[source]¶
Handle a decoded 64-bit float field.
- Parameters:
field_id (int)
value (float)
- Return type:
None
- on_string(field_id, value)[source]¶
Handle a decoded UTF-8 string field.
- Parameters:
field_id (int)
value (str)
- Return type:
None
- on_bytes(field_id, value)[source]¶
Handle a decoded raw byte-blob field.
- Parameters:
field_id (int)
value (bytes)
- Return type:
None
- on_unsigned_array(field_id, values)[source]¶
Handle a decoded unsigned-integer array field.
- Parameters:
field_id (int)
values (list[int])
- Return type:
None
- on_signed_array(field_id, values)[source]¶
Handle a decoded signed-integer array field.
- Parameters:
field_id (int)
values (list[int])
- Return type:
None
- on_float32_array(field_id, values)[source]¶
Handle a decoded 32-bit float array field.
- Parameters:
field_id (int)
values (list[float])
- Return type:
None
- on_float32_array_bits(field_id, count, payload)[source]¶
The raw wire bytes of a 32-bit float array, undecoded.
Override this and the decoder calls it instead of
on_float32_array().payloadis a read-onlymemoryviewof exactly4 * countlittle-endian bytes — the array’s payload as it lay on the wire — andsofab.Encoder.write_float32_array_bits()puts it back verbatim.§6.5’s requirement is stated over “every
fp32position — a scalarfp32(§4.6) and each element of anfp32array (§4.8)”, so the scalar channel alone would not meet it.The bytes do not outlive the call. They are the caller’s own input, borrowed for the duration of this callback exactly as a fed chunk is (§6, chunk lifetime), and a handler that still needs them afterwards copies them. That is §6.7’s second route — “the codec passes the value through the callback … and the caller copies it. The second route is not a view” — and it is why nothing is allocated to deliver an array of any length.
- Parameters:
field_id (int)
count (int)
payload (Any)
- Return type:
None
- class sofab.Binding(closed=False)[source]¶
Bases:
objectWhere each field id’s value belongs. Build once, decode many times.
Every binder method returns
self, so a table reads as one statement.atis a slot index — intowordsfor the numeric kinds, intoobjectsforstring(),bytes(),string_into()andblob_into().count_atis an optionalwordsslot the decoder writes the field’s arrival into:1for a scalar that turned up, the element count for an array, the byte length for astring_into()/blob_into()payload, the number of occurrences for a sequence. Slots the decoder never writes are left exactly as the caller prepared them, which is how a decode reports absence without inventing a sentinel.An array’s
count_atis written when the array’s count header is read, not when its last element lands — that is where the destination is settled (§6.6.3). On a decode that completes the value is the same either way; a decode that ends INCOMPLETE or INVALID inside an array may already have written it.closeddecides what happens to an id this table does not name. Open (the default), it goes to the decoder’s visitor, as it would without a table. Closed, it is skipped exactly as a decoder with no visitor skips it — no hook, nothing materialized, no cap spent, decode stays COMPLETE — and a nested sequence the table does not name is skipped whole.That is what a child table wants. The decoder descends into a bound sequence without telling the visitor, so the visitor still believes the walk is in the parent’s scope, and an id the child does not name would reach it under the parent’s identity: an unknown field a newer sender added inside a struct would land in whichever parent field shares its id. A closed child cannot hand one over. The flag belongs to the table, so a child bound from two places behaves the same in both.
- Parameters:
closed (bool)
- property entries: tuple[Entry, ...]¶
The rows, in the order they were bound. The engines compile this.
- property closed: bool¶
Whether an id this table does not name is skipped rather than handed to the visitor; see
Binding.
- property words_required: int¶
Slots the
wordsbuffer must hold — i.e. it must be at leastwords_required * 8bytes. Counts this table only; a childsequence()binding shares the same buffer, so take the maximum over the whole tree (or give every table disjoint slots, which is what generated code does).
- property objects_required: int¶
Entries the
objectslist must hold.
- property tree_words_required: int¶
words_requiredover this table and every table reachable throughsequence(). A child shares the parent’s storage, so this is the size the one buffer has to have.
- property tree_objects_required: int¶
objects_requiredover the whole tree; seetree_words_required.
- freeze()[source]¶
Close the table — this one and every child — and return the whole reachable set.
A binding is a build-once artifact: a
sofab.Decoderderives its storage requirements and one handler per table from it and holds them, so a table that changed afterwards would decode against a stale copy. Freezing at first use makes that a clear error instead. Called for you — building the decoder is what freezes the table — and idempotent, so calling it yourself is harmless.It is deliberately the whole tree: a child bound into a parent is reachable only downwards, so freezing the root is the only moment at which every table in it can be reached at once.
- Return type:
list[Binding]
- unsigned(field_id, at, count_at=None, max_value=None)[source]¶
Bind an unsigned-integer field to
wordsslotat(uint64).max_valueis the schema’s declared width (0xFFfor au8, or for abitfieldwhose highestposis 7). The slot is 64 bits wide whatever the field declares, so nothing about the storage enforces a narrower width, and MESSAGE_SPEC §1 then requires an explicit check: given, a value above it is INVALID at the value, before it is stored — so a message truncated behind it is INVALID, not INCOMPLETE (§5.2).- Parameters:
field_id (int)
at (int)
count_at (int | None)
max_value (int | None)
- Return type:
- signed(field_id, at, count_at=None, min_value=None, max_value=None)[source]¶
Bind a signed-integer field to
wordsslotat(int64).min_value/max_valueare the schema’s declared width (-128/127for ani8, or for anenumwhose constants all fit one); seeunsigned(). Either side may be given on its own.- Parameters:
field_id (int)
at (int)
count_at (int | None)
min_value (int | None)
max_value (int | None)
- Return type:
- boolean(field_id, at, count_at=None)[source]¶
Bind a boolean field to
wordsslotatas0or1.A boolean has no wire type of its own (§4.4) — it arrives as an unsigned integer and is accepted under that tag — but it is not the unsigned binding, because §4.4 splits the two rules that meet here:
canonical on encode:
sofab.Encoder.write_bool()writestrueas1;tolerant on decode: every value other than
0reads as true. Such a value is not INVALID — there is nothing to reject, only something to normalize — so the slot gets1, never the42the sender happened to write, and a re-encode emits1.
Doing that here is what keeps the rule off every caller: a slot a completed decode filled can be tested for truth or compared to
1, and both agree. (Only a completed one — like every other bound kind, a decode that ends INCOMPLETE or INVALID says nothing about what it has written so far;boolean_array()spells out what that means for an array caught mid-payload.) Note there is deliberately no declared-width argument, the wayunsigned()hasmax_value: §4.4 gives a boolean no width bound at all, unlike anenumor abitfield, so binding one with a ceiling of1— which would make42INVALID — is exactly the reading the clause rules out.- Parameters:
field_id (int)
at (int)
count_at (int | None)
- Return type:
- float32(field_id, at, count_at=None)[source]¶
Bind an
fp32field towordsslotat, widened to a nativedouble(read it back through a.cast("d")view).- Parameters:
field_id (int)
at (int)
count_at (int | None)
- Return type:
- float64(field_id, at, count_at=None)[source]¶
Bind an
fp64field towordsslotatas adouble.- Parameters:
field_id (int)
at (int)
count_at (int | None)
- Return type:
- string(field_id, at, maxlen=0, count_at=None)[source]¶
Bind a UTF-8
stringfield toobjects[at].maxlenis the schema’s declared byte length, or0for a field the schema leaves unbounded. Declaring it makes the field schema-bounded: a longer payload is INVALID (MESSAGE_SPEC §7.1) and the receiver-sidemax_dyn_string_lencap no longer applies to it (§6.2.1). Left at0the cap applies as usual.- Parameters:
field_id (int)
at (int)
maxlen (int)
count_at (int | None)
- Return type:
- bytes(field_id, at, maxlen=0, count_at=None)[source]¶
Bind a
blobfield toobjects[at]; seestring()formaxlen.- Parameters:
field_id (int)
at (int)
maxlen (int)
count_at (int | None)
- Return type:
- string_into(field_id, at, maxlen=0, count_at=None)[source]¶
Bind a UTF-8
stringfield into the buffer already inobjects[at]— nostris built.This is §6.6.3’s third shape: “into a destination the caller declared before the decode began — a field-id → slot table — which is the bullet above with the choice made once instead of per field”.
string()names a slot to put a value in, and the value is one the decoder has to build from the only size it has, the wire’s. This names a slot that already holds the storage: put a writable, contiguous, single-byte buffer (abytearray, amemoryviewover one, anarray("B")) atobjects[at]before decoding, and the decoder validates the payload as UTF-8 and copies the wire bytes into it.Nothing here is sized from the wire. A destination shorter than the announced length is refused with
sofab.SofaArgumentError— §6.3’sInvalidArgument, because “the message is well-formed and within every bound it declares — what does not fit is the storage this caller offered” — and it is never grown.count_atreceives the payload’s byte length, which is how the caller knows how much of its buffer is live; an absent field leaves the slot exactly as it was prepared, as everywhere else. (The arrival flagstring()writes would not be enough here: a caller holding the bytes needs to know how many of them are the message’s.)maxlenis the schema’s declared byte length, exactly as onstring(): declaring it makes a longer payload INVALID (MESSAGE_SPEC §7.1) and takes the receiver-sidemax_dyn_string_lencap off the field (§6.2.1). Left at0the field is schema-unbounded — and the cap does not apply to it either, on the same reasoningsofab.Visitor.on_string_begin()carries: the cap exists to stop the sender dictating the receiver’s allocation, and a caller that put the buffer there sized it itself. What bounds this field is the buffer, and a payload past it is refused at the length word before a byte is copied.- Parameters:
field_id (int)
at (int)
maxlen (int)
count_at (int | None)
- Return type:
- blob_into(field_id, at, maxlen=0, count_at=None)[source]¶
Bind a
blobfield into the buffer already inobjects[at]; seestring_into(). The only difference is the onebytes()has fromstring()— the payload is copied verbatim and not validated as UTF-8.- Parameters:
field_id (int)
at (int)
maxlen (int)
count_at (int | None)
- Return type:
- unsigned_array(field_id, at, cap, count_at=None, elem_max=None)[source]¶
Bind an unsigned-integer array to
words[at:at + cap].capis the schema’s maximum element count. A message declaring more is malformed against that schema, so it is rejected as INVALID (MESSAGE_SPEC §7.1) — the decoder never sizes storage from the wire.elem_maxis the schema’s declared element width (0xFFfor au8array, and so on). Given, it is checked at each element, before the element is stored, so a too-wide value is INVALID at the element that carries it rather than after the array completes.- Parameters:
field_id (int)
at (int)
cap (int)
count_at (int | None)
elem_max (int | None)
- Return type:
- signed_array(field_id, at, cap, count_at=None, elem_min=None, elem_max=None)[source]¶
Bind a signed-integer array to
words[at:at + cap](int64).The two halves of the declared width are independent: either may be given on its own and bounds its own side (see
unsigned_array()).- Parameters:
field_id (int)
at (int)
cap (int)
count_at (int | None)
elem_min (int | None)
elem_max (int | None)
- Return type:
- boolean_array(field_id, at, cap, count_at=None)[source]¶
Bind an array of booleans to
words[at:at + cap]as0/1.The element half of
boolean(): an array of boolean travels as an array of unsigned (§4.4 gives booleans no wire type), and §4.4’s decode rule applies per element, so each slot is normalized as it is stored.capis the schema’s maximum element count and behaves exactly as it does forunsigned_array(). There is noelem_max, for the reasonboolean()takes no width: a boolean element outside0..1is normalized, never rejected.The elements are normalized when the array completes, not one at a time — the pass runs once over the slots the payload filled, so the element loop every other array kind shares carries no test of its own. A decode that ends INCOMPLETE or INVALID inside this array may therefore leave raw wire values in the slots it already wrote, exactly as it may already have written
count_at(seeBinding). The nextsofab.Decoder.feed()refills the array from element zero, so what a completed decode leaves is always0/1.- Parameters:
field_id (int)
at (int)
cap (int)
count_at (int | None)
- Return type:
- float32_array(field_id, at, cap, count_at=None)[source]¶
Bind an
fp32array towords[at:at + cap], widened todoubleper element.- Parameters:
field_id (int)
at (int)
cap (int)
count_at (int | None)
- Return type:
- float64_array(field_id, at, cap, count_at=None)[source]¶
Bind an
fp64array towords[at:at + cap].- Parameters:
field_id (int)
at (int)
cap (int)
count_at (int | None)
- Return type:
- sequence(field_id, child, count_at=None)[source]¶
Descend into a nested sequence with
childas its table (§4.9).The child writes into the same
words/objectsstorage, so a whole message tree decodes into one flat pair of buffers.count_atcounts how many times the sequence occurred, which is what tells a caller whether an optional sub-message was present.A sequence with no binding is skipped whole, sub-tree and all — the auto-skip §5.2 requires — and costs nothing but the walk.
- class sofab.Field¶
Bases:
objectDescribes the field the decoder is currently positioned on.
Byte-for-byte compatible attribute surface with
sofab.types.Field.- count¶
- id¶
- size¶
- subtype¶
- type¶
- class sofab.WireType(*values)[source]¶
Bases:
IntEnumThe 3 low bits of a field header.
- UNSIGNED = 0¶
- SIGNED = 1¶
- FIXLEN = 2¶
- ARRAY_UNSIGNED = 3¶
- ARRAY_SIGNED = 4¶
- ARRAY_FIXLEN = 5¶
- SEQUENCE_START = 6¶
- SEQUENCE_END = 7¶
- class sofab.Status(*values)[source]¶
Bases:
IntEnumThe three-valued decode outcome CORELIB_PLAN §5.2 requires.
Returned by every
sofab.Decoder.feed(), describing the bytes consumed so far — not a verdict on the message as a whole:COMPLETE— the consumed bytes end exactly at a field boundary. A valid message may end here; more fields may also still follow.INCOMPLETE— the bytes end inside a construct. This is not an error. The partial tail is retained and the nextfeedcontinues from it. Whether an incomplete message is acceptable is the caller’s decision: only its framing (a length prefix, a datagram boundary, EOF) knows whether more bytes can still come.INVALID— the bytes are malformed regardless of what follows. Terminal; the reason is onsofab.Decoder.error.
There is deliberately no
finish/endstep that could reclassifyINCOMPLETEas an error (§5.2): the statusfeedreturned is the answer. A receiver-side limit rejection (§6.2.1) is not one of these three — it is a well-formed message the receiver declined, so it arrives on the error channel asSofaLimitError, never asINVALID(§6.3).- COMPLETE = 0¶
- INCOMPLETE = 1¶
- INVALID = 2¶
- class sofab.FixlenSubtype(*values)[source]¶
Bases:
IntEnumThe 3 low bits of a fixlen length header.
- FP32 = 0¶
- FP64 = 1¶
- STRING = 2¶
- BLOB = 3¶
- exception sofab.SofaDecodeError[source]¶
Bases:
SofaErrorMalformed input — invalid regardless of what bytes might follow: an overflowing (>64-bit) varint, a bad fixlen subtype, an out-of-range id/count/length, invalid UTF-8, nesting past
MAX_DEPTH, or a dangling sequence end (MESSAGE_SPEC§7 INVALID).This is deliberately not raised for truncation — bytes that simply end inside a field are
SofaIncompleteError(§7 INCOMPLETE), a distinct non-error outcome that is not a subclass of this class, soexcept SofaDecodeErrordoes not catch it.
- exception sofab.SofaIncompleteError[source]¶
Bases:
SofaErrorTruncated input — the bytes end inside a field (
MESSAGE_SPEC§7 INCOMPLETE): an unterminated varint, a fixlen/array payload shorter than its declared length, an array element that runs off the end, or a nested sequence that is never closed.This is not malformed: more bytes could complete the message, and the caller owns end-of-input. It is a sibling of
SofaDecodeErrorunderSofaError, not a subclass of it, so callers can tell “need more bytes” apart from “these bytes are garbage”.
- exception sofab.SofaLimitError[source]¶
Bases:
SofaErrorA wire-declared array count or fixlen (string/blob) length exceeded a receiver-configured decode limit (
Decoder(max_dyn_array_count=…, max_dyn_string_len=…, max_dyn_blob_len=…)).This is a policy rejection, not wire malformation: the bytes are perfectly well-formed and would decode fine under a looser limit — the receiver simply declined to allocate for them. It is never what a decoder raises for a limit that was not stated at all: there is no limit to raise then, so an omitted
max_dyn_*argument isSofaArgumentError(§6.2.1, §6.3).It is therefore a sibling of
SofaDecodeErrorunderSofaError, not a subclass of it, soexcept SofaDecodeErrordoes not catch it and differential fuzzing does not see a limit rejection as a conformance divergence from another engine.It is raised only for a field the schema leaves unbounded: where the schema states a
count:/maxlen:the handler says so — by declaring that bound on itssofab.Bindingentry, or by answeringsofab.Visitor.on_schema_bound()— and that bound governs instead, an over-bound value beingSofaDecodeError(CORELIB_PLAN §6.2.1/§6.3, MESSAGE_SPEC §7.1). Nor is it raised for a field nothing materializes — a skipped one, or one read into storage the handler returned fromsofab.Visitor.on_blob_begin()/sofab.Visitor.on_array_begin().
- exception sofab.SofaArgumentError[source]¶
Bases:
SofaErrorThe caller’s own request is invalid — the
InvalidArgumentoutcome of CORELIB_PLAN §6.3, which is the only code that taxonomy has for a caller mistake (every remaining malformed input isSofaDecodeError).Named after the code it carries. §6.3 lets a port “adapt casing and idiom”, and this class used to take that as far as
SofaRangeError— which read narrower than the code is: a destination too short for what a hook was told is a caller mistake, not a value out of range. Every other port keeps the word (Error::Argumentin Rust,Error::InvalidArgumentin C++), so this one does too.SofaRangeErrorremains as an alias.On construction it covers a receiver cap the caller did not state. §6.2.1 fixes the provenance of the three
max_dyn_*numbers even where the codec performs the comparison: a codec “MUST NOT hold a limit of its own, MUST NOT supply a default for one it was not given, MUST NOT read an omitted argument as unlimited, and MUST NOT clamp to one”. So all three are required, onsofab.Decoderand on thesofab.collectorshelpers alike, and omitting one is a mistake in the call rather than a property of the message or of the deployment — which is exactly what this code is for.SofaLimitErrorwould say something untrue about it: it promises a limit to raise that was never configured.On encode, a value (or id/count) is not writable: either it is outside the permitted range, or it is not an integer at all: integer fields accept whatever Python considers losslessly an integer (any object with
__index__—int,bool,IntEnum, NumPy integers), and refuse everything else rather than silently truncating it. Afloatis therefore rejected,3.0included; convert explicitly withint(x)if that is what you mean. The same code covers the encoder’s other invalid calls:getvalue()on a caller-owned fixed buffer, and a sequence end without a matching begin.On decode it covers the caller’s own storage not fitting what the message announced — a destination that cannot hold what was announced: a buffer handed back from
sofab.Visitor.on_blob_begin()orsofab.Visitor.on_array_begin()shorter than the size those hooks were told, or areassembly=buffer too small for a construct spanning a chunk. The handler was given the count or length first and answered with storage that does not fit it, so the mistake is the caller’s; §6.6.3 has the codec refuse such a destination “rather than growing it”, and §6.2.1 forbids clamping into it.That second case is the only ceiling left on a destination the caller supplies. A receiver’s configured
max_dyn_*limit does not also apply to it: the limit exists to stop the sender dictating the receiver’s allocation, and a handler that returns a buffer has sized that buffer itself (SofaLimitError).It is not raised when a field’s wire type merely contradicts the type a binding declares for it: that is MESSAGE_SPEC §7.3, which the decoder answers by skipping the field (see
sofab.Decoder).
- sofab.SofaRangeError¶
alias of
SofaArgumentError
- exception sofab.SofaBufferError[source]¶
Bases:
SofaErrorA fixed encoder buffer filled up and no flush sink was provided.
- sofab.zigzag_encode(v)[source]¶
Map a signed int to unsigned:
(n << 1) ^ (n >> 63)(64-bit).- Parameters:
v (int)
- Return type:
int
- sofab.zigzag_decode(u)[source]¶
Inverse of
zigzag_encode():(z >> 1) ^ -(z & 1).- Parameters:
u (int)
- Return type:
int
- sofab.reserve_leaf()¶
Native twin of
sofab.collectors.reserve_leaf().
- sofab.reserve_elem()¶
Native twin of
sofab.collectors.reserve_elem().
- sofab.reserve_row()¶
Native twin of
sofab.collectors.reserve_row().
- sofab.IMPL = 'native'¶
"native"when the compiledsofab._speedupsextension is in use, else"python".- Type:
Which implementation
Encoder/Decoderresolve to