Skip to main content

oxideav_core/registry/
slice.rs

1//! Distributed-slice auto-registration.
2//!
3//! Each sibling crate that ships a codec / container / filter / source
4//! deposits a [`Registrar`] into the global [`REGISTRARS`] slice via the
5//! [`crate::register!`] macro. [`RuntimeContext::with_all_features`]
6//! walks the slice and invokes every registrar exactly once on the
7//! `RuntimeContext` it's building.
8//!
9//! This replaces the historical umbrella-driven registration where
10//! `oxideav::with_all_features()` enumerated every sibling explicitly.
11//! With the slice, a sibling registers itself just by being linked into
12//! the binary — no umbrella plumbing required. Consumers depend on the
13//! sibling crates they want; pulling them in is enough.
14//!
15//! # Standalone opt-out
16//!
17//! Each sibling's `register!()` call lives behind that crate's
18//! default-on `registry` cargo feature. Consumers that want the
19//! standalone (no-`oxideav-core`-dep) build path turn the feature off
20//! and the macro call disappears — no linkme entry, no slice
21//! contribution.
22//!
23//! # Wasm
24//!
25//! linkme's distributed-slice machinery works on `wasm32-unknown-unknown`
26//! and `wasm32-wasi` via wasm-ld's section grouping (recent stable
27//! Rust toolchains ship a recent enough lld). The classic
28//! `__attribute__((constructor))` path used by `ctor` / `inventory`
29//! does NOT work on wasm; this is the deliberate reason linkme was
30//! chosen over those.
31
32use crate::RuntimeContext;
33
34/// One entry in the distributed registrar slice. Carries the sibling
35/// crate's display name (for the `with_all_features_traced` hook +
36/// `with_all_features_filtered` opt-out filter) plus the function
37/// pointer to invoke against the runtime context.
38pub struct Registrar {
39    /// Short display name for the sibling — typically the crate's
40    /// shortened identifier (`"aac"`, `"vp8"`, `"webp"`, …). Used by
41    /// the trace hook and the filter hook.
42    pub name: &'static str,
43    /// The register function. Takes a mutable reference to the
44    /// runtime context being built; should install whatever codec /
45    /// container / filter / source factories the sibling provides.
46    pub func: fn(&mut RuntimeContext),
47}
48
49/// Global slice of registrars. Each sibling crate's
50/// [`crate::register!`] call deposits one entry here; the slice is
51/// materialised at link time.
52#[linkme::distributed_slice]
53pub static REGISTRARS: [Registrar];
54
55impl RuntimeContext {
56    /// Build a [`RuntimeContext`] populated by every sibling crate
57    /// linked into the binary.
58    ///
59    /// Walks [`REGISTRARS`] and invokes each entry's `func` against a
60    /// fresh context. Ordering is link-determined — siblings should
61    /// not assume any particular order.
62    pub fn with_all_features() -> Self {
63        Self::with_all_features_traced(|_| {})
64    }
65
66    /// Like [`with_all_features`](Self::with_all_features) but invokes
67    /// `trace(name)` immediately before each sibling's `register` fn.
68    /// Useful for diagnosing register-time hangs — the last name passed
69    /// to `trace` names the offending sibling.
70    pub fn with_all_features_traced<F: FnMut(&str)>(mut trace: F) -> Self {
71        let mut ctx = Self::new();
72        for reg in REGISTRARS {
73            trace(reg.name);
74            (reg.func)(&mut ctx);
75        }
76        ctx
77    }
78
79    /// Like [`with_all_features`](Self::with_all_features) but skips
80    /// entries whose name `filter` returns `false` for. Used by CLIs to
81    /// implement opt-outs like `--no-hwaccel` (skip `videotoolbox` /
82    /// `audiotoolbox`).
83    pub fn with_all_features_filtered<F: FnMut(&str) -> bool>(mut filter: F) -> Self {
84        let mut ctx = Self::new();
85        for reg in REGISTRARS {
86            if filter(reg.name) {
87                (reg.func)(&mut ctx);
88            }
89        }
90        ctx
91    }
92}
93
94/// Auto-register a sibling crate's `register` fn into the global
95/// [`REGISTRARS`] slice.
96///
97/// Place at module scope inside the sibling crate, gated behind the
98/// crate's `registry` cargo feature so the standalone build path
99/// stays decoupled from `oxideav-core`:
100///
101/// ```ignore
102/// pub fn register(ctx: &mut oxideav_core::RuntimeContext) {
103///     /* install factories */
104/// }
105///
106/// #[cfg(feature = "registry")]
107/// oxideav_core::register!("aac", register);
108/// ```
109///
110/// The macro expands to a `#[linkme::distributed_slice]` static. The
111/// sibling crate must have `linkme = "0.3"` in its `[dependencies]`
112/// so the absolute path `::linkme::distributed_slice` resolves at
113/// macro expansion time.
114#[macro_export]
115macro_rules! register {
116    ($name:literal, $func:path) => {
117        // `#[used]` keeps rustc from dropping the static during the
118        // pre-link DCE pass; without it, integration tests that don't
119        // directly reference the static drop the slice entry on the
120        // floor and the slice walker observes an empty registry.
121        #[used]
122        #[::linkme::distributed_slice($crate::REGISTRARS)]
123        static __OXIDEAV_AUTO_REGISTRAR: $crate::Registrar = $crate::Registrar {
124            name: $name,
125            func: $func,
126        };
127    };
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn empty_registry_walks_cleanly() {
136        // The test binary doesn't link any sibling crates, so the slice
137        // is empty here — `with_all_features` returns a fresh context
138        // with no registrations.
139        let ctx = RuntimeContext::with_all_features();
140        assert_eq!(ctx.codecs.decoder_ids().count(), 0);
141    }
142
143    #[test]
144    fn trace_hook_fires_zero_times_on_empty_registry() {
145        let mut names = Vec::<String>::new();
146        let _ctx = RuntimeContext::with_all_features_traced(|n| names.push(n.to_string()));
147        assert!(names.is_empty());
148    }
149
150    #[test]
151    fn filter_hook_fires_zero_times_on_empty_registry() {
152        let mut names = Vec::<String>::new();
153        let _ctx = RuntimeContext::with_all_features_filtered(|n| {
154            names.push(n.to_string());
155            true
156        });
157        assert!(names.is_empty());
158    }
159}