Research: Rust/cbindgen Integration for libvmaf Feature Extractors (ADR-0707)¶
Date: 2026-05-28 Branch: feat/tad-rust-pilot ADR: ADR-0707
Objective¶
Establish whether a Rust staticlib can be linked into libvmaf.so via a Meson custom_target(cargo build) + declare_dependency without disrupting existing tests, build configurations, or the Netflix golden-data gate.
Key findings¶
1. cbindgen header generation works correctly¶
cbindgen 0.27 (current stable) generates a valid C header from Rust #[no_mangle] pub unsafe extern "C" functions. The header is written to $OUT_DIR/include/vmafx_tad.h during cargo build. The signatures are stable and simple enough that we re-declare them directly in the C wrapper (tad_rust.c) rather than depending on the generated header at compile time — this avoids a generated-file dependency in the Meson source tree.
2. LTO interaction requires architectural care¶
With -flto=auto (the repo default), the linker resolves all undefined symbols across all TUs in a link step, even those in dead code paths. Placing tad_rust.c in the libvmaf_feature intermediate static lib caused every test executable (including those that never call TAD) to require libvmafx_tad.a on their link line.
Solution: compile tad_rust.c as a direct source of the library('vmaf') target (not into libvmaf_feature.a), and gate the vmaf_fex_tad extern + list entry in feature_extractor.c behind #if HAVE_RUST_TAD. This way:
- Static-lib-linked test binaries see no TAD symbols at all.
- The shared
libvmaf.socompilestad_rust.cwith-DHAVE_RUST_TAD(viadeclare_dependency compile_args) and links the Rust archive.
3. Meson archive linking¶
declare_dependency(link_args: [path_to_archive]) is the correct Meson idiom for pre-built static archives that are not themselves Meson static_library() targets. The archive path must be absolute (derived from meson.current_build_dir() / archive_name). The sources: [custom_target] field in the dependency ensures the custom_target runs before any link step that consumes the dependency.
4. Rust panic=abort is required¶
Without panic = "abort" in the workspace [profile.release], the Rust staticlib references rust_begin_unwind which requires linking libstd. With panic = "abort", the archive is self-contained (depends only on libc which the C linker already brings in). This is essential for embedding Rust in a C shared library.
5. VMAF score isolation confirmed¶
A CPU build with --feature tad produces per-frame tad and tad_sad scores without affecting the standard VMAF score path. The vmaf_fex_tad extractor is registered in feature_extractor_list[] but is never activated unless explicitly requested via --feature tad or a model that references it (no such model exists).
6. Netflix golden-data gate unaffected¶
The TAD extractor is not part of any VMAF model. The three canonical Netflix golden pairs produce the same VMAF scores whether or not enable_rust_features=true. Verified by confirming vmaf_fex_tad is only invoked when --feature tad is present in the feature request list.
Recipe for future Rust feature extractors¶
- Add a crate under
core/src/feature/rust/<name>/following the TAD template. - Add the crate path to the workspace
membersin the rootCargo.toml. - In
core/src/meson.build: add acustom_target+declare_dependencyfor the new archive (copy the TAD block, change the name). - Add a
tad_rust.c-style C wrapper atcore/src/feature/<name>_rust.c. Compile it as a direct source oflibrary('vmaf')(not intolibvmaf_feature.a). - Register the extractor in
feature_extractor.cbehind a#if HAVE_<NAME>guard. - Wire the new define through the
declare_dependency compile_args.
See ADR-0707 for the full rationale and constraint summary.