use std::any::type_name;
use std::ffi::CString;
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_mutex::{PluginGuard, PluginMutex};
pub type SharedPlugin<P> = Arc<PluginMutex<P>>;
pub fn shared_plugin<P>(plugin: P) -> SharedPlugin<P> {
Arc::new(PluginMutex::new(plugin))
}
pub fn lock_plugin<P>(plugin: &PluginMutex<P>) -> PluginGuard<'_, P> {
plugin.lock()
}
pub fn try_lock_plugin<P>(plugin: &PluginMutex<P>) -> Option<PluginGuard<'_, P>> {
plugin.try_lock()
}
#[cfg(any(not(target_os = "linux"), miri))]
mod plugin_mutex {
use std::ops::{Deref, DerefMut};
use std::sync::{Mutex, MutexGuard, PoisonError, TryLockError};
pub struct PluginMutex<T>(Mutex<T>);
pub struct PluginGuard<'a, T>(MutexGuard<'a, T>);
impl<T> PluginMutex<T> {
pub fn new(value: T) -> Self {
Self(Mutex::new(value))
}
pub fn lock(&self) -> PluginGuard<'_, T> {
PluginGuard(self.0.lock().unwrap_or_else(PoisonError::into_inner))
}
pub fn try_lock(&self) -> Option<PluginGuard<'_, T>> {
match self.0.try_lock() {
Ok(guard) => Some(PluginGuard(guard)),
Err(TryLockError::Poisoned(poisoned)) => Some(PluginGuard(poisoned.into_inner())),
Err(TryLockError::WouldBlock) => None,
}
}
}
impl<T> Deref for PluginGuard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T> DerefMut for PluginGuard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
}
#[cfg(all(target_os = "linux", not(miri)))]
mod plugin_mutex {
use std::cell::UnsafeCell;
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
pub struct PluginMutex<T> {
raw: Box<UnsafeCell<libc::pthread_mutex_t>>,
data: UnsafeCell<T>,
}
unsafe impl<T: Send> Send for PluginMutex<T> {}
unsafe impl<T: Send> Sync for PluginMutex<T> {}
impl<T> PluginMutex<T> {
pub fn new(value: T) -> Self {
let raw = Box::new(UnsafeCell::new(libc::PTHREAD_MUTEX_INITIALIZER));
unsafe {
let mut attr: libc::pthread_mutexattr_t = std::mem::zeroed();
if libc::pthread_mutexattr_init(&raw mut attr) == 0 {
let _ = libc::pthread_mutexattr_setprotocol(
&raw mut attr,
libc::PTHREAD_PRIO_INHERIT,
);
let _ = libc::pthread_mutex_init(raw.get(), &raw const attr);
let _ = libc::pthread_mutexattr_destroy(&raw mut attr);
}
}
Self {
raw,
data: UnsafeCell::new(value),
}
}
pub fn lock(&self) -> PluginGuard<'_, T> {
unsafe {
libc::pthread_mutex_lock(self.raw.get());
}
PluginGuard {
lock: self,
_not_send: PhantomData,
}
}
pub fn try_lock(&self) -> Option<PluginGuard<'_, T>> {
(unsafe { libc::pthread_mutex_trylock(self.raw.get()) } == 0).then_some(PluginGuard {
lock: self,
_not_send: PhantomData,
})
}
}
impl<T> Drop for PluginMutex<T> {
fn drop(&mut self) {
unsafe {
libc::pthread_mutex_destroy(self.raw.get());
}
}
}
pub struct PluginGuard<'a, T> {
lock: &'a PluginMutex<T>,
_not_send: PhantomData<*const ()>,
}
impl<T> Deref for PluginGuard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.lock.data.get() }
}
}
impl<T> DerefMut for PluginGuard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.lock.data.get() }
}
}
impl<T> Drop for PluginGuard<'_, T> {
fn drop(&mut self) {
unsafe {
libc::pthread_mutex_unlock(self.lock.raw.get());
}
}
}
}
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 first_bus_layout<P: PluginExport>() -> Option<BusLayout> {
P::bus_layouts().into_iter().next()
}
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>"
}
}
#[cfg(test)]
mod plugin_mutex_tests {
use std::sync::Arc;
use super::{lock_plugin, shared_plugin, try_lock_plugin};
#[test]
fn lock_round_trips_data() {
let plugin = shared_plugin(41);
*lock_plugin(&plugin) += 1;
assert_eq!(*lock_plugin(&plugin), 42);
}
#[test]
fn try_lock_reports_contention() {
let plugin = shared_plugin(0u32);
let held = lock_plugin(&plugin);
assert!(try_lock_plugin(&plugin).is_none());
drop(held);
assert!(try_lock_plugin(&plugin).is_some());
}
#[test]
fn excludes_across_threads() {
let plugin = shared_plugin(0u64);
let threads: Vec<_> = (0..4)
.map(|_| {
let plugin = Arc::clone(&plugin);
std::thread::spawn(move || {
for _ in 0..10_000 {
*lock_plugin(&plugin) += 1;
}
})
})
.collect();
for t in threads {
t.join().unwrap();
}
assert_eq!(*lock_plugin(&plugin), 40_000);
}
#[test]
fn panicking_holder_does_not_wedge_the_lock() {
let plugin = shared_plugin(7);
let for_panic = Arc::clone(&plugin);
let _ = std::thread::spawn(move || {
let _guard = lock_plugin(&for_panic);
panic!("wedge attempt");
})
.join();
assert_eq!(*lock_plugin(&plugin), 7);
assert!(try_lock_plugin(&plugin).is_some());
}
}