Skip to content

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 tad works again in Rust builds. The Rust archive's symbols no longer leak from libvmaf.so where 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.cpp covers 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 of enable_rust_features and 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 rc4 until the v1.0.0-rc.3 tag; 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.