Skip to main content

truce_loader/
lib.rs

1//! Hot-reload mechanics for truce: dylib loading, ABI canary, and the
2//! shells (`HotShell<P, S>`, `StaticShell<P, L, S>`) that bridge the
3//! user-facing `truce_plugin::PluginLogic` / `truce_plugin::PluginLogic64`
4//! leaf traits onto [`truce_core::PluginRuntime`] for format wrappers.
5//!
6//! Plugin authors don't reach into this crate directly. They write
7//! `impl PluginLogic for MyPlugin` (the leaf trait is sample-pinned
8//! via the prelude re-export) and the `truce::plugin!` macro picks
9//! the static or hot shell based on the `shell` Cargo feature.
10//!
11//! # ABI boundary
12//!
13//! `PluginLogic` is a stateless descriptor with a separate `type DspState`,
14//! so the DSP state can live in the *shell* rather than the reloadable
15//! dylib. The dylib exports a flat set of Rust-ABI functions
16//! (`export_plugin!`) over an opaque `*mut ()` state pointer (an erased
17//! `Box<State>`) plus the shell's `Arc<Params>` pointer. `HotShell` owns
18//! the state and, on a reload, carries it into the new code by a save /
19//! load round-trip through the plugin's persistence blob (when
20//! `PRESERVE_DSP_STATE` is set) - so a reverb tail survives the swap
21//! whenever the plugin serializes it - and re-inits otherwise.
22//! `StaticShell` holds a typed `L::DspState` directly.
23//!
24//! ```ignore
25//! use truce_loader::{AbiCanary, PluginLogic, PluginLogicCore};
26//!
27//! struct MyPlugin;                 // stateless descriptor
28//! impl PluginLogic for MyPlugin { type DspState = MyState; /* ... */ }
29//!
30//! // Emitted by `truce::plugin!` (plugin authors don't write these).
31//! // `Sample` resolves through the prelude alias (`f32` for `prelude` /
32//! // `prelude32` / `prelude64m`, `f64` for `prelude64`).
33//! #[unsafe(no_mangle)]
34//! pub fn truce_init_state(params: *const ()) -> *mut () { /* Box<State> */ }
35//! #[unsafe(no_mangle)]
36//! pub fn truce_process(state: *mut (), params: *const (), /* ... */) { }
37//!
38//! #[unsafe(no_mangle)]
39//! pub fn truce_abi_canary_v2() -> AbiCanary { AbiCanary::current::<Sample>() }
40//! ```
41
42#[doc(hidden)]
43pub mod __macro_deps {
44    pub use truce_core;
45    // `truce_plugin` carries the `PluginLogicCore` blanket the
46    // `export_plugin!` / `export_static!` macros need to name
47    // (`<L as PluginLogicCore<Sample>>::supports_in_place()` etc.).
48    // Re-exported here so the macro can resolve it via
49    // `$crate::__macro_deps::truce_plugin` regardless of whether the
50    // caller has `truce-plugin` as a direct dep.
51    pub use truce_plugin;
52}
53
54mod canary;
55mod safe_types;
56
57#[cfg(feature = "shell")]
58mod loader;
59#[cfg(feature = "shell")]
60pub mod shell;
61pub mod static_shell;
62
63pub use canary::{ABI_EPOCH, AbiCanary};
64pub use safe_types::*;
65// Source the leaf + core traits directly from `truce-plugin` rather
66// than via the optional `truce-gui` re-export, so these names are
67// reachable regardless of whether the `builtin-gui` feature is on.
68pub use truce_plugin::{PluginLogic, PluginLogic64, PluginLogicCore};
69
70#[cfg(feature = "shell")]
71pub use loader::NativeLoader;
72
73/// Export the `#[unsafe(no_mangle)]` symbols the hot-reload shell binds.
74///
75/// The dylib no longer hands the shell a `Box<dyn PluginLogicCore>`
76/// trait object. Instead it exports a flat set of Rust-ABI functions
77/// that operate on an **opaque state pointer** (`*mut ()`, an erased
78/// `Box<State>`): the shell owns the state, so on a hot-reload it can
79/// serialize the state through the origin dylib and restore it into
80/// freshly-init'd state under the new code (gated by the
81/// `truce_preserve_dsp_state` export).
82///
83/// `params_ptr` is a raw `Arc<Params>` pointer from the shell; each call
84/// borrows `&Params` from it (no refcount change - the shell keeps the
85/// `Arc` alive for the call's duration). `Sample` is the prelude's
86/// `type Sample` alias (`f32` for `prelude` / `prelude32` / `prelude64m`,
87/// `f64` for `prelude64`); the canary's `sample_precision` byte guards a
88/// precision-mismatched load.
89#[macro_export]
90macro_rules! export_plugin {
91    ($logic:ty, $params:ty) => {
92        /// Build the initial DSP state; returns an erased `Box<State>`.
93        #[unsafe(no_mangle)]
94        pub fn truce_init_state(params_ptr: *const ()) -> *mut () {
95            let params: &$params = unsafe { &*(params_ptr as *const $params) };
96            // Background tasks are not yet wired through the hot-reload
97            // dylib boundary; pass an empty context (no spawner).
98            let cx = $crate::__macro_deps::truce_core::tasks::InitContext::new(
99                ::core::option::Option::None,
100            );
101            let state = <$logic as $crate::PluginLogicCore<Sample>>::init(params, &cx);
102            Box::into_raw(Box::new(state)).cast::<()>()
103        }
104
105        /// Drop a state allocated by *this* dylib's `truce_init_state`.
106        /// Called by the shell through the origin dylib (kept alive by
107        /// the loader's leaked-handle policy) so `State`'s `Drop` runs
108        /// with the code that produced it.
109        #[unsafe(no_mangle)]
110        pub fn truce_drop_state(state: *mut ()) {
111            drop(unsafe {
112                Box::from_raw(state.cast::<<$logic as $crate::PluginLogicCore<Sample>>::DspState>())
113            });
114        }
115
116        /// Whether the shell should carry live DSP state across a reload
117        /// (the plugin's `PRESERVE_DSP_STATE`). Carry-over is a save / load
118        /// round-trip through this dylib's persistence blob, never a raw
119        /// reinterpretation of the old bytes, so it stays sound even when
120        /// the `State` layout changed between builds.
121        #[unsafe(no_mangle)]
122        pub fn truce_preserve_dsp_state() -> bool {
123            <$logic as $crate::PluginLogicCore<Sample>>::PRESERVE_DSP_STATE
124        }
125
126        #[unsafe(no_mangle)]
127        pub fn truce_reset(
128            state: *mut (),
129            params_ptr: *const (),
130            config: &$crate::__macro_deps::truce_core::config::AudioConfig,
131        ) {
132            let state = unsafe {
133                &mut *state.cast::<<$logic as $crate::PluginLogicCore<Sample>>::DspState>()
134            };
135            let params: &$params = unsafe { &*(params_ptr as *const $params) };
136            <$logic as $crate::PluginLogicCore<Sample>>::reset(state, params, config);
137        }
138
139        #[unsafe(no_mangle)]
140        pub fn truce_process(
141            state: *mut (),
142            params_ptr: *const (),
143            buffer: &mut $crate::__macro_deps::truce_core::buffer::AudioBuffer<Sample>,
144            events: &$crate::__macro_deps::truce_core::events::EventList,
145            ctx: &mut $crate::__macro_deps::truce_core::process::ProcessContext,
146        ) -> $crate::__macro_deps::truce_core::process::ProcessStatus {
147            let state = unsafe {
148                &mut *state.cast::<<$logic as $crate::PluginLogicCore<Sample>>::DspState>()
149            };
150            let params: &$params = unsafe { &*(params_ptr as *const $params) };
151            <$logic as $crate::PluginLogicCore<Sample>>::process(state, params, buffer, events, ctx)
152        }
153
154        #[unsafe(no_mangle)]
155        pub fn truce_latency(state: *const ()) -> u32 {
156            let state =
157                unsafe { &*state.cast::<<$logic as $crate::PluginLogicCore<Sample>>::DspState>() };
158            <$logic as $crate::PluginLogicCore<Sample>>::latency(state)
159        }
160
161        #[unsafe(no_mangle)]
162        pub fn truce_tail(state: *const ()) -> u32 {
163            let state =
164                unsafe { &*state.cast::<<$logic as $crate::PluginLogicCore<Sample>>::DspState>() };
165            <$logic as $crate::PluginLogicCore<Sample>>::tail(state)
166        }
167
168        #[unsafe(no_mangle)]
169        pub fn truce_save_state(state: *const ()) -> Vec<u8> {
170            let state =
171                unsafe { &*state.cast::<<$logic as $crate::PluginLogicCore<Sample>>::DspState>() };
172            <$logic as $crate::PluginLogicCore<Sample>>::save_state(state)
173        }
174
175        #[unsafe(no_mangle)]
176        pub fn truce_snapshot_into(state: *const (), buf: &mut Vec<u8>) -> bool {
177            let state =
178                unsafe { &*state.cast::<<$logic as $crate::PluginLogicCore<Sample>>::DspState>() };
179            <$logic as $crate::PluginLogicCore<Sample>>::snapshot_into(state, buf)
180        }
181
182        #[unsafe(no_mangle)]
183        pub fn truce_snapshot_version(state: *const ()) -> ::core::option::Option<u64> {
184            let state =
185                unsafe { &*state.cast::<<$logic as $crate::PluginLogicCore<Sample>>::DspState>() };
186            <$logic as $crate::PluginLogicCore<Sample>>::snapshot_version(state)
187        }
188
189        #[unsafe(no_mangle)]
190        pub fn truce_load_state(
191            state: *mut (),
192            data: &[u8],
193        ) -> Result<(), $crate::__macro_deps::truce_core::state::StateLoadError> {
194            let state = unsafe {
195                &mut *state.cast::<<$logic as $crate::PluginLogicCore<Sample>>::DspState>()
196            };
197            <$logic as $crate::PluginLogicCore<Sample>>::load_state(state, data)
198        }
199
200        #[unsafe(no_mangle)]
201        pub fn truce_state_changed(state: *mut (), params_ptr: *const ()) {
202            let state = unsafe {
203                &mut *state.cast::<<$logic as $crate::PluginLogicCore<Sample>>::DspState>()
204            };
205            let params: &$params = unsafe { &*(params_ptr as *const $params) };
206            <$logic as $crate::PluginLogicCore<Sample>>::state_changed(state, params);
207        }
208
209        // Editor construction lives in its own symbol: it is
210        // receiverless (over the shared `Arc<Params>`), so the shell
211        // rebuilds the editor from this dylib's `$logic` without
212        // touching the DSP state. A reload swaps in the new editor
213        // code - the host picks it up on the next editor close+open.
214        #[unsafe(no_mangle)]
215        pub fn truce_build_editor(
216            params_ptr: *const (),
217        ) -> Box<dyn $crate::__macro_deps::truce_core::editor::Editor> {
218            let params: Arc<$params> = unsafe {
219                Arc::increment_strong_count(params_ptr as *const $params);
220                Arc::from_raw(params_ptr as *const $params)
221            };
222            <$logic as $crate::__macro_deps::truce_plugin::PluginEditor<Sample>>::editor(params)
223        }
224
225        // `_v2` because `AbiCanary` crosses this boundary *by value*
226        // (sret): if the two sides disagreed about its size, the call
227        // itself would corrupt the caller's stack before any field
228        // compare. A canary-layout change therefore renames the symbol,
229        // so a mismatched pair fails at `dlsym` - cleanly.
230        #[unsafe(no_mangle)]
231        pub fn truce_abi_canary_v2() -> $crate::AbiCanary {
232            $crate::AbiCanary::current::<Sample>()
233        }
234    };
235}