use std::mem::{align_of, size_of};
use std::ptr;
use truce_core::buffer::AudioBuffer;
use truce_core::events::{Event, EventBody, TransportInfo as Transport};
use truce_core::process::{ProcessContext, ProcessStatus};
use truce_gui_types::interaction::WidgetRegion;
use truce_gui_types::layout::GridLayout;
use truce_gui_types::theme::{Color, Theme};
pub const ABI_EPOCH: u32 = 6;
#[repr(C)]
pub struct AbiCanary {
pub abi_epoch: u32,
pub audio_buffer_size: usize,
pub process_context_size: usize,
pub process_status_size: usize,
pub event_size: usize,
pub event_body_size: usize,
pub transport_size: usize,
pub widget_region_size: usize,
pub theme_size: usize,
pub plugin_layout_size: usize,
pub color_size: usize,
pub vec_u8_size: usize,
pub option_usize_size: usize,
pub audio_buffer_align: usize,
pub process_status_align: usize,
pub result_normal_disc: u8,
pub result_tail_disc: u8,
pub result_keepalive_disc: u8,
pub rustc_version_hash: u64,
pub sample_precision: u8,
}
impl AbiCanary {
#[must_use]
pub fn current<S: truce_params::sample::Sample>() -> Self {
#[allow(clippy::cast_possible_truncation)]
let sample_precision = (size_of::<S>() * 8) as u8;
Self {
abi_epoch: ABI_EPOCH,
audio_buffer_size: size_of::<AudioBuffer<S>>(),
process_context_size: size_of::<ProcessContext>(),
process_status_size: size_of::<ProcessStatus>(),
event_size: size_of::<Event>(),
event_body_size: size_of::<EventBody>(),
transport_size: size_of::<Transport>(),
widget_region_size: size_of::<WidgetRegion>(),
theme_size: size_of::<Theme>(),
plugin_layout_size: size_of::<GridLayout>(),
color_size: size_of::<Color>(),
vec_u8_size: size_of::<Vec<u8>>(),
option_usize_size: size_of::<Option<usize>>(),
audio_buffer_align: align_of::<AudioBuffer<S>>(),
process_status_align: align_of::<ProcessStatus>(),
result_normal_disc: discriminant_byte(&ProcessStatus::Normal),
result_tail_disc: discriminant_byte(&ProcessStatus::Tail(0)),
result_keepalive_disc: discriminant_byte(&ProcessStatus::KeepAlive),
rustc_version_hash: rustc_hash(),
sample_precision,
}
}
#[must_use]
pub fn matches(&self, other: &Self) -> bool {
self.field_diffs(other).is_empty()
}
#[must_use]
pub fn diff_report(&self, other: &Self) -> String {
let diffs = self.field_diffs(other);
if diffs.is_empty() {
"no differences".into()
} else {
format!("ABI mismatches:\n{}", diffs.join("\n"))
}
}
fn field_diffs(&self, other: &Self) -> Vec<String> {
let mut diffs = Vec::new();
macro_rules! check {
($field:ident) => {
if self.$field != other.$field {
diffs.push(format!(
" {}: shell={}, dylib={}",
stringify!($field),
self.$field,
other.$field
));
}
};
}
check!(abi_epoch);
check!(audio_buffer_size);
check!(process_context_size);
check!(process_status_size);
check!(event_size);
check!(event_body_size);
check!(transport_size);
check!(widget_region_size);
check!(theme_size);
check!(plugin_layout_size);
check!(color_size);
check!(vec_u8_size);
check!(option_usize_size);
check!(audio_buffer_align);
check!(process_status_align);
check!(result_normal_disc);
check!(result_tail_disc);
check!(result_keepalive_disc);
check!(rustc_version_hash);
check!(sample_precision);
diffs
}
}
fn discriminant_byte<T>(value: &T) -> u8 {
unsafe { *ptr::from_ref::<T>(value).cast::<u8>() }
}
fn rustc_hash() -> u64 {
env!("TRUCE_RUSTC_HASH").parse().unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::{ABI_EPOCH, AbiCanary};
#[test]
fn same_build_matches_itself() {
let a = AbiCanary::current::<f32>();
let b = AbiCanary::current::<f32>();
assert!(a.matches(&b));
}
#[test]
fn epoch_mismatch_fails_and_names_the_field() {
let a = AbiCanary::current::<f32>();
let mut b = AbiCanary::current::<f32>();
b.abi_epoch = ABI_EPOCH - 1;
assert!(!a.matches(&b));
assert!(a.diff_report(&b).contains("abi_epoch"));
}
#[test]
fn precision_mismatch_fails() {
let a = AbiCanary::current::<f32>();
let b = AbiCanary::current::<f64>();
assert!(!a.matches(&b));
}
}