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