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