use std::any::type_name;
use std::ffi::CString;
use std::os::raw::c_char;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Arc;
use truce_params::ParamInfo;
use crate::bus::BusLayout;
use crate::export::PluginExport;
pub use plugin_cell::{PluginCell, PluginGuard};
pub type SharedPlugin<P> = Arc<PluginCell<P>>;
pub fn shared_plugin<P>(plugin: P) -> SharedPlugin<P> {
Arc::new(PluginCell::new(plugin))
}
pub fn enter_plugin<P>(plugin: &PluginCell<P>) -> PluginGuard<'_, P> {
plugin.enter()
}
#[must_use]
pub fn save_extra(snapshot: &crate::snapshot::SnapshotSlot) -> Vec<u8> {
snapshot.read().unwrap_or_default()
}
mod plugin_cell {
use std::cell::UnsafeCell;
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicU64, Ordering};
pub struct PluginCell<T> {
data: UnsafeCell<T>,
handoff: AtomicU64,
#[cfg(debug_assertions)]
held: std::sync::atomic::AtomicBool,
}
unsafe impl<T: Send> Send for PluginCell<T> {}
unsafe impl<T: Send> Sync for PluginCell<T> {}
impl<T> PluginCell<T> {
pub fn new(value: T) -> Self {
Self {
data: UnsafeCell::new(value),
handoff: AtomicU64::new(0),
#[cfg(debug_assertions)]
held: std::sync::atomic::AtomicBool::new(false),
}
}
#[allow(
clippy::missing_panics_doc,
reason = "the only panic is the debug-only overlap detector, compiled out in release"
)]
pub fn enter(&self) -> PluginGuard<'_, T> {
self.handoff.load(Ordering::Acquire);
#[cfg(debug_assertions)]
assert!(
!self.held.swap(true, Ordering::Relaxed),
"plugin ownership cell entered while already held: the host \
overlapped process() with a lifecycle callback"
);
PluginGuard {
cell: self,
_not_send: PhantomData,
}
}
}
pub struct PluginGuard<'a, T> {
cell: &'a PluginCell<T>,
_not_send: PhantomData<*const ()>,
}
impl<T> Deref for PluginGuard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.cell.data.get() }
}
}
impl<T> DerefMut for PluginGuard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.cell.data.get() }
}
}
impl<T> Drop for PluginGuard<'_, T> {
fn drop(&mut self) {
#[cfg(debug_assertions)]
self.cell.held.store(false, Ordering::Relaxed);
self.cell.handoff.fetch_add(1, Ordering::Release);
}
}
}
pub struct ParamCStrings {
pub name: CString,
pub short_name: CString,
pub unit: CString,
pub group: CString,
}
impl ParamCStrings {
#[must_use]
pub fn from_info(info: &ParamInfo) -> Self {
Self {
name: CString::new(info.name).unwrap_or_default(),
short_name: CString::new(info.short_name).unwrap_or_default(),
unit: CString::new(info.unit.as_str()).unwrap_or_default(),
group: CString::new(info.group).unwrap_or_default(),
}
}
}
#[must_use]
pub fn default_io_channels<P: PluginExport>() -> Option<(u32, u32)> {
P::bus_layouts()
.first()
.map(|l| (l.total_input_channels(), l.total_output_channels()))
}
#[must_use]
pub fn max_io_channels<P: PluginExport>() -> Option<(u32, u32)> {
P::bus_layouts().iter().fold(None, |acc, l| {
let (in_, out) = (l.total_input_channels(), l.total_output_channels());
Some(acc.map_or((in_, out), |(ai, ao): (u32, u32)| {
(ai.max(in_), ao.max(out))
}))
})
}
#[must_use]
pub fn first_bus_layout<P: PluginExport>() -> Option<BusLayout> {
P::bus_layouts().into_iter().next()
}
#[must_use]
pub fn find_bus_layout<P: PluginExport>(inputs: u32, outputs: u32) -> Option<usize> {
P::bus_layouts()
.iter()
.position(|l| l.total_input_channels() == inputs && l.total_output_channels() == outputs)
}
pub fn log_missing_bus_layout<P: PluginExport>(format: &str) {
eprintln!(
"[truce {format}] {}::bus_layouts() returned an empty list - \
plugin will not register. Plugins with no audio I/O (e.g. \
aumi MIDI-effects) should return vec![BusLayout::new()] \
explicitly.",
type_name::<P>(),
);
}
pub fn log_midi_ports_clamped(format: &str, direction: &str, declared: u8) {
if declared > 1 {
eprintln!(
"[truce {format}] plugin declares {declared} MIDI {direction} ports, but {format} \
carries one - routing all {direction} MIDI to port 0.",
);
}
}
pub fn run_register<P>(format: &str, body: impl FnOnce()) {
let result = catch_unwind(AssertUnwindSafe(body));
if let Err(payload) = result {
eprintln!(
"[truce {format}] panic during register for {}: {}",
type_name::<P>(),
extract_panic_msg(&payload),
);
}
}
#[must_use]
pub fn run_audio_block<P>(format: &str, body: impl FnOnce()) -> bool {
let result = catch_unwind(AssertUnwindSafe(body));
if let Err(payload) = result {
eprintln!(
"[truce {format}] panic in process() for {}: {}",
type_name::<P>(),
extract_panic_msg(&payload),
);
return false;
}
true
}
pub fn run_audio_block_with<P, R>(format: &str, fallback: R, body: impl FnOnce() -> R) -> R {
match catch_unwind(AssertUnwindSafe(body)) {
Ok(r) => r,
Err(payload) => {
eprintln!(
"[truce {format}] panic in process() for {}: {}",
type_name::<P>(),
extract_panic_msg(&payload),
);
fallback
}
}
}
pub fn run_extern_callback_with<P, R>(
format: &str,
action: &str,
fallback: R,
body: impl FnOnce() -> R,
) -> R {
match catch_unwind(AssertUnwindSafe(body)) {
Ok(r) => r,
Err(payload) => {
eprintln!(
"[truce {format}] panic in {action} for {}: {}",
type_name::<P>(),
extract_panic_msg(&payload),
);
fallback
}
}
}
fn extract_panic_msg(payload: &Box<dyn std::any::Any + Send>) -> &str {
if let Some(s) = payload.downcast_ref::<&'static str>() {
s
} else if let Some(s) = payload.downcast_ref::<String>() {
s.as_str()
} else {
"<non-string panic payload>"
}
}
#[must_use]
pub unsafe fn copy_c_str(out: *mut c_char, out_len: usize, text: &str) -> usize {
let bytes = text.as_bytes();
let mut len = bytes.len().min(out_len - 1);
while len > 0 && !text.is_char_boundary(len) {
len -= 1;
}
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr().cast::<c_char>(), out, len);
*out.add(len) = 0;
}
len
}
#[cfg(test)]
mod plugin_cell_tests {
use std::sync::Arc;
use super::{enter_plugin, shared_plugin};
#[test]
fn lock_round_trips_data() {
let plugin = shared_plugin(41);
*enter_plugin(&plugin) += 1;
assert_eq!(*enter_plugin(&plugin), 42);
}
#[test]
fn repeated_ownership_publishes_writes() {
let plugin = shared_plugin(0u64);
for _ in 0..1000 {
*enter_plugin(&plugin) += 1;
}
assert_eq!(*enter_plugin(&plugin), 1000);
}
#[test]
fn handoff_carries_writes_across_a_thread() {
let plugin = shared_plugin(0u32);
let worker = {
let plugin = Arc::clone(&plugin);
std::thread::spawn(move || {
*enter_plugin(&plugin) = 99;
})
};
worker.join().unwrap();
assert_eq!(*enter_plugin(&plugin), 99);
}
#[test]
fn panicking_owner_does_not_wedge_the_cell() {
let plugin = shared_plugin(7);
let for_panic = Arc::clone(&plugin);
let _ = std::thread::spawn(move || {
let _guard = enter_plugin(&for_panic);
panic!("wedge attempt");
})
.join();
assert_eq!(*enter_plugin(&plugin), 7);
}
}
#[cfg(test)]
mod copy_c_str_tests {
use super::copy_c_str;
use std::os::raw::c_char;
fn run(text: &str, cap: usize) -> (usize, String) {
let mut buf = vec![0 as c_char; cap];
let n = unsafe { copy_c_str(buf.as_mut_ptr(), cap, text) };
#[allow(clippy::cast_sign_loss)]
let bytes: Vec<u8> = buf[..n].iter().map(|&c| c as u8).collect();
(n, String::from_utf8(bytes).unwrap())
}
#[test]
fn copies_when_it_fits() {
let (n, s) = run("-6 dB", 32);
assert_eq!(n, 5);
assert_eq!(s, "-6 dB");
}
#[test]
fn writes_the_trailing_nul() {
let mut buf = [1 as c_char; 8];
let n = unsafe { copy_c_str(buf.as_mut_ptr(), 8, "ab") };
assert_eq!(n, 2);
assert_eq!(buf[2], 0, "NUL terminator written after the content");
}
#[test]
fn truncates_ascii_to_capacity() {
let (n, s) = run("abcdef", 4);
assert_eq!(n, 3);
assert_eq!(s, "abc");
}
#[test]
fn truncates_on_a_char_boundary() {
let (n, s) = run("12°", 4);
assert_eq!(n, 2, "dropped the half-written degree sign");
assert_eq!(s, "12");
}
#[test]
fn multibyte_that_fits_is_untouched() {
let text = "−12 dB";
let (n, s) = run(text, 32);
assert_eq!(n, text.len());
assert_eq!(s, text);
}
}