#[cfg(feature = "rt-paranoid")]
mod imp {
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
const MAX_FRAMES: usize = 32;
#[derive(Clone, Copy)]
struct FrameBuf {
ips: [usize; MAX_FRAMES],
len: usize,
}
impl FrameBuf {
const EMPTY: Self = Self {
ips: [0; MAX_FRAMES],
len: 0,
};
}
#[derive(Clone, Copy)]
enum Kind {
Alloc,
Free,
Lock,
}
impl Kind {
fn noun(self) -> &'static str {
match self {
Kind::Alloc => "allocation",
Kind::Free => "free",
Kind::Lock => "lock",
}
}
}
thread_local! {
static DEPTH: Cell<u32> = const { Cell::new(0) };
static RECORDING: Cell<bool> = const { Cell::new(false) };
static VIOLATIONS: Cell<u32> = const { Cell::new(0) };
static FIRST: Cell<FrameBuf> = const { Cell::new(FrameBuf::EMPTY) };
static FIRST_KIND: Cell<Kind> = const { Cell::new(Kind::Alloc) };
static AUDIT: Cell<Option<u32>> = const { Cell::new(None) };
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Mode {
Count,
Panic,
Trap,
}
impl Mode {
const fn to_u8(self) -> u8 {
match self {
Mode::Count => 0,
Mode::Panic => 1,
Mode::Trap => 2,
}
}
fn from_u8(v: u8) -> Self {
match v {
1 => Mode::Panic,
2 => Mode::Trap,
_ => Mode::Count,
}
}
}
static MODE: AtomicU8 = AtomicU8::new(Mode::Count.to_u8());
pub fn set_mode(mode: Mode) {
MODE.store(mode.to_u8(), Ordering::Relaxed);
}
fn mode() -> Mode {
Mode::from_u8(MODE.load(Ordering::Relaxed))
}
static CHECK_DEALLOC: AtomicBool = AtomicBool::new(false);
pub fn set_check_dealloc(enabled: bool) {
CHECK_DEALLOC.store(enabled, Ordering::Relaxed);
}
#[must_use]
pub fn check_dealloc() -> bool {
CHECK_DEALLOC.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn current_mode() -> Mode {
mode()
}
pub struct RtSection {
_private: (),
}
impl RtSection {
#[inline]
#[must_use]
pub fn enter() -> Self {
DEPTH.with(|d| d.set(d.get().wrapping_add(1)));
Self { _private: () }
}
}
impl Drop for RtSection {
fn drop(&mut self) {
let depth = DEPTH.with(|d| {
let n = d.get().wrapping_sub(1);
d.set(n);
n
});
if depth == 0 {
let count = VIOLATIONS.with(|v| v.replace(0));
if count > 0 {
if AUDIT.with(Cell::get).is_some() {
AUDIT.with(|a| a.set(a.get().map(|n| n.saturating_add(count))));
} else {
report(count);
}
}
}
}
}
pub fn audit<R>(f: impl FnOnce() -> R) -> (R, u32) {
let prev = AUDIT.with(|a| a.replace(Some(0)));
let r = f();
let count = AUDIT.with(|a| a.replace(prev)).unwrap_or(0);
(r, count)
}
#[must_use]
pub fn is_active() -> bool {
true
}
#[cfg(test)]
pub(crate) fn count_allocs<R>(f: impl FnOnce() -> R) -> u32 {
DEPTH.with(|d| d.set(d.get().wrapping_add(1)));
VIOLATIONS.with(|v| v.set(0));
let _ = f();
let n = VIOLATIONS.with(|v| v.replace(0));
DEPTH.with(|d| d.set(d.get().wrapping_sub(1)));
n
}
pub fn allow_alloc<R>(f: impl FnOnce() -> R) -> R {
struct Restore(u32);
impl Drop for Restore {
fn drop(&mut self) {
DEPTH.with(|d| d.set(self.0));
}
}
let _restore = Restore(DEPTH.with(|d| d.replace(0)));
f()
}
#[inline]
fn note_violation(kind: Kind) {
if DEPTH.with(Cell::get) == 0 || RECORDING.with(Cell::get) {
return;
}
RECORDING.with(|r| r.set(true));
let n = VIOLATIONS.with(|v| {
let n = v.get().wrapping_add(1);
v.set(n);
n
});
if n == 1 {
FIRST_KIND.with(|k| k.set(kind));
capture_first();
}
if mode() == Mode::Trap {
std::process::abort();
}
RECORDING.with(|r| r.set(false));
}
pub(super) fn note_lock() {
note_violation(Kind::Lock);
}
fn capture_first() {
let mut buf = FrameBuf::EMPTY;
backtrace::trace(|frame| {
if buf.len < MAX_FRAMES {
buf.ips[buf.len] = frame.ip() as usize;
buf.len += 1;
true
} else {
false
}
});
FIRST.with(|f| f.set(buf));
}
fn report(count: u32) {
use std::fmt::Write as _;
let buf = FIRST.with(|f| f.replace(FrameBuf::EMPTY));
let noun = FIRST_KIND.with(Cell::get).noun();
let mut frames = String::new();
for &ip in &buf.ips[..buf.len] {
backtrace::resolve(ip as *mut _, |s| {
let name = s.name().map(|n| n.to_string()).unwrap_or_default();
if name.starts_with("truce_core::rt") || name.starts_with("backtrace") {
return; }
match (s.filename(), s.lineno()) {
(Some(file), Some(line)) => {
let _ = write!(frames, "\n {name} ({}:{line})", file.display());
}
_ if !name.is_empty() => {
let _ = write!(frames, "\n {name}");
}
_ => {}
}
});
}
let mut msg = format!(
"truce rt-paranoid: {count} real-time violation(s) on the audio thread in process()"
);
if !frames.is_empty() {
let _ = write!(msg, "\n first violation ({noun}):");
msg.push_str(&frames);
}
match mode() {
Mode::Panic if !std::thread::panicking() => panic!("{msg}"),
_ => eprintln!("{msg}"),
}
}
pub struct RtCheckAlloc;
impl RtCheckAlloc {
#[must_use]
pub const fn new() -> Self {
Self
}
}
impl Default for RtCheckAlloc {
fn default() -> Self {
Self::new()
}
}
unsafe impl GlobalAlloc for RtCheckAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
note_violation(Kind::Alloc);
unsafe { System.alloc(layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
note_violation(Kind::Alloc);
unsafe { System.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
note_violation(Kind::Alloc);
unsafe { System.realloc(ptr, layout, new_size) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
if CHECK_DEALLOC.load(Ordering::Relaxed) {
note_violation(Kind::Free);
}
unsafe { System.dealloc(ptr, layout) }
}
}
}
#[cfg(not(feature = "rt-paranoid"))]
mod imp {
pub struct RtSection {
_private: (),
}
impl RtSection {
#[inline]
#[must_use]
pub fn enter() -> Self {
Self { _private: () }
}
}
#[inline]
pub fn allow_alloc<R>(f: impl FnOnce() -> R) -> R {
f()
}
#[inline]
pub fn audit<R>(f: impl FnOnce() -> R) -> (R, u32) {
(f(), 0)
}
#[must_use]
#[inline]
pub fn is_active() -> bool {
false
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Mode {
Count,
Panic,
Trap,
}
#[inline]
pub fn set_mode(_mode: Mode) {}
#[inline]
pub fn set_check_dealloc(_enabled: bool) {}
#[must_use]
#[inline]
pub fn check_dealloc() -> bool {
false
}
#[inline]
pub(super) fn note_lock() {}
}
pub use imp::{
Mode, RtSection, allow_alloc, audit, check_dealloc, is_active, set_check_dealloc, set_mode,
};
#[cfg(feature = "rt-paranoid")]
pub use imp::RtCheckAlloc;
pub struct Mutex<T: ?Sized> {
inner: std::sync::Mutex<T>,
}
impl<T> Mutex<T> {
pub const fn new(value: T) -> Self {
Self {
inner: std::sync::Mutex::new(value),
}
}
pub fn into_inner(self) -> std::sync::LockResult<T> {
self.inner.into_inner()
}
}
impl<T: ?Sized> Mutex<T> {
pub fn lock(&self) -> std::sync::LockResult<std::sync::MutexGuard<'_, T>> {
imp::note_lock();
self.inner.lock()
}
pub fn try_lock(&self) -> std::sync::TryLockResult<std::sync::MutexGuard<'_, T>> {
imp::note_lock();
self.inner.try_lock()
}
pub fn get_mut(&mut self) -> std::sync::LockResult<&mut T> {
self.inner.get_mut()
}
}
pub struct RwLock<T: ?Sized> {
inner: std::sync::RwLock<T>,
}
impl<T> RwLock<T> {
pub const fn new(value: T) -> Self {
Self {
inner: std::sync::RwLock::new(value),
}
}
pub fn into_inner(self) -> std::sync::LockResult<T> {
self.inner.into_inner()
}
}
impl<T: ?Sized> RwLock<T> {
pub fn read(&self) -> std::sync::LockResult<std::sync::RwLockReadGuard<'_, T>> {
imp::note_lock();
self.inner.read()
}
pub fn write(&self) -> std::sync::LockResult<std::sync::RwLockWriteGuard<'_, T>> {
imp::note_lock();
self.inner.write()
}
pub fn try_read(&self) -> std::sync::TryLockResult<std::sync::RwLockReadGuard<'_, T>> {
imp::note_lock();
self.inner.try_read()
}
pub fn try_write(&self) -> std::sync::TryLockResult<std::sync::RwLockWriteGuard<'_, T>> {
imp::note_lock();
self.inner.try_write()
}
pub fn get_mut(&mut self) -> std::sync::LockResult<&mut T> {
self.inner.get_mut()
}
}
#[cfg(all(test, feature = "rt-paranoid"))]
#[global_allocator]
static TEST_ALLOC: RtCheckAlloc = RtCheckAlloc::new();
#[cfg(all(test, feature = "rt-paranoid"))]
mod tests {
use super::allow_alloc;
use super::imp::count_allocs;
use std::hint::black_box;
#[test]
fn alloc_in_section_is_flagged() {
let n = count_allocs(|| {
let v: Vec<u8> = Vec::with_capacity(4096);
black_box(v.as_ptr());
});
assert!(n >= 1, "expected the in-section allocation to be flagged");
}
#[test]
fn no_alloc_in_section_is_clean() {
let n = count_allocs(|| {
let x = black_box(2) + black_box(3);
black_box(x);
});
assert_eq!(n, 0);
}
#[test]
fn dealloc_flagged_only_when_enabled() {
use super::set_check_dealloc;
let outside = Vec::<u8>::with_capacity(4096);
let off = count_allocs(|| drop(black_box(outside)));
assert_eq!(
off, 0,
"a free must not be flagged with dealloc checking off"
);
set_check_dealloc(true);
let outside = Vec::<u8>::with_capacity(4096);
let on = count_allocs(|| drop(black_box(outside)));
set_check_dealloc(false);
assert!(on >= 1, "a free must be flagged with dealloc checking on");
}
#[test]
fn lock_in_section_is_flagged() {
use super::Mutex;
let m = Mutex::new(0u32);
drop(m.lock());
let n = count_allocs(|| {
let _g = m.lock().unwrap();
});
assert!(n >= 1, "a lock taken inside a section must be flagged");
}
#[test]
fn rwlock_write_in_section_is_flagged() {
use super::RwLock;
let rw = RwLock::new(0u32);
drop(rw.write());
let n = count_allocs(|| {
let _g = rw.write().unwrap();
});
assert!(
n >= 1,
"a write lock taken inside a section must be flagged"
);
}
#[test]
fn allow_alloc_suppresses_flagging() {
let n = count_allocs(|| {
allow_alloc(|| {
let v: Vec<u8> = Vec::with_capacity(4096);
black_box(v.as_ptr());
});
});
assert_eq!(n, 0, "allow_alloc should suspend checking for its scope");
}
#[test]
fn shared_plugin_warms_the_mediation_lock() {
use super::imp::count_allocs;
use crate::wrapper::{lock_plugin, shared_plugin};
let shared = shared_plugin(vec![0u8; 16]);
let n = count_allocs(|| {
drop(lock_plugin(&shared));
});
assert_eq!(n, 0, "the mediation lock's first lock must be warmed");
}
#[test]
fn set_mode_overrides_the_default() {
use super::imp::current_mode;
use super::{Mode, set_mode};
set_mode(Mode::Panic);
assert_eq!(current_mode(), Mode::Panic);
set_mode(Mode::Count);
assert_eq!(current_mode(), Mode::Count);
}
}