ADR-1713: Rust feature-extractor twins behind the unchanged C ABI (RC4 framework)¶
- Status: Accepted (2026-10-07, Q-087)
- Date: 2026-10-05
- Deciders: maintainer (popup 2026-10-07, Q-087); lusoris
- Tags:
rust,build,feature-extractor,abi,rc4,testing
Context¶
RC4 (ADR-1421, ADR-1490, epic #1723) owns the first full Rust metric: the four features of vmaf_v1.0.16_3d0h (cambi, speed_chroma, integer adm3, integer motion3) and the model prediction in Rust, bit-identical to the C extractors, with libvmaf.so's ABI and the ffmpeg filter unchanged and a documented fallback to C. Five lanes write those pieces in parallel, so they need one framework first.
The only Rust in libvmaf so far is the TAD pilot (ADR-0707): one crate built as its own static library, a hand-written C wrapper, a cbindgen header generated at build time that nothing includes, and a HAVE_RUST_TAD define that feature_extractor.cpp never sees, because that file is compiled in the feature static library without the Rust dependency. A build with -Denable_rust_features=true therefore answers --feature tad with "problem loading feature extractor: tad". The same build exports the Rust standard library's symbols from libvmaf.so, because the archive's objects keep default visibility. Neither shape scales to four twins of existing C extractors that must be selectable at run time and must emit the C's feature names, options and flush behaviour exactly.
Decision¶
We will build every Rust extractor as an rlib reached from one static library crate, vmafx-core-rs (core/src/rust/staticlib), which is the only Rust archive libvmaf links. A Rust extractor is a twin of a C extractor: it implements the Extractor trait of vmafx-fex (core/src/rust/fex) and lists itself in its crate's TWINS; the C shim core/src/rust/shim/rust_twins.cpp registers it as <c name>_rust by copying the C extractor's descriptor (option table, priv_size, provided features, flags, reads_prev_prev_ref) and replacing the callbacks with generic ones that read the parsed options back by name, pass plane views and previous references to Rust, and route scores through the C feature-name dictionary and collector. Rust never sees a libvmaf struct. The ABI (vmafx_fex::abi) is described by a cbindgen header that is generated by a developer script, committed, drift-checked in CI and guarded by a layout test. The libvmaf-linked crates form their own Cargo workspace (core/src/rust/Cargo.toml) whose lockfile holds no external crate, so the build runs with cargo --offline --locked from an empty cargo home; the root workspace keeps the bindings, which need bindgen. Every profile Meson builds uses panic = "abort". The C extractor stays the default: the environment variable VMAF_FEATURE_IMPL=rust (read once, like VMAF_<BACKEND>_DISPATCH) makes every registration path replace a CPU extractor by its Rust twin when one exists and log the C fallback when none does; --feature <name>_rust selects a twin directly; the build option stays enable_rust_features. A differential harness (scripts/ci/rust_twin_diff.py) runs the same binary with both settings and requires equal doubles on every metric of every frame, with the JSON receipt naming the twin that ran. The shared contract the RC4 lanes code against is docs/development/rust-extractor-framework.md.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| Rust twins registered next to the C extractors, selected at run time (chosen) | C stays default and fallback in the same binary; the harness compares both paths of one build; option tables, names and flags have one definition (the C's) | A shim that copies descriptors and reads options back from a C-layout priv blob | Only design that gives a selectable path, a same-build comparison and no second option table |
| Rust replaces the C extractor entry points at build time | No selection logic; smaller binary | No same-binary differential test; a Rust defect has no runtime fallback; the golden gate could not compare | Fallback to C is an RC4 exit criterion |
| Pure Rust library with its own C API, called by C feature code | Clean Rust API | A second extractor surface, options and names defined twice, C call sites rewritten per feature | Duplicates behaviour (HISS-19) and changes more C than the twins need |
| One static library per feature crate (TAD's shape) | Crates independent | Two Rust staticlibs in one link duplicate the standard library symbols | Does not link with more than one crate |
| cbindgen at build time (TAD's shape) | Header always current | Build needs cbindgen and about twenty crates from the network; the generated header was never included | Offline container and distribution builds |
| Hand-written header | No tool | Silent drift between Rust and C layouts | The generated header plus layout test catches drift |
| One workspace for bindings and libvmaf-linked crates | One lockfile, one cargo invocation | Cargo resolves the whole workspace for -p, so an offline build needs the bindings' bindgen in the registry cache | A separate workspace keeps the libvmaf build offline |
catch_unwind at every entry point | A panic becomes an error code | Unwinding needs the unwind runtime in libvmaf; state after a caught panic is suspect | panic = "abort" plus a deny-list of panicking constructs; Rust aborts at an extern "C" boundary anyway |
Runtime selection through a new public API or VmafConfiguration field | Discoverable from code | Changes libvmaf.h and the ABI, which RC4 must not do | Environment variable has precedent (ADR-0488) and reaches the ffmpeg filter unchanged |
A twin-specific option on each C extractor (impl=rust) | Per-feature control | Changes every C option table and the feature-name keys | Option keys feed feature names and model resolution |
Consequences¶
- Positive: lanes write only Rust in their own crate; option parsing, defaults, ranges, aliases, feature-name decoration and the collector stay C, so a twin cannot drift from them.
--feature tadworks again in Rust builds. The Rust archive's symbols no longer leak fromlibvmaf.sowhere the linker supports--exclude-libs. Clippy runs on every workspace crate. - Negative: the shim keeps a C-layout priv blob per twin context just to hold the parsed options; registry iteration in
feature_extractor.cppcovers a static list and a lazily built Rust list. The dev container has no Rust toolchain yet, so published artifacts cannot carry the Rust path until a follow-up adds a pinned toolchain. - Neutral / follow-ups: lanes S, M, A, C and P land the twins and the predictor; task 7 of #1723 adds the CLI flag
--feature_impl, decides the default ofenable_rust_featuresand records throughput. Windows linking of the Rust archive (its native libraries) is not covered by this change.
References¶
- Maintainer direction 2026-10-05 (paraphrased): start RC4 in parallel with the RC3 cut; RC4 pull requests stay drafts with the label
rc4until thev1.0.0-rc.3tag; promote the TAD pilot into the extractor framework the four features share. - Epic #1723, task 1.
- ADR-0702, ADR-0707, ADR-0488, ADR-1341, ADR-1461.
- Research digest: Research-2151.
Q-087(maintainer popup 2026-10-07): accepted as written.