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
21#[repr(C)]
26pub struct AbiCanary {
27 pub trait_object_size: usize,
28 pub audio_buffer_size: usize,
29 pub process_context_size: usize,
30 pub process_status_size: usize,
31 pub event_size: usize,
32 pub event_body_size: usize,
33 pub transport_size: usize,
34 pub widget_region_size: usize,
35 pub theme_size: usize,
36 pub plugin_layout_size: usize,
37 pub color_size: usize,
38 pub vec_u8_size: usize,
39 pub option_usize_size: usize,
40 pub audio_buffer_align: usize,
41 pub process_status_align: usize,
42 pub result_normal_disc: u8,
43 pub result_tail_disc: u8,
44 pub result_keepalive_disc: u8,
45 pub rustc_version_hash: u64,
46 pub sample_precision: u8,
55}
56
57impl AbiCanary {
58 #[must_use]
63 pub fn current<S: truce_params::sample::Sample>() -> Self {
64 #[allow(clippy::cast_possible_truncation)]
67 let sample_precision = (size_of::<S>() * 8) as u8;
68 Self {
69 trait_object_size: size_of::<*const dyn PluginLogicCore<S>>() * 2,
70 audio_buffer_size: size_of::<AudioBuffer<S>>(),
71 process_context_size: size_of::<ProcessContext>(),
72 process_status_size: size_of::<ProcessStatus>(),
73 event_size: size_of::<Event>(),
74 event_body_size: size_of::<EventBody>(),
75 transport_size: size_of::<Transport>(),
76 widget_region_size: size_of::<WidgetRegion>(),
77 theme_size: size_of::<Theme>(),
78 plugin_layout_size: size_of::<GridLayout>(),
79 color_size: size_of::<Color>(),
80 vec_u8_size: size_of::<Vec<u8>>(),
81 option_usize_size: size_of::<Option<usize>>(),
82 audio_buffer_align: align_of::<AudioBuffer<S>>(),
83 process_status_align: align_of::<ProcessStatus>(),
84 result_normal_disc: discriminant_byte(&ProcessStatus::Normal),
85 result_tail_disc: discriminant_byte(&ProcessStatus::Tail(0)),
86 result_keepalive_disc: discriminant_byte(&ProcessStatus::KeepAlive),
87 rustc_version_hash: rustc_hash(),
88 sample_precision,
89 }
90 }
91
92 #[must_use]
93 pub fn matches(&self, other: &Self) -> bool {
94 self.field_diffs(other).is_empty()
95 }
96
97 #[must_use]
98 pub fn diff_report(&self, other: &Self) -> String {
99 let diffs = self.field_diffs(other);
100 if diffs.is_empty() {
101 "no differences".into()
102 } else {
103 format!("ABI mismatches:\n{}", diffs.join("\n"))
104 }
105 }
106
107 fn field_diffs(&self, other: &Self) -> Vec<String> {
108 let mut diffs = Vec::new();
109 macro_rules! check {
110 ($field:ident) => {
111 if self.$field != other.$field {
112 diffs.push(format!(
113 " {}: shell={}, dylib={}",
114 stringify!($field),
115 self.$field,
116 other.$field
117 ));
118 }
119 };
120 }
121 check!(trait_object_size);
125 check!(audio_buffer_size);
126 check!(process_context_size);
127 check!(process_status_size);
128 check!(event_size);
129 check!(event_body_size);
130 check!(transport_size);
131 check!(widget_region_size);
132 check!(theme_size);
133 check!(plugin_layout_size);
134 check!(color_size);
135 check!(vec_u8_size);
136 check!(option_usize_size);
137 check!(audio_buffer_align);
138 check!(process_status_align);
139 check!(result_normal_disc);
140 check!(result_tail_disc);
141 check!(result_keepalive_disc);
142 check!(rustc_version_hash);
143 check!(sample_precision);
144 diffs
145 }
146}
147
148fn discriminant_byte<T>(value: &T) -> u8 {
149 unsafe { *ptr::from_ref::<T>(value).cast::<u8>() }
159}
160
161fn rustc_hash() -> u64 {
162 env!("TRUCE_RUSTC_HASH").parse().unwrap_or(0)
163}
164
165#[derive(Default)]
180pub struct ProbePlugin {
181 last_load_state: RefCell<Vec<u8>>,
182}
183
184impl<S: Sample> PluginLogicCore<S> for ProbePlugin {
185 fn supports_in_place() -> bool
186 where
187 Self: Sized,
188 {
189 false
190 }
191
192 fn bus_layouts() -> Vec<truce_core::bus::BusLayout>
193 where
194 Self: Sized,
195 {
196 vec![truce_core::bus::BusLayout::stereo()]
197 }
198
199 fn reset(&mut self, _sr: f64, _bs: usize) {}
200
201 fn process(
202 &mut self,
203 _buffer: &mut AudioBuffer<S>,
204 _events: &EventList,
205 _context: &mut ProcessContext,
206 ) -> ProcessStatus {
207 ProcessStatus::Normal
208 }
209
210 fn save_state(&self) -> Vec<u8> {
211 let cached = self.last_load_state.borrow();
215 if cached.is_empty() {
216 vec![0xCA, 0xFE]
217 } else {
218 cached.clone()
219 }
220 }
221 fn load_state(&mut self, data: &[u8]) -> Result<(), truce_core::state::StateLoadError> {
222 *self.last_load_state.borrow_mut() = data.to_vec();
223 Ok(())
224 }
225 fn state_changed(&mut self) {}
226 fn latency(&self) -> u32 {
227 0xAAAA
228 }
229 fn tail(&self) -> u32 {
230 0xBBBB
231 }
232
233 fn editor(&self) -> Box<dyn truce_core::editor::Editor> {
234 struct UnreachableEditor;
239 impl truce_core::editor::Editor for UnreachableEditor {
240 fn size(&self) -> (u32, u32) {
241 unreachable!("probe editor was opened by accident")
242 }
243 fn open(
244 &mut self,
245 _: truce_core::editor::RawWindowHandle,
246 _: truce_core::editor::PluginContext,
247 ) {
248 unreachable!("probe editor was opened by accident")
249 }
250 fn close(&mut self) {}
251 fn idle(&mut self) {}
252 }
253 Box::new(UnreachableEditor)
254 }
255}
256
257#[cfg(feature = "shell")]
278pub fn verify_probe<S: Sample>(probe: &mut dyn PluginLogicCore<S>) -> Result<(), ProbeError> {
279 if probe.latency() != 0xAAAA {
280 return Err(ProbeError::Latency {
281 expected: 0xAAAA,
282 actual: probe.latency(),
283 });
284 }
285 if probe.tail() != 0xBBBB {
286 return Err(ProbeError::Tail {
287 expected: 0xBBBB,
288 actual: probe.tail(),
289 });
290 }
291 if probe.save_state() != vec![0xCA, 0xFE] {
292 return Err(ProbeError::SaveStateDefault);
293 }
294 let sentinel = vec![0xDEu8, 0xAD, 0xBE, 0xEF];
297 probe
298 .load_state(&sentinel)
299 .map_err(ProbeError::LoadStateFailed)?;
300 if probe.save_state() != sentinel {
301 return Err(ProbeError::LoadSaveRoundTrip);
302 }
303 Ok(())
304}
305
306#[cfg(feature = "shell")]
310#[derive(Debug)]
311pub enum ProbeError {
312 Latency { expected: u32, actual: u32 },
314 Tail { expected: u32, actual: u32 },
316 SaveStateDefault,
319 LoadStateFailed(truce_core::state::StateLoadError),
322 LoadSaveRoundTrip,
325}
326
327#[cfg(feature = "shell")]
328impl std::fmt::Display for ProbeError {
329 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330 match self {
331 Self::Latency { expected, actual } => {
332 write!(f, "latency: expected 0x{expected:X}, got 0x{actual:X}")
333 }
334 Self::Tail { expected, actual } => {
335 write!(f, "tail: expected 0x{expected:X}, got 0x{actual:X}")
336 }
337 Self::SaveStateDefault => f.write_str("save_state (default): expected [0xCA, 0xFE]"),
338 Self::LoadStateFailed(e) => write!(f, "load_state probe: {e}"),
339 Self::LoadSaveRoundTrip => f.write_str("load_state/save_state round-trip mismatch"),
340 }
341 }
342}
343
344#[cfg(feature = "shell")]
345impl std::error::Error for ProbeError {}