Skip to main content

truce_loader/
static_shell.rs

1//! `StaticShell` - embeds the plugin directly into the binary.
2//!
3//! No dlopen, no file watcher, no Mutex. Same types as `HotShell`
4//! but zero runtime overhead. Use via `export_static!`.
5
6use std::sync::Arc;
7use std::sync::atomic::{AtomicU32, Ordering};
8
9use truce_core::buffer::AudioBuffer;
10use truce_core::bus::BusLayout;
11use truce_core::editor::Editor;
12use truce_core::events::{EventBody, EventList};
13use truce_core::info::PluginInfo;
14use truce_core::plugin::PluginRuntime;
15use truce_core::process::{ProcessContext, ProcessStatus};
16use truce_params::Params;
17use truce_params::sample::Sample;
18use truce_plugin::PluginLogicCore;
19
20// ---------------------------------------------------------------------------
21// StaticShell
22// ---------------------------------------------------------------------------
23
24/// A static plugin shell that embeds the user's `PluginLogic` impl
25/// directly into the format-wrapper binary.
26///
27/// Same bridging as `HotShell` but without `NativeLoader`, `Mutex`,
28/// file watching, or any dynamic loading overhead. Use via `export_static!`.
29pub struct StaticShell<P: Params, L: PluginLogicCore<S>, S: Sample = f32> {
30    pub params: Arc<P>,
31    logic: L,
32    meters: Arc<[AtomicU32; 256]>,
33    sample_rate: f64,
34    _sample: std::marker::PhantomData<fn() -> S>,
35}
36
37// SAFETY: `StaticShell` owns `Arc<P>` (params, `Sync` by the
38// `Params` trait contract), `L` (the user's logic - `Send + 'static`
39// per the `PluginLogicCore` bound), an `AtomicU32`-backed meters
40// array, and a `PhantomData<fn() -> S>`. No raw pointers, no
41// `!Send` fields, no interior mutability that escapes the shell's
42// own `&mut` borrows. The host contract that format wrappers
43// invoke methods on a single thread at a time per instance is what
44// keeps the embedded `L` safe to access without an inner mutex -
45// same model `HotShell` uses through `parking_lot::Mutex`.
46unsafe impl<P: Params, L: PluginLogicCore<S>, S: Sample> Send for StaticShell<P, L, S> {}
47
48impl<P: Params + Default + 'static, L: PluginLogicCore<S> + 'static, S: Sample>
49    StaticShell<P, L, S>
50{
51    /// Create from pre-constructed parts. The plugin logic should
52    /// hold an `Arc::clone` of the same params.
53    pub fn from_parts(params: Arc<P>, logic: L) -> Self {
54        Self {
55            params,
56            logic,
57            meters: Arc::new(std::array::from_fn(|_| AtomicU32::new(0))),
58            sample_rate: 44100.0,
59            _sample: std::marker::PhantomData,
60        }
61    }
62
63    /// Access the plugin logic (for testing).
64    pub fn logic_ref(&self) -> &L {
65        &self.logic
66    }
67
68    /// Mutable access to the plugin logic (for testing).
69    pub fn logic_ref_mut(&mut self) -> &mut L {
70        &mut self.logic
71    }
72}
73
74impl<P: Params + Default + 'static, L: PluginLogicCore<S> + 'static, S: Sample> PluginRuntime
75    for StaticShell<P, L, S>
76{
77    type Sample = S;
78
79    fn info() -> PluginInfo
80    where
81        Self: Sized,
82    {
83        unreachable!("StaticShell::info() should not be called statically")
84    }
85
86    fn bus_layouts() -> Vec<BusLayout>
87    where
88        Self: Sized,
89    {
90        unreachable!("StaticShell::bus_layouts() should not be called statically")
91    }
92
93    fn init(&mut self) {}
94
95    fn reset(&mut self, sample_rate: f64, max_block_size: usize) {
96        self.sample_rate = sample_rate;
97        self.params.set_sample_rate(sample_rate);
98        self.logic.reset(sample_rate, max_block_size);
99    }
100
101    fn process(
102        &mut self,
103        buffer: &mut AudioBuffer<S>,
104        events: &EventList,
105        context: &mut ProcessContext,
106    ) -> ProcessStatus {
107        // Apply parameter change events to the shell's params.
108        // ParamChange values from format wrappers are PLAIN (already
109        // denormalized). `set_normalized` here would double-denormalize.
110        for e in events.iter() {
111            if let EventBody::ParamChange { id, value } = &e.body {
112                self.params.set_plain(*id, *value);
113            }
114        }
115
116        // No sync needed - plugin reads from the same Arc<Params>.
117
118        // Build a ProcessContext with param/meter callbacks for the logic.
119        let params = &self.params;
120        let meters = &self.meters;
121        let param_fn = |id: u32| -> f64 { params.get_plain(id).unwrap_or(0.0) };
122        let meter_fn = |id: u32, v: f32| {
123            // Meter IDs are offset by `truce_params::METER_ID_BASE`;
124            // mirror the offset in `get_meter` exactly.
125            let idx = id.wrapping_sub(truce_params::METER_ID_BASE) as usize;
126            if let Some(slot) = meters.get(idx) {
127                slot.store(v.to_bits(), Ordering::Relaxed);
128            }
129        };
130        let mut ctx = ProcessContext::new(
131            context.transport,
132            context.sample_rate,
133            buffer.num_samples(),
134            &mut *context.output_events,
135        )
136        .with_params(&param_fn)
137        .with_meters(&meter_fn);
138
139        self.logic.process(buffer, events, &mut ctx)
140    }
141
142    fn save_state(&self) -> Vec<u8> {
143        self.logic.save_state()
144    }
145
146    fn load_state(&mut self, data: &[u8]) -> Result<(), truce_core::state::StateLoadError> {
147        let result = self.logic.load_state(data);
148        // Plugin-side cache invalidation runs in the same `&mut`
149        // borrow window so the next `process()` block sees the
150        // refreshed caches - fire it whether or not load_state
151        // succeeded so partial state still triggers a refresh.
152        PluginLogicCore::state_changed(&mut self.logic);
153        result
154    }
155
156    fn editor(&mut self) -> Option<Box<dyn Editor>> {
157        Some(PluginLogicCore::editor(&self.logic))
158    }
159
160    fn latency(&self) -> u32 {
161        self.logic.latency()
162    }
163    fn tail(&self) -> u32 {
164        self.logic.tail()
165    }
166
167    fn get_meter(&self, meter_id: u32) -> f32 {
168        // Meter IDs live in a dedicated high range starting at
169        // `truce_params::METER_ID_BASE`; storage is offset into
170        // `self.meters`. `wrapping_sub` keeps out-of-range ids from
171        // panicking - they fall through to the `get` -> None path.
172        let idx = meter_id.wrapping_sub(truce_params::METER_ID_BASE) as usize;
173        if let Some(slot) = self.meters.get(idx) {
174            f32::from_bits(slot.load(Ordering::Relaxed))
175        } else {
176            0.0
177        }
178    }
179}
180
181// ---------------------------------------------------------------------------
182// export_static! macro
183// ---------------------------------------------------------------------------
184
185/// Compile-time static embedding of a `PluginLogic` impl into the binary.
186///
187/// Produces a `__HotShellWrapper` struct that implements `Plugin + PluginExport`,
188/// so format export macros (`export_clap!`, `export_vst3!`, etc.) work unchanged.
189/// No dlopen, no file watcher, zero runtime overhead. Bus layouts come from
190/// `<$logic as PluginLogic>::bus_layouts()` - override the trait method to
191/// pick something other than the stereo default.
192///
193/// ```ignore
194/// export_static! {
195///     params: GainParams,
196///     info: plugin_info!(...),
197///     logic: Gain,
198/// }
199///
200/// #[cfg(feature = "clap")]
201/// truce_clap::export_clap!(__HotShellWrapper);
202/// ```
203#[macro_export]
204macro_rules! export_static {
205    (
206        params: $params:ty,
207        info: $info:expr,
208        logic: $logic:ty,
209    ) => {
210        pub struct __HotShellWrapper {
211            // `Sample` here resolves to the type alias the user
212            // imported from a prelude (`prelude` / `prelude32` →
213            // `f32`; `prelude64` → `f64`; `prelude64m` → `f32`). The
214            // `PluginLogic<Sample>` bound on the user's impl must
215            // match this, so the prelude is what picks the audio
216            // buffer precision end-to-end.
217            inner: $crate::static_shell::StaticShell<$params, $logic, Sample>,
218        }
219
220        impl $crate::__macro_deps::truce_core::plugin::PluginRuntime for __HotShellWrapper {
221            type Sample = Sample;
222
223            fn supports_in_place() -> bool
224            where
225                Self: Sized,
226            {
227                // `PluginLogicCore<Sample>` is the wrapper-facing
228                // trait; the user impl'd one of the leaf traits
229                // (`PluginLogic` / `PluginLogic64`), and the blanket
230                // bridge defined alongside those traits in
231                // `truce-plugin` makes them also satisfy
232                // `PluginLogicCore<Sample>` automatically. Sample
233                // resolves through the prelude alias in scope at the
234                // macro call site.
235                <$logic as $crate::__macro_deps::truce_plugin::PluginLogicCore<Sample>>::supports_in_place()
236            }
237
238            fn info() -> $crate::__macro_deps::truce_core::info::PluginInfo
239            where
240                Self: Sized,
241            {
242                $info
243            }
244
245            fn bus_layouts() -> Vec<$crate::__macro_deps::truce_core::bus::BusLayout>
246            where
247                Self: Sized,
248            {
249                <$logic as $crate::__macro_deps::truce_plugin::PluginLogicCore<Sample>>::bus_layouts()
250            }
251
252            fn init(&mut self) {
253                self.inner.init();
254            }
255
256            fn reset(&mut self, sample_rate: f64, max_block_size: usize) {
257                self.inner.reset(sample_rate, max_block_size);
258            }
259
260            fn process(
261                &mut self,
262                buffer: &mut $crate::__macro_deps::truce_core::buffer::AudioBuffer<Sample>,
263                events: &$crate::__macro_deps::truce_core::events::EventList,
264                context: &mut $crate::__macro_deps::truce_core::process::ProcessContext,
265            ) -> $crate::__macro_deps::truce_core::process::ProcessStatus {
266                self.inner.process(buffer, events, context)
267            }
268
269            fn save_state(&self) -> Vec<u8> {
270                self.inner.save_state()
271            }
272
273            fn load_state(
274                &mut self,
275                data: &[u8],
276            ) -> Result<(), $crate::__macro_deps::truce_core::state::StateLoadError> {
277                self.inner.load_state(data)
278            }
279
280            fn editor(
281                &mut self,
282            ) -> Option<Box<dyn $crate::__macro_deps::truce_core::editor::Editor>> {
283                self.inner.editor()
284            }
285
286            fn latency(&self) -> u32 {
287                self.inner.latency()
288            }
289            fn tail(&self) -> u32 {
290                self.inner.tail()
291            }
292            fn get_meter(&self, meter_id: u32) -> f32 {
293                self.inner.get_meter(meter_id)
294            }
295        }
296
297        impl $crate::__macro_deps::truce_core::export::PluginExport for __HotShellWrapper {
298            type Params = $params;
299
300            fn create() -> Self {
301                let params = std::sync::Arc::new(<$params>::new());
302                let logic = <$logic>::new(std::sync::Arc::clone(&params));
303                Self {
304                    inner: $crate::static_shell::StaticShell::from_parts(params, logic),
305                }
306            }
307
308            fn params(&self) -> &$params {
309                &self.inner.params
310            }
311
312            fn params_arc(&self) -> std::sync::Arc<$params> {
313                std::sync::Arc::clone(&self.inner.params)
314            }
315        }
316    };
317}