Skip to main content

truce_core/
export.rs

1use std::sync::Arc;
2
3use crate::plugin::PluginRuntime;
4use truce_params::{ParamInfo, Params};
5
6/// Unified export trait for all plugin formats.
7///
8/// Implement this once on your plugin type. All format wrappers
9/// (CLAP, VST3, AU, standalone) use this to construct your plugin
10/// and access its parameters.
11///
12/// ```ignore
13/// impl PluginExport for MyPlugin {
14///     type Params = MyParams;
15///     fn create() -> Self { Self::new() }
16///     fn params(&self) -> &MyParams { &self.params }
17///     fn params_arc(&self) -> Arc<MyParams> { self.params.clone() }
18/// }
19/// ```
20///
21/// All parameter mutation goes through the atomic-backed accessors on
22/// `&Params` - no `&mut Params` accessor is required, which keeps the
23/// trait usable while the editor holds an `Arc<Params>` reader.
24pub trait PluginExport: PluginRuntime + Sized {
25    type Params: Params;
26
27    /// Construct a new instance of the plugin.
28    fn create() -> Self;
29
30    /// Immutable access to the parameter struct.
31    fn params(&self) -> &Self::Params;
32
33    /// Get a shared `Arc` reference to the parameter struct.
34    ///
35    /// Used by format wrappers to pass params to GUI closures without
36    /// raw pointers. The Arc is cloned (cheap ref-count bump), not the
37    /// params themselves.
38    fn params_arc(&self) -> Arc<Self::Params>;
39
40    /// Shared meter storage handle, mirroring [`Self::params_arc`]:
41    /// the audio thread publishes meter values into this store from
42    /// inside `process()` (via the shells' meter callback), and GUI
43    /// `get_meter` closures read it - never the plugin instance,
44    /// whose `&mut` the audio thread holds for the whole block.
45    ///
46    /// The `truce::plugin!` shells own the store and return their
47    /// handle here; a hand-written `PluginExport` impl keeps one
48    /// alongside its params `Arc`.
49    fn meter_store(&self) -> Arc<crate::meters::MeterStore>;
50
51    /// Shared snapshot slot for lock-free state save. The shell (audio
52    /// thread) publishes the plugin's custom state into it after each
53    /// block when the plugin overrides `snapshot_into`; the wrapper's
54    /// `save_state` reads it without taking the plugin lock. A plugin
55    /// that doesn't opt in never publishes, so the slot stays empty and
56    /// `save_state` falls back to the locked path.
57    ///
58    /// The `truce::plugin!` shells own the slot and return their handle
59    /// here; a hand-written impl keeps one alongside its params `Arc`.
60    fn snapshot_slot(&self) -> Arc<crate::snapshot::SnapshotSlot>;
61
62    /// A lock-free editor builder: hand it the param store and it
63    /// returns the editor (or `None` for a headless plugin).
64    ///
65    /// Called once at instance creation - format wrappers cache the
66    /// returned closure *outside* the plugin lock (alongside
67    /// `params_arc`), then invoke it when the host opens the GUI, so
68    /// editor construction never takes the plugin lock and never waits
69    /// on an in-flight audio block. The closure binds only the
70    /// lock-free param store, so a `--shell` build's closure rebuilds
71    /// from the *reloaded* dylib (GUI hot-reload survives, picked up on
72    /// the next editor close+open). The `truce::plugin!` shells provide
73    /// this; a hand-written impl returns a closure that builds its
74    /// editor directly. Default: a closure that returns `None`.
75    fn editor_builder(&self) -> crate::editor::EditorBuilder<Self::Params> {
76        Box::new(|_params| None)
77    }
78
79    /// Static parameter metadata for registration-time access.
80    ///
81    /// Format wrappers' `register_*` paths (`truce-vst2`, `truce-vst3`,
82    /// `truce-au`, `truce-aax`) call this instead of the historical
83    /// `Self::create().params().param_infos()` walk, which constructed
84    /// a full plugin instance - including any allocation the
85    /// constructor did (DSP buffers, FFT plans, image atlases) - just
86    /// to read static metadata. On platforms where registration runs
87    /// from C++ static initializers (notably AAX `Describe`) those
88    /// allocations sit in a fragile init-order regime; avoiding them
89    /// closes a class of platform-dependent registration bugs.
90    ///
91    /// Default impl prefers
92    /// [`Params::param_infos_static`]
93    /// when it returns a non-empty vec (the `#[derive(Params)]` path
94    /// emits an override built from compile-time metadata) and falls
95    /// back to the runtime construction otherwise - so plugins with
96    /// hand-written `Params` impls that don't override the static
97    /// path keep working unchanged.
98    #[must_use]
99    fn param_infos_static() -> Vec<ParamInfo> {
100        let from_params = <Self::Params as Params>::param_infos_static();
101        if from_params.is_empty() {
102            Self::create().params().param_infos()
103        } else {
104            from_params
105        }
106    }
107
108    /// Static "does this plugin have an editor" predicate. AAX's
109    /// `Describe` path needs to know this at registration time
110    /// (`has_editor` field on the static descriptor). Paired with
111    /// [`Self::param_infos_static`], this is the second reason every
112    /// format's registration walk constructed a plugin.
113    ///
114    /// Default impl falls back to that runtime path so unannotated
115    /// plugins keep working. Plugins that want to avoid the
116    /// static-init plugin construction (notably for AAX hosts that
117    /// run `Describe` very early) override with a `const`-style
118    /// answer:
119    ///
120    /// ```ignore
121    /// impl PluginExport for MyPlugin {
122    ///     // ...
123    ///     fn has_editor_static() -> bool { true }
124    /// }
125    /// ```
126    ///
127    /// VST2 / VST3 / AU never call this - they don't need the answer
128    /// at registration time.
129    #[must_use]
130    fn has_editor_static() -> bool {
131        let plugin = Self::create();
132        plugin.editor_builder()(plugin.params_arc()).is_some()
133    }
134}