diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4afdc2aa..b787b64c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -471,9 +471,11 @@ jobs:
docker rm "$CID"
chmod +x /usr/local/bin/conformance_test_runner /usr/local/bin/protoc
- # Both builds use the same target dir so build-script artifacts
- # (buffa-codegen, buffa-build, syn, etc.) are compiled only once.
- # The std binary is copied out before the no_std build overwrites it.
+ # All three builds use the same target dir so build-script artifacts
+ # (buffa-codegen, buffa-build, syn, etc.) are compiled only once. The std
+ # and table binaries are copied out before the next build overwrites
+ # them; the no_std build stays last because its run uses the artifact in
+ # place.
- name: Determine host target
id: host
run: echo "triple=$(rustc -vV | grep '^host:' | cut -d' ' -f2)" >> "$GITHUB_OUTPUT"
@@ -486,6 +488,14 @@ jobs:
- name: Save std binary
run: cp conformance/target/${{ steps.host.outputs.triple }}/release/conformance /tmp/buffa-conformance-std
+ - name: Build conformance binary (table)
+ run: >-
+ cargo build --release --manifest-path conformance/Cargo.toml
+ --features table --target ${{ steps.host.outputs.triple }}
+
+ - name: Save table binary
+ run: cp conformance/target/${{ steps.host.outputs.triple }}/release/conformance /tmp/buffa-conformance-table
+
- name: Build conformance binary (no_std)
run: >-
cargo build --release --manifest-path conformance/Cargo.toml
@@ -549,3 +559,10 @@ jobs:
--failure_list conformance/known_failures_view_vtable.txt
--maximum_edition 2024
/tmp/buffa-conformance-std
+
+ - name: Run conformance tests (via-table)
+ run: >-
+ conformance_test_runner
+ --failure_list conformance/known_failures_table.txt
+ --maximum_edition 2024
+ /tmp/buffa-conformance-table
diff --git a/.gitignore b/.gitignore
index e3ba196b..088f70d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
# Conformance proto files are fetched from the tools image.
conformance/protos/
conformance/target-nostd/
+conformance/target-table/
# Cargo build artefacts
target/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5b3d8594..b8105d7b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -82,22 +82,22 @@ needed — the runner just has to be built from protobuf source once:
```bash
task conformance-tools-local # one-time: cmake-builds the runner into .local/bin,
# populates conformance/protos/ (~10-20 min)
-task conformance-local # seven runs, same failure lists as the Docker path
+task conformance-local # eight runs, same failure lists as the Docker path
```
Requires cmake, a C++ toolchain, and protoc v30+ on PATH or `$PROTOC`
(`task install-protoc`). Set `CONFORMANCE_OUT=
` to tee per-run logs.
-**Understanding the output**: The conformance runner executes seven runs
-(std, no_std, via-view, via-lazy, view-json, via-reflect, via-vtable), each
+**Understanding the output**: The conformance runner executes eight runs
+(std, no_std, via-view, via-lazy, view-json, via-reflect, via-vtable, via-table), each
producing two suites:
-1. Binary + JSON suite — expects thousands of successes (~5500 std, ~5500 no_std). The via-view and via-lazy runs only handle binary→binary (~2800); the view-json, via-reflect, and via-vtable runs handle binary→JSON (and via-reflect also binary→binary).
-2. Text format suite — 883 successes for std and no_std (the full suite); via-view, via-lazy, view-json, via-reflect, and via-vtable show `0 successes, 883 skipped` (those modes have no `TextFormat` path).
+1. Binary + JSON suite — expects thousands of successes (~5500 std and no_std; 5,549 table). The via-view and via-lazy runs only handle binary→binary (~2800); the view-json, via-reflect, and via-vtable runs handle binary→JSON (and via-reflect also binary→binary).
+2. Text format suite — 883 successes for std, no_std and table (the full suite); via-view, via-lazy, view-json, via-reflect, and via-vtable show `0 successes, 883 skipped` (those modes have no `TextFormat` path).
-So a healthy run shows **14 `CONFORMANCE SUITE PASSED` lines**.
+So a healthy run shows **16 `CONFORMANCE SUITE PASSED` lines**.
-The Dockerfile builds **two binaries**: one with default features (std) and one with `--no-default-features` (no_std). The std binary is reused for the view/reflect runs by setting an env var:
+The Dockerfile builds **three binaries**: one with default features (std), one with `--no-default-features` (no_std) and one with `--features table` (table). The std binary is reused for the view/reflect runs by setting an env var:
- **via-view** (`BUFFA_VIA_VIEW=1`) — binary input through `decode_view → to_owned_message → encode`, verifying owned/view decoder parity.
- **via-lazy** (`BUFFA_VIA_LAZY=1`) — binary input through `decode_lazy → to_owned_message → encode` on the lazy view family (`lazy_views(true)`), verifying the lazy decoder (record arms, fragment merge, budget capture) against the corpus.
@@ -105,13 +105,15 @@ The Dockerfile builds **two binaries**: one with default features (std) and one
- **via-reflect** (`BUFFA_VIA_REFLECT=1`) — binary/JSON I/O through `DynamicMessage`'s descriptor-driven codec and reflective serde, verifying the runtime reflection codec independently of any generated type.
- **via-vtable** (`BUFFA_VIA_VTABLE=1`) — binary→JSON: decode the view, walk its vtable `ReflectMessage` surface to rebuild a `DynamicMessage`, then serialize to JSON. Verifies the generated `impl ReflectMessage for FooView`. It reuses `DynamicMessage`'s JSON serializer (which passes the corpus cleanly under via-reflect), so any failure isolates a bug in the vtable `get`/`has`/`for_each_set` surface. Requires the conformance crate's `reflect` feature, so it is absent from the no_std binary.
-**Expected failures** are listed in `conformance/known_failures.txt` (std binary+JSON), `conformance/known_failures_nostd.txt` (no_std binary+JSON), `conformance/known_failures_view.txt` (via-view), `conformance/known_failures_lazy.txt` (via-lazy), `conformance/known_failures_view_json.txt` (view-json), `conformance/known_failures_reflect.txt` (via-reflect), `conformance/known_failures_view_vtable.txt` (via-vtable), and `conformance/known_failures_text.txt` (text format — shared between std and no_std; currently empty). The text list is passed via `--text_format_failure_list` since the runner validates each suite's list independently. When a previously-failing test starts passing, remove it from the relevant file; when a new test is expected to fail, add it.
+**via-table** is a separate binary, built with `--features table` (no env var), that runs the whole std run — binary, JSON and text — against test messages generated with `CodecStrategy::Table`. It exercises the table codec's decode, size and encode paths, oneofs, maps and the bridge to well-known-type children on the proto3, editions and nested proto2 messages. The messages the table cannot handle run through the unrolled codec instead: messages with extension ranges under JSON, `MessageSet` messages, group types and the messages that hold them. That includes the top-level `TestAllTypesProto2`, which via-table therefore does not exercise on the table. `buffa-build` prints a `cargo:warning` naming the messages that fell back, and a compile-time check in `conformance/src/main.rs` fails the build if the messages that must use the table stop doing so. The `table` feature needs Rust 1.77; `conformance/Cargo.toml` says why.
+
+**Expected failures** are listed in `conformance/known_failures.txt` (std binary+JSON), `conformance/known_failures_nostd.txt` (no_std binary+JSON), `conformance/known_failures_view.txt` (via-view), `conformance/known_failures_lazy.txt` (via-lazy), `conformance/known_failures_view_json.txt` (view-json), `conformance/known_failures_reflect.txt` (via-reflect), `conformance/known_failures_view_vtable.txt` (via-vtable), `conformance/known_failures_table.txt` (via-table; currently empty), and `conformance/known_failures_text.txt` (text format — shared between std, no_std and table; currently empty). The text list is passed via `--text_format_failure_list` since the runner validates each suite's list independently. When a previously-failing test starts passing, remove it from the relevant file; when a new test is expected to fail, add it.
**Capturing output**: To save per-run logs for analysis, mount a directory and set `CONFORMANCE_OUT`:
```bash
docker run --rm -v /tmp/conf:/out -e CONFORMANCE_OUT=/out buffa-conformance
-# logs: /tmp/conf/conformance-{std,nostd,view,lazy,view-json,reflect,vtable}.log
+# logs: /tmp/conf/conformance-{std,nostd,view,lazy,view-json,reflect,vtable,table}.log
```
**Upgrading the protobuf version**: bump `TOOLS_IMAGE` in `Taskfile.yml` and `PROTOC_VERSION` in `.github/workflows/ci.yml`, then:
diff --git a/Taskfile.yml b/Taskfile.yml
index 3e0ac5ac..d5a59b4a 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -77,9 +77,9 @@ tasks:
conformance-local:
desc: >-
- Run the full six-run conformance suite without Docker, using the
+ Run the full eight-run conformance suite without Docker, using the
runner from conformance-tools-local and host cargo builds of the
- std/no_std conformance binaries. Requires protoc v30+ on PATH or
+ std/table/no_std conformance binaries. Requires protoc v30+ on PATH or
$PROTOC.
cmds:
- scripts/run-conformance-local.sh
diff --git a/conformance/Cargo.toml b/conformance/Cargo.toml
index e35a6368..61dc4702 100644
--- a/conformance/Cargo.toml
+++ b/conformance/Cargo.toml
@@ -31,6 +31,10 @@ default = ["buffa-std"]
# whose generated code therefore omits the reflect impls.
buffa-std = ["buffa/std", "buffa-types/std", "reflect"]
reflect = ["buffa-types/reflect"]
+# Generates the test messages with `CodecStrategy::Table`, for the via-table
+# run. Needs Rust 1.77 (`offset_of!`); buffa-build refuses older compilers, so
+# the default build stays at the 1.75 MSRV.
+table = []
[dependencies]
buffa = { path = "../buffa", default-features = false, features = ["json", "text"] }
diff --git a/conformance/Dockerfile b/conformance/Dockerfile
index 7e21daf8..7e45f3bd 100644
--- a/conformance/Dockerfile
+++ b/conformance/Dockerfile
@@ -35,9 +35,18 @@ COPY buffa-types/ buffa-types/
COPY protoc-gen-buffa/ protoc-gen-buffa/
COPY conformance/ conformance/
-RUN cargo build --release --manifest-path conformance/Cargo.toml
-RUN cargo build --release --manifest-path conformance/Cargo.toml \
- --no-default-features --target-dir conformance/target-nostd
+# All three builds share one target dir, so the dependencies and build scripts
+# that do not depend on the features (buffa-codegen, buffa-build, syn, ...) are
+# compiled once. Each binary is copied out before the next build overwrites it.
+RUN mkdir /out \
+ && cargo build --release --manifest-path conformance/Cargo.toml \
+ && cp conformance/target/release/conformance /out/buffa-conformance \
+ && cargo build --release --manifest-path conformance/Cargo.toml \
+ --features table \
+ && cp conformance/target/release/conformance /out/buffa-conformance-table \
+ && cargo build --release --manifest-path conformance/Cargo.toml \
+ --no-default-features \
+ && cp conformance/target/release/conformance /out/buffa-conformance-nostd
# ── Stage 3: runtime image ────────────────────────────────────────────────
FROM ubuntu:24.04
@@ -46,12 +55,12 @@ RUN apt-get update && apt-get install -y libstdc++6 libgcc-s1 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=tools /conformance_test_runner /usr/local/bin/conformance_test_runner
-COPY --from=rust-builder \
- /workspace/conformance/target/release/conformance /usr/local/bin/buffa-conformance
-COPY --from=rust-builder \
- /workspace/conformance/target-nostd/release/conformance /usr/local/bin/buffa-conformance-nostd
+COPY --from=rust-builder /out/buffa-conformance /usr/local/bin/buffa-conformance
+COPY --from=rust-builder /out/buffa-conformance-nostd /usr/local/bin/buffa-conformance-nostd
+COPY --from=rust-builder /out/buffa-conformance-table /usr/local/bin/buffa-conformance-table
COPY conformance/known_failures.txt /known_failures.txt
COPY conformance/known_failures_nostd.txt /known_failures_nostd.txt
+COPY conformance/known_failures_table.txt /known_failures_table.txt
COPY conformance/known_failures_view.txt /known_failures_view.txt
COPY conformance/known_failures_lazy.txt /known_failures_lazy.txt
COPY conformance/known_failures_view_json.txt /known_failures_view_json.txt
diff --git a/conformance/build.rs b/conformance/build.rs
index fdb66123..c8f37528 100644
--- a/conformance/build.rs
+++ b/conformance/build.rs
@@ -27,7 +27,7 @@ fn main() {
// so the no_std binary — built `--no-default-features` — omits it.
// TestAllTypesProto3 with serde + textproto enabled.
- buffa_build::Config::new()
+ config()
.files(&["protos/google/protobuf/test_messages_proto3.proto"])
.includes(&["protos/"])
.generate_json(true)
@@ -39,7 +39,7 @@ fn main() {
.expect("buffa_build failed for test_messages_proto3.proto");
// TestAllTypesProto2 with serde + textproto enabled for proto2 conformance.
- buffa_build::Config::new()
+ config()
.files(&["protos/google/protobuf/test_messages_proto2.proto"])
.includes(&["protos/"])
.generate_json(true)
@@ -54,7 +54,7 @@ fn main() {
// Editions test messages: proto3 behavior via editions.
let editions_proto3 = protos_dir.join("editions/golden/test_messages_proto3_editions.proto");
if editions_proto3.exists() {
- buffa_build::Config::new()
+ config()
.files(&[&editions_proto3])
.includes(&[&protos_dir])
.generate_json(true)
@@ -66,7 +66,7 @@ fn main() {
.expect("buffa_build failed for test_messages_proto3_editions.proto");
// Editions test messages: proto2 behavior via editions.
- buffa_build::Config::new()
+ config()
.files(&["protos/editions/golden/test_messages_proto2_editions.proto"])
.includes(&["protos/"])
.generate_json(true)
@@ -82,7 +82,7 @@ fn main() {
// JSON enabled for the extension registry (the text `[pkg.ext]` bracket
// syntax resolves through the same registry structs); text enabled for
// the RunDelimitedTests suite.
- buffa_build::Config::new()
+ config()
.files(&["protos/conformance/test_protos/test_messages_edition2023.proto"])
.includes(&["protos/"])
.generate_json(true)
@@ -103,6 +103,18 @@ fn main() {
emit_reflect_fds(&manifest_dir);
}
+/// The generator configuration shared by every schema. With the crate's
+/// `table` feature it selects [`buffa_build::CodecStrategy::Table`], so that
+/// the via-table run drives the table codec through the corpus.
+fn config() -> buffa_build::Config {
+ let config = buffa_build::Config::new();
+ if std::env::var_os("CARGO_FEATURE_TABLE").is_some() {
+ config.codec_strategy(buffa_build::CodecStrategy::Table)
+ } else {
+ config
+ }
+}
+
/// Write `OUT_DIR/conformance_protos.fds` containing the conformance test
/// message types and their transitive imports.
fn emit_reflect_fds(manifest_dir: &std::path::Path) {
diff --git a/conformance/known_failures_table.txt b/conformance/known_failures_table.txt
new file mode 100644
index 00000000..4a5c7264
--- /dev/null
+++ b/conformance/known_failures_table.txt
@@ -0,0 +1,9 @@
+# Known conformance test failures for the table codec (CodecStrategy::Table).
+#
+# Tests listed here are expected to fail in the via-table run and will not
+# cause it to report a failure. Each line is a test name (matched by the
+# runner with --failure_list). Remove entries as failures are fixed.
+#
+# The messages the table cannot handle run through the unrolled codec in this
+# run, including the top-level TestAllTypesProto2, so its tests do not exercise
+# the table.
diff --git a/conformance/known_failures_text.txt b/conformance/known_failures_text.txt
index cc6b28c4..35a6f91d 100644
--- a/conformance/known_failures_text.txt
+++ b/conformance/known_failures_text.txt
@@ -4,4 +4,5 @@
# The text-format suite runs separately from the binary+JSON suite and
# validates this list independently. Remove entries as failures are fixed.
#
-# std and no_std share this file — textproto has no std-only paths.
+# All text-format runs (std, no_std, table) share this file — textproto has
+# no std-only paths.
diff --git a/conformance/run-conformance.sh b/conformance/run-conformance.sh
index d9bceeb6..29724504 100755
--- a/conformance/run-conformance.sh
+++ b/conformance/run-conformance.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-set -e
+set -eo pipefail
# The conformance_test_runner always runs two suites per invocation:
# 1. Binary + JSON (expect thousands of successes)
@@ -87,3 +87,11 @@ BUFFA_VIA_VTABLE=1 run_suite vtable \
--failure_list /known_failures_view_vtable.txt \
--maximum_edition 2024 \
/usr/local/bin/buffa-conformance
+
+# Via-table mode: the std run against a binary generated with CodecStrategy::Table.
+run_suite table \
+ conformance_test_runner \
+ --failure_list /known_failures_table.txt \
+ --text_format_failure_list /known_failures_text.txt \
+ --maximum_edition 2024 \
+ /usr/local/bin/buffa-conformance-table
diff --git a/conformance/src/main.rs b/conformance/src/main.rs
index b031098e..e80a2044 100644
--- a/conformance/src/main.rs
+++ b/conformance/src/main.rs
@@ -129,6 +129,30 @@ fn setup_type_registry() {
set_type_registry(reg);
}
+// ── Table codec ──────────────────────────────────────────────────────────
+//
+// Built with the `table` feature, the test messages are generated with
+// `CodecStrategy::Table` and this binary is the via-table run. A message that
+// the strategy leaves unrolled has no `__BUFFA_TABLE_*` static, so naming the
+// ones below fails the build if a fallback rule ever moves them back to the
+// unrolled codec and the run would silently stop testing the table.
+// `TestAllTypesProto2` is not among them: its extension ranges and group
+// fields keep it unrolled, so proto2 reaches the table through its nested and
+// sibling messages.
+
+#[cfg(all(not(no_protos), feature = "table"))]
+const _: () = {
+ fn is_table(_: &'static buffa::table::Table) {}
+ fn pin() {
+ is_table(&proto3::__BUFFA_TABLE_TestAllTypesProto3);
+ is_table(&proto3::test_all_types_proto3::__BUFFA_TABLE_NestedMessage);
+ is_table(&proto2::__BUFFA_TABLE_TestLargeOneof);
+ is_table(&proto2::test_all_types_proto2::__BUFFA_TABLE_NestedMessage);
+ #[cfg(has_editions_protos)]
+ is_table(&editions_proto3::__BUFFA_TABLE_TestAllTypesProto3);
+ }
+};
+
// ── Via-view mode ────────────────────────────────────────────────────────
//
// When `BUFFA_VIA_VIEW=1`, binary input is routed through
diff --git a/scripts/run-conformance-local.sh b/scripts/run-conformance-local.sh
index 869342c6..f5b59ecc 100755
--- a/scripts/run-conformance-local.sh
+++ b/scripts/run-conformance-local.sh
@@ -2,9 +2,9 @@
# Run the full protobuf conformance suite without Docker.
#
# Native equivalent of conformance/run-conformance.sh + conformance/Dockerfile:
-# builds the std and no_std conformance binaries with the host cargo, then
-# drives them through conformance_test_runner (built by
-# scripts/build-conformance-tools.sh) for the same seven runs as the Docker
+# builds the std, table and no_std conformance binaries with the host cargo,
+# then drives them through conformance_test_runner (built by
+# scripts/build-conformance-tools.sh) for the same eight runs as the Docker
# image. The runner talks to the testee over stdin/stdout pipes, so no
# container plumbing is needed.
#
@@ -21,6 +21,10 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
RUNNER="${ROOT}/.local/bin/conformance_test_runner"
CONF="${ROOT}/conformance"
+# rust-toolchain.toml selects the toolchain from the working directory, so the
+# builds below run from the repository root whatever the caller's directory is.
+cd "${ROOT}"
+
if [ ! -x "${RUNNER}" ]; then
echo "conformance_test_runner not found at ${RUNNER}."
echo "Run: task conformance-tools-local"
@@ -31,12 +35,15 @@ if [ ! -f "${CONF}/protos/conformance.proto" ]; then
exit 1
fi
-echo "=== Building conformance binaries (std + no_std) ==="
+echo "=== Building conformance binaries (std + table + no_std) ==="
cargo build --release --manifest-path "${CONF}/Cargo.toml"
+cargo build --release --manifest-path "${CONF}/Cargo.toml" \
+ --features table --target-dir "${CONF}/target-table"
cargo build --release --manifest-path "${CONF}/Cargo.toml" \
--no-default-features --target-dir "${CONF}/target-nostd"
STD_BIN="${CONF}/target/release/conformance"
+TABLE_BIN="${CONF}/target-table/release/conformance"
NOSTD_BIN="${CONF}/target-nostd/release/conformance"
run_suite() {
@@ -96,4 +103,11 @@ BUFFA_VIA_VTABLE=1 run_suite vtable \
--maximum_edition 2024 \
"${STD_BIN}"
-echo "All seven conformance runs completed."
+run_suite table \
+ "${RUNNER}" \
+ --failure_list "${CONF}/known_failures_table.txt" \
+ --text_format_failure_list "${CONF}/known_failures_text.txt" \
+ --maximum_edition 2024 \
+ "${TABLE_BIN}"
+
+echo "All eight conformance runs completed."