1use truce_core::buffer::AudioBuffer;
5use truce_core::events::{Event, EventBody, EventList, TransportInfo as Transport};
6use truce_core::process::{ProcessContext, ProcessStatus};
7use truce_gui::interaction::WidgetRegion;
8use truce_gui::layout::GridLayout;
9use truce_gui::render::RenderBackend;
10use truce_gui::theme::{Color, Theme};
11
12use crate::traits::PluginLogic;
13
14#[repr(C)]
19pub struct AbiCanary {
20 pub trait_object_size: usize,
21 pub audio_buffer_size: usize,
22 pub process_context_size: usize,
23 pub process_status_size: usize,
24 pub event_size: usize,
25 pub event_body_size: usize,
26 pub transport_size: usize,
27 pub widget_region_size: usize,
28 pub theme_size: usize,
29 pub plugin_layout_size: usize,
30 pub color_size: usize,
31 pub vec_u8_size: usize,
32 pub option_usize_size: usize,
33 pub audio_buffer_align: usize,
34 pub process_status_align: usize,
35 pub result_normal_disc: u8,
36 pub result_tail_disc: u8,
37 pub result_keepalive_disc: u8,
38 pub rustc_version_hash: u64,
39}
40
41impl AbiCanary {
42 pub fn current() -> Self {
43 Self {
44 trait_object_size: std::mem::size_of::<*const dyn PluginLogic>() * 2,
45 audio_buffer_size: std::mem::size_of::<AudioBuffer>(),
46 process_context_size: std::mem::size_of::<ProcessContext>(),
47 process_status_size: std::mem::size_of::<ProcessStatus>(),
48 event_size: std::mem::size_of::<Event>(),
49 event_body_size: std::mem::size_of::<EventBody>(),
50 transport_size: std::mem::size_of::<Transport>(),
51 widget_region_size: std::mem::size_of::<WidgetRegion>(),
52 theme_size: std::mem::size_of::<Theme>(),
53 plugin_layout_size: std::mem::size_of::<GridLayout>(),
54 color_size: std::mem::size_of::<Color>(),
55 vec_u8_size: std::mem::size_of::<Vec<u8>>(),
56 option_usize_size: std::mem::size_of::<Option<usize>>(),
57 audio_buffer_align: std::mem::align_of::<AudioBuffer>(),
58 process_status_align: std::mem::align_of::<ProcessStatus>(),
59 result_normal_disc: discriminant_byte(&ProcessStatus::Normal),
60 result_tail_disc: discriminant_byte(&ProcessStatus::Tail(0)),
61 result_keepalive_disc: discriminant_byte(&ProcessStatus::KeepAlive),
62 rustc_version_hash: rustc_hash(),
63 }
64 }
65
66 pub fn matches(&self, other: &Self) -> bool {
67 self.trait_object_size == other.trait_object_size
68 && self.audio_buffer_size == other.audio_buffer_size
69 && self.process_context_size == other.process_context_size
70 && self.process_status_size == other.process_status_size
71 && self.event_size == other.event_size
72 && self.event_body_size == other.event_body_size
73 && self.transport_size == other.transport_size
74 && self.widget_region_size == other.widget_region_size
75 && self.theme_size == other.theme_size
76 && self.plugin_layout_size == other.plugin_layout_size
77 && self.color_size == other.color_size
78 && self.vec_u8_size == other.vec_u8_size
79 && self.option_usize_size == other.option_usize_size
80 && self.audio_buffer_align == other.audio_buffer_align
81 && self.process_status_align == other.process_status_align
82 && self.result_normal_disc == other.result_normal_disc
83 && self.result_tail_disc == other.result_tail_disc
84 && self.result_keepalive_disc == other.result_keepalive_disc
85 && self.rustc_version_hash == other.rustc_version_hash
86 }
87
88 pub fn diff_report(&self, other: &Self) -> String {
89 let mut diffs = Vec::new();
90 macro_rules! check {
91 ($field:ident) => {
92 if self.$field != other.$field {
93 diffs.push(format!(
94 " {}: shell={}, dylib={}",
95 stringify!($field), self.$field, other.$field
96 ));
97 }
98 };
99 }
100 check!(trait_object_size);
101 check!(audio_buffer_size);
102 check!(process_context_size);
103 check!(process_status_size);
104 check!(event_size);
105 check!(event_body_size);
106 check!(transport_size);
107 check!(widget_region_size);
108 check!(theme_size);
109 check!(plugin_layout_size);
110 check!(color_size);
111 check!(vec_u8_size);
112 check!(option_usize_size);
113 check!(audio_buffer_align);
114 check!(process_status_align);
115 check!(result_normal_disc);
116 check!(result_tail_disc);
117 check!(result_keepalive_disc);
118 check!(rustc_version_hash);
119 if diffs.is_empty() {
120 "no differences".into()
121 } else {
122 format!("ABI mismatches:\n{}", diffs.join("\n"))
123 }
124 }
125}
126
127fn discriminant_byte<T>(value: &T) -> u8 {
128 unsafe { *(value as *const T as *const u8) }
129}
130
131fn rustc_hash() -> u64 {
132 env!("TRUCE_RUSTC_HASH").parse().unwrap_or(0)
133}
134
135pub struct ProbePlugin;
145
146impl PluginLogic for ProbePlugin {
147 fn new() -> Self { Self }
148 fn reset(&mut self, _sr: f64, _bs: usize) {}
149
150 fn process(
151 &mut self,
152 _buffer: &mut AudioBuffer,
153 _events: &EventList,
154 _context: &mut ProcessContext,
155 ) -> ProcessStatus {
156 ProcessStatus::Normal
157 }
158
159 fn render(&self, _backend: &mut dyn RenderBackend) {}
160
161 fn layout(&self) -> truce_gui::layout::GridLayout {
162 let mut gl = truce_gui::layout::GridLayout::build("", "", 1, 80.0, vec![], vec![]);
163 gl.width = 0xDEAD;
164 gl.height = 0xBEEF;
165 gl
166 }
167
168 fn hit_test(&self, _w: &[WidgetRegion], _x: f32, _y: f32) -> Option<usize> {
169 Some(42)
170 }
171
172 fn save_state(&self) -> Vec<u8> { vec![0xCA, 0xFE] }
173 fn load_state(&mut self, _data: &[u8]) {}
174 fn latency(&self) -> u32 { 0xAAAA }
175 fn tail(&self) -> u32 { 0xBBBB }
176}
177
178pub fn verify_probe(probe: &dyn PluginLogic) -> Result<(), String> {
180 if probe.latency() != 0xAAAA {
181 return Err(format!("latency: expected 0xAAAA, got 0x{:X}", probe.latency()));
182 }
183 if probe.tail() != 0xBBBB {
184 return Err(format!("tail: expected 0xBBBB, got 0x{:X}", probe.tail()));
185 }
186 let layout = probe.layout();
187 if layout.width != 0xDEAD || layout.height != 0xBEEF {
188 return Err(format!(
189 "layout: expected 0xDEAD×0xBEEF, got 0x{:X}×0x{:X}",
190 layout.width, layout.height
191 ));
192 }
193 if probe.hit_test(&[], 0.0, 0.0) != Some(42) {
194 return Err("hit_test: expected Some(42)".into());
195 }
196 if probe.save_state() != vec![0xCA, 0xFE] {
197 return Err("save_state: expected [0xCA, 0xFE]".into());
198 }
199 Ok(())
200}