1use std::cell::RefCell;
5use std::mem::{align_of, size_of};
6use std::ptr;
7
8use truce_core::buffer::AudioBuffer;
9use truce_core::events::{Event, EventBody, EventList, TransportInfo as Transport};
10use truce_core::process::{ProcessContext, ProcessStatus};
11use truce_gui_types::interaction::WidgetRegion;
16use truce_gui_types::layout::GridLayout;
17use truce_gui_types::theme::{Color, Theme};
18use truce_params::sample::Sample;
19use truce_plugin::PluginLogicCore;
20
21pub const ABI_EPOCH: u32 = 2;
35
36#[repr(C)]
41pub struct AbiCanary {
42 pub abi_epoch: u32,
45 pub trait_object_size: usize,
46 pub audio_buffer_size: usize,
47 pub process_context_size: usize,
48 pub process_status_size: usize,
49 pub event_size: usize,
50 pub event_body_size: usize,
51 pub transport_size: usize,
52 pub widget_region_size: usize,
53 pub theme_size: usize,
54 pub plugin_layout_size: usize,
55 pub color_size: usize,
56 pub vec_u8_size: usize,
57 pub option_usize_size: usize,
58 pub audio_buffer_align: usize,
59 pub process_status_align: usize,
60 pub result_normal_disc: u8,
61 pub result_tail_disc: u8,
62 pub result_keepalive_disc: u8,
63 pub rustc_version_hash: u64,
64 pub sample_precision: u8,
73}
74
75impl AbiCanary {
76 #[must_use]
81 pub fn current<S: truce_params::sample::Sample>() -> Self {
82 #[allow(clippy::cast_possible_truncation)]
85 let sample_precision = (size_of::<S>() * 8) as u8;
86 Self {
87 abi_epoch: ABI_EPOCH,
88 trait_object_size: size_of::<*const dyn PluginLogicCore<S>>() * 2,
89 audio_buffer_size: size_of::<AudioBuffer<S>>(),
90 process_context_size: size_of::<ProcessContext>(),
91 process_status_size: size_of::<ProcessStatus>(),
92 event_size: size_of::<Event>(),
93 event_body_size: size_of::<EventBody>(),
94 transport_size: size_of::<Transport>(),
95 widget_region_size: size_of::<WidgetRegion>(),
96 theme_size: size_of::<Theme>(),
97 plugin_layout_size: size_of::<GridLayout>(),
98 color_size: size_of::<Color>(),
99 vec_u8_size: size_of::<Vec<u8>>(),
100 option_usize_size: size_of::<Option<usize>>(),
101 audio_buffer_align: align_of::<AudioBuffer<S>>(),
102 process_status_align: align_of::<ProcessStatus>(),
103 result_normal_disc: discriminant_byte(&ProcessStatus::Normal),
104 result_tail_disc: discriminant_byte(&ProcessStatus::Tail(0)),
105 result_keepalive_disc: discriminant_byte(&ProcessStatus::KeepAlive),
106 rustc_version_hash: rustc_hash(),
107 sample_precision,
108 }
109 }
110
111 #[must_use]
112 pub fn matches(&self, other: &Self) -> bool {
113 self.field_diffs(other).is_empty()
114 }
115
116 #[must_use]
117 pub fn diff_report(&self, other: &Self) -> String {
118 let diffs = self.field_diffs(other);
119 if diffs.is_empty() {
120 "no differences".into()
121 } else {
122 format!("ABI mismatches:\n{}", diffs.join("\n"))
123 }
124 }
125
126 fn field_diffs(&self, other: &Self) -> Vec<String> {
127 let mut diffs = Vec::new();
128 macro_rules! check {
129 ($field:ident) => {
130 if self.$field != other.$field {
131 diffs.push(format!(
132 " {}: shell={}, dylib={}",
133 stringify!($field),
134 self.$field,
135 other.$field
136 ));
137 }
138 };
139 }
140 check!(abi_epoch);
144 check!(trait_object_size);
145 check!(audio_buffer_size);
146 check!(process_context_size);
147 check!(process_status_size);
148 check!(event_size);
149 check!(event_body_size);
150 check!(transport_size);
151 check!(widget_region_size);
152 check!(theme_size);
153 check!(plugin_layout_size);
154 check!(color_size);
155 check!(vec_u8_size);
156 check!(option_usize_size);
157 check!(audio_buffer_align);
158 check!(process_status_align);
159 check!(result_normal_disc);
160 check!(result_tail_disc);
161 check!(result_keepalive_disc);
162 check!(rustc_version_hash);
163 check!(sample_precision);
164 diffs
165 }
166}
167
168fn discriminant_byte<T>(value: &T) -> u8 {
169 unsafe { *ptr::from_ref::<T>(value).cast::<u8>() }
179}
180
181fn rustc_hash() -> u64 {
182 env!("TRUCE_RUSTC_HASH").parse().unwrap_or(0)
183}
184
185#[derive(Default)]
200pub struct ProbePlugin {
201 last_load_state: RefCell<Vec<u8>>,
202}
203
204impl<S: Sample> PluginLogicCore<S> for ProbePlugin {
205 fn supports_in_place() -> bool
206 where
207 Self: Sized,
208 {
209 false
210 }
211
212 fn bus_layouts() -> Vec<truce_core::bus::BusLayout>
213 where
214 Self: Sized,
215 {
216 vec![truce_core::bus::BusLayout::stereo()]
217 }
218
219 fn reset(&mut self, _sr: f64, _bs: usize) {}
220
221 fn process(
222 &mut self,
223 _buffer: &mut AudioBuffer<S>,
224 _events: &EventList,
225 _context: &mut ProcessContext,
226 ) -> ProcessStatus {
227 ProcessStatus::Normal
228 }
229
230 fn save_state(&self) -> Vec<u8> {
231 let cached = self.last_load_state.borrow();
235 if cached.is_empty() {
236 vec![0xCA, 0xFE]
237 } else {
238 cached.clone()
239 }
240 }
241 fn load_state(&mut self, data: &[u8]) -> Result<(), truce_core::state::StateLoadError> {
242 *self.last_load_state.borrow_mut() = data.to_vec();
243 Ok(())
244 }
245 fn state_changed(&mut self) {}
246 fn migrate_state(
247 _foreign: &truce_core::state::ForeignState,
248 ) -> Option<truce_core::state::MigratedState>
249 where
250 Self: Sized,
251 {
252 None
254 }
255 fn latency(&self) -> u32 {
256 0xAAAA
257 }
258 fn tail(&self) -> u32 {
259 0xBBBB
260 }
261
262 fn editor(&self) -> Box<dyn truce_core::editor::Editor> {
263 struct UnreachableEditor;
268 impl truce_core::editor::Editor for UnreachableEditor {
269 fn size(&self) -> (u32, u32) {
270 unreachable!("probe editor was opened by accident")
271 }
272 fn open(
273 &mut self,
274 _: truce_core::editor::RawWindowHandle,
275 _: truce_core::editor::PluginContext,
276 ) {
277 unreachable!("probe editor was opened by accident")
278 }
279 fn close(&mut self) {}
280 fn idle(&mut self) {}
281 }
282 Box::new(UnreachableEditor)
283 }
284}
285
286#[cfg(feature = "shell")]
307pub fn verify_probe<S: Sample>(probe: &mut dyn PluginLogicCore<S>) -> Result<(), ProbeError> {
308 if probe.latency() != 0xAAAA {
309 return Err(ProbeError::Latency {
310 expected: 0xAAAA,
311 actual: probe.latency(),
312 });
313 }
314 if probe.tail() != 0xBBBB {
315 return Err(ProbeError::Tail {
316 expected: 0xBBBB,
317 actual: probe.tail(),
318 });
319 }
320 if probe.save_state() != vec![0xCA, 0xFE] {
321 return Err(ProbeError::SaveStateDefault);
322 }
323 let sentinel = vec![0xDEu8, 0xAD, 0xBE, 0xEF];
326 probe
327 .load_state(&sentinel)
328 .map_err(ProbeError::LoadStateFailed)?;
329 if probe.save_state() != sentinel {
330 return Err(ProbeError::LoadSaveRoundTrip);
331 }
332 Ok(())
333}
334
335#[cfg(feature = "shell")]
339#[derive(Debug)]
340pub enum ProbeError {
341 Latency { expected: u32, actual: u32 },
343 Tail { expected: u32, actual: u32 },
345 SaveStateDefault,
348 LoadStateFailed(truce_core::state::StateLoadError),
351 LoadSaveRoundTrip,
354}
355
356#[cfg(feature = "shell")]
357impl std::fmt::Display for ProbeError {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 match self {
360 Self::Latency { expected, actual } => {
361 write!(f, "latency: expected 0x{expected:X}, got 0x{actual:X}")
362 }
363 Self::Tail { expected, actual } => {
364 write!(f, "tail: expected 0x{expected:X}, got 0x{actual:X}")
365 }
366 Self::SaveStateDefault => f.write_str("save_state (default): expected [0xCA, 0xFE]"),
367 Self::LoadStateFailed(e) => write!(f, "load_state probe: {e}"),
368 Self::LoadSaveRoundTrip => f.write_str("load_state/save_state round-trip mismatch"),
369 }
370 }
371}
372
373#[cfg(feature = "shell")]
374impl std::error::Error for ProbeError {}
375
376#[cfg(test)]
377mod tests {
378 use super::{ABI_EPOCH, AbiCanary};
379
380 #[test]
381 fn same_build_matches_itself() {
382 let a = AbiCanary::current::<f32>();
383 let b = AbiCanary::current::<f32>();
384 assert!(a.matches(&b));
385 }
386
387 #[test]
388 fn epoch_mismatch_fails_and_names_the_field() {
389 let a = AbiCanary::current::<f32>();
392 let mut b = AbiCanary::current::<f32>();
393 b.abi_epoch = ABI_EPOCH - 1;
394 assert!(!a.matches(&b));
395 assert!(a.diff_report(&b).contains("abi_epoch"));
396 }
397
398 #[test]
399 fn precision_mismatch_fails() {
400 let a = AbiCanary::current::<f32>();
401 let b = AbiCanary::current::<f64>();
402 assert!(!a.matches(&b));
403 }
404}