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}