Skip to content

table: add the interpreters behind table-driven message codecs - #468

Draft
iainmcgin wants to merge 2 commits into
mainfrom
table-runtime
Draft

iainmcgin wants to merge 2 commits into
mainfrom
table-runtime

Conversation

@iainmcgin

@iainmcgin iainmcgin commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Adds buffa::table, the runtime for the table-driven Message codec proposed in #463: a #[doc(hidden)] module that sizes, writes, and decodes a message from a static Table<M> of 12-byte field entries. Nothing calls it yet; the generator that emits tables is #469.

Stacked on #467, whose pre-sized cursor the encode interpreter writes through, and adds a #[doc(hidden)] EncodeSink::__with_pre_sized for it.

Kind is the field type crossed with its cardinality, so the interpreter dispatches once per field. Decoding runs over a contiguous &[u8], gathering a non-contiguous Buf once.

Table::new is unsafe, because its offsets and kinds must describe the struct. Generated code builds tables only through the exported __table! and __table_entry! macros, which hold the unsafe block, take each offset from offset_of!, and require the field's type to equal the type its kind stores. A wrong kind is a compile error, and generated code compiles under #![forbid(unsafe_code)]. A new CI step runs the module's tests under Miri. Table::new also compares an ABI constant, so tables from a different codegen version fail to build.

Deviation from #463, which proposed a buffa cargo feature for the unsafe: there is none, because the unsafe block is inside the macros. offset_of! needs Rust 1.77, so on the 1.75 MSRV __table! expands to a compile_error! naming the version.

Behaviour that differs from unrolled code, listed in the module docs: a length past the end of its enclosing message fails at once with UnexpectedEof, Table::merge_field needs a contiguous buffer (so the type of a group field cannot be a table message), and clear() resets to Default, releasing allocations.

About 2,400 lines are outside tests.rs, well over the 250-line guideline: the interpreters, the macros that make them sound, and the Miri tests that check them are not usable or checkable apart.

The provided encode methods on Message and ViewEncode now compute the
exact size, then write any BufMut into that many contiguous bytes of its
spare capacity through a shared bounds-checked cursor (PreSized). Each
message's write_to is compiled once for all BufMut sinks instead of once
per sink type, and encoding into a BytesMut with room no longer makes a
call per tag and varint byte.

A sink with less room than the message is filled through a scratch Vec
and appended with one put_slice. Rope and types that implement
EncodeSink without BufMut still receive every write individually.
Generated lazy views and DynamicMessage are unchanged.

A write_to that produces more bytes than compute_size declared now
panics in release builds too, and in debug builds one that produces
fewer panics in every encode entry point.
A `Table<M>` describes a message as a sorted array of twelve-byte
entries (field number, byte offset, and a kind giving the field type and
cardinality). One set of interpreters, shared by every message, sizes,
writes and decodes a message from its table, so a message costs a table
rather than three per-field code sequences.

The interpreters cover scalars, strings, bytes, enums, messages and
their repeated and packed forms, with unknown-field preservation. They
follow the unrolled code's wire format, two-pass size protocol, and
recursion, unknown-field and element-memory limits. Encoding into any
`BufMut` runs one non-generic function in this crate through the
pre-sized cursor; other sinks get an instance generic over the sink.

`Table::new` is `unsafe`, because offsets and kinds must describe the
struct's layout, and checks the rest of the table's structure (order,
aux indexes and variants, the dense lookup) at compile time when it
initialises a static.
@github-actions

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Base automatically changed from iain/single-sink-encode to main September 26, 2026 19:59

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant