truce_loader/lib.rs
1//! Hot-reload mechanics for truce: dylib loading, ABI canary,
2//! vtable probe, and the shells (`HotShell<P, S>`, `StaticShell<P, L, S>`)
3//! that bridge the user-facing `truce_plugin::PluginLogic` /
4//! `truce_plugin::PluginLogic64` leaf traits onto
5//! [`truce_core::PluginRuntime`] for format wrappers.
6//!
7//! Plugin authors don't reach into this crate directly. They write
8//! `impl PluginLogic for MyPlugin` (the leaf trait is sample-pinned
9//! via the prelude re-export) and the `truce::plugin!` macro picks
10//! the static or hot shell based on the `shell` Cargo feature.
11//!
12//! # ABI boundary
13//!
14//! Across the dylib boundary the shell holds a
15//! `Box<dyn truce_plugin::PluginLogicCore<S>>` - the generic
16//! wrapper-facing trait that both leaf traits forward into via
17//! blanket impls in `truce-plugin`. The single trait object
18//! carries DSP and GUI methods through one vtable, with `S` baked
19//! in by the shell's generic parameter (and recorded in
20//! `AbiCanary::sample_precision` so a precision mismatch fails
21//! the canary check before vtable-binding).
22//!
23//! ```ignore
24//! use truce_loader::{AbiCanary, PluginLogic, PluginLogicCore};
25//!
26//! struct MyPlugin { /* ... */ }
27//! impl PluginLogic for MyPlugin { /* DSP + GUI */ }
28//!
29//! // Emitted by `truce::plugin!`; plugin authors don't write these
30//! // by hand. `Sample` resolves through the prelude alias
31//! // (`f32` for `prelude` / `prelude32` / `prelude64m`,
32//! // `f64` for `prelude64`). The macro also emits a
33//! // `truce_vtable_probe` symbol that constructs an internal
34//! // `ProbePlugin`; that type lives in `__macro_deps` and isn't
35//! // intended for direct use.
36//! #[unsafe(no_mangle)]
37//! pub fn truce_create(p: *const ()) -> Box<dyn PluginLogicCore<Sample>> {
38//! Box::new(MyPlugin::new(/* params from p */))
39//! }
40//!
41//! #[unsafe(no_mangle)]
42//! pub fn truce_abi_canary() -> AbiCanary { AbiCanary::current::<Sample>() }
43//! ```
44
45#[doc(hidden)]
46pub mod __macro_deps {
47 pub use truce_core;
48 // `truce_plugin` carries the `PluginLogicCore` blanket the
49 // `export_plugin!` / `export_static!` macros need to name
50 // (`<L as PluginLogicCore<Sample>>::supports_in_place()` etc.).
51 // Re-exported here so the macro can resolve it via
52 // `$crate::__macro_deps::truce_plugin` regardless of whether the
53 // caller has `truce-plugin` as a direct dep.
54 pub use truce_plugin;
55 // `ProbePlugin` is a vtable-binding shim emitted into every
56 // `export_plugin!` expansion as the `truce_vtable_probe` symbol.
57 // It is not a public type; plugin authors never name it. Kept
58 // reachable here under `$crate::__macro_deps::` so the macro can
59 // resolve it without leaking the type at the crate root.
60 pub use crate::canary::ProbePlugin;
61}
62
63mod canary;
64mod safe_types;
65
66#[cfg(feature = "shell")]
67mod loader;
68#[cfg(feature = "shell")]
69pub mod shell;
70pub mod static_shell;
71
72pub use canary::AbiCanary;
73// `ProbePlugin`, `verify_probe`, and `ProbeError` are loader-internal.
74// `ProbePlugin` lives under `__macro_deps` so the `export_plugin!`
75// macro's `truce_vtable_probe` body can name it without leaking the
76// type at the crate root. `verify_probe` / `ProbeError` are only used
77// by `NativeLoader::build_candidate` (gated on `feature = "shell"`)
78// and are reached via `crate::canary` directly inside `loader.rs`.
79// Format wrappers and plugin authors reach the probe by dlopening
80// the `truce_vtable_probe` symbol the macro emits, not by `use` import.
81pub use safe_types::*;
82// Source the leaf + core traits directly from `truce-plugin` rather
83// than via the optional `truce-gui` re-export, so these names are
84// reachable regardless of whether the `builtin-gui` feature is on.
85pub use truce_plugin::{PluginLogic, PluginLogic64, PluginLogicCore};
86
87#[cfg(feature = "shell")]
88pub use loader::NativeLoader;
89
90/// Export the `#[unsafe(no_mangle)]` functions required by the shell.
91///
92/// `params_ptr` is a raw `Arc<Params>` pointer from the shell.
93/// The plugin receives shared params - one copy, no sync.
94#[macro_export]
95macro_rules! export_plugin {
96 ($logic:ty, $params:ty) => {
97 // `Sample` here is the prelude's `type Sample = ...` alias:
98 // `f32` for `prelude` / `prelude32` / `prelude64m`, `f64` for
99 // `prelude64`. `HotShell<P, S>` is generic over `S: Sample`
100 // so both precisions hot-reload through the same dylib export
101 // shape; the canary's `sample_precision` byte at load time
102 // guards against a shell built for one precision dlopening a
103 // dylib built for the other.
104 #[unsafe(no_mangle)]
105 pub fn truce_create(params_ptr: *const ()) -> Box<dyn $crate::PluginLogicCore<Sample>> {
106 let params: Arc<$params> = unsafe {
107 Arc::increment_strong_count(params_ptr as *const $params);
108 Arc::from_raw(params_ptr as *const $params)
109 };
110 // The plugin impls one of the leaf traits
111 // (`PluginLogic` for f32 or `PluginLogic64` for f64); the
112 // blanket impl inside `truce-plugin` gives it
113 // `PluginLogicCore<Sample>` automatically, so the cast
114 // here just picks the right vtable.
115 Box::new(<$logic>::new(params))
116 }
117
118 #[unsafe(no_mangle)]
119 pub fn truce_abi_canary() -> $crate::AbiCanary {
120 // `Sample` from the prelude - the dylib stamps its
121 // chosen precision into the canary so the shell can
122 // reject a mismatched load before vtable-binding.
123 $crate::AbiCanary::current::<Sample>()
124 }
125
126 #[unsafe(no_mangle)]
127 pub fn truce_vtable_probe() -> Box<dyn $crate::PluginLogicCore<Sample>> {
128 Box::new($crate::__macro_deps::ProbePlugin::default())
129 }
130 };
131}