#[cfg(bela_device)]
use core::ffi::c_void;
use core::ops::DerefMut;
#[cfg(test)]
use core::ptr;
use core::sync::atomic::{AtomicU64, Ordering};
use std::ffi::CString;
use std::sync::{Mutex, PoisonError};
use crate::context::CallbackContext;
use crate::error::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Priority(u8);
impl Priority {
pub const AUDIO: Self = Self(95);
#[must_use]
pub const fn new(priority: u8) -> Option<Self> {
if priority > 99 {
return None;
}
Some(Self(priority))
}
}
static GENERATION: AtomicU64 = AtomicU64::new(0);
static LIFECYCLE: Mutex<Lifecycle> = Mutex::new(Lifecycle {
generation: 0,
accepting: true,
});
struct Lifecycle {
generation: u64,
accepting: bool,
}
#[cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "only the device-gated system module tears an audio system down"
)
)]
pub(crate) fn teardown<R>(shut_down: impl FnOnce() -> R) -> R {
struct Reopen;
impl Drop for Reopen {
fn drop(&mut self) {
lifecycle().accepting = true;
}
}
{
let mut lifecycle = lifecycle();
lifecycle.accepting = false;
lifecycle.generation += 1;
GENERATION.store(lifecycle.generation, Ordering::Release);
}
let _reopen = Reopen;
shut_down()
}
fn lifecycle() -> impl DerefMut<Target = Lifecycle> {
LIFECYCLE.lock().unwrap_or_else(PoisonError::into_inner)
}
#[derive(Debug)]
pub struct AuxiliaryTask {
#[cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "tasks cannot be created off-device, but the type still has to compile there"
)
)]
raw: bela_sys::AuxiliaryTask,
generation: u64,
}
unsafe impl Send for AuxiliaryTask {}
unsafe impl Sync for AuxiliaryTask {}
impl AuxiliaryTask {
pub fn new<F>(name: &str, priority: Priority, callback: F) -> Result<Self, Error>
where
F: FnMut() + Send + 'static,
{
let name = CString::new(name).map_err(|_| Error::TaskName)?;
let state: *mut F = Box::into_raw(Box::new(callback));
let lifecycle = lifecycle();
if !lifecycle.accepting {
drop(unsafe { Box::from_raw(state) });
return Err(Error::TaskCreateWhileStopping);
}
Self::create::<F>(&name, priority, lifecycle.generation, state)
}
#[cfg(bela_device)]
fn create<F: FnMut()>(
name: &CString,
priority: Priority,
generation: u64,
state: *mut F,
) -> Result<Self, Error> {
let raw = unsafe {
bela_sys::Bela_createAuxiliaryTask(
Some(trampoline::<F>),
i32::from(priority.0),
name.as_ptr(),
state.cast::<c_void>(),
)
};
if raw.is_null() {
drop(unsafe { Box::from_raw(state) });
return Err(Error::TaskCreate);
}
Ok(Self { raw, generation })
}
#[cfg(not(bela_device))]
fn create<F: FnMut()>(
_name: &CString,
_priority: Priority,
_generation: u64,
state: *mut F,
) -> Result<Self, Error> {
drop(unsafe { Box::from_raw(state) });
Err(Error::TaskUnavailable)
}
pub fn schedule(&self, _context: &impl CallbackContext) {
if !self.is_current() {
return;
}
self.schedule_raw();
}
fn is_current(&self) -> bool {
GENERATION.load(Ordering::Acquire) == self.generation
}
#[cfg(bela_device)]
fn schedule_raw(&self) {
let _ = unsafe { bela_sys::Bela_scheduleAuxiliaryTask(self.raw) };
}
#[cfg(not(bela_device))]
#[allow(
clippy::unused_self,
reason = "mirrors the device signature; unreachable because tasks cannot be created off-device"
)]
const fn schedule_raw(&self) {}
}
#[cfg(test)]
pub(crate) fn test_handle() -> AuxiliaryTask {
AuxiliaryTask {
raw: ptr::null_mut(),
generation: GENERATION.load(Ordering::Acquire),
}
}
#[cfg(bela_device)]
unsafe extern "C" fn trampoline<F: FnMut()>(arg: *mut c_void) {
let callback = unsafe { &mut *arg.cast::<F>() };
callback();
}
#[cfg(test)]
mod tests {
use core::time::Duration;
use std::panic::catch_unwind;
use std::sync::mpsc;
use std::thread;
use super::*;
static SERIALISE: Mutex<()> = Mutex::new(());
const TASK_PRIORITY: Priority = Priority::new(50).expect("50 is within Bela's priority range");
use super::test_handle as handle;
#[test]
fn tearing_down_the_audio_system_retires_the_handles() {
let _order = SERIALISE.lock().unwrap_or_else(PoisonError::into_inner);
let task = handle();
assert!(task.is_current(), "a fresh handle should be live");
teardown(|| {
assert!(
!task.is_current(),
"handles must be retired before anything can delete the tasks"
);
});
assert!(!task.is_current(), "and stay retired afterwards");
}
#[test]
fn a_later_audio_system_does_not_revive_an_earlier_handle() {
let _order = SERIALISE.lock().unwrap_or_else(PoisonError::into_inner);
let old = handle();
teardown(|| ());
let new = handle();
assert!(
!old.is_current(),
"a handle from the previous audio system must stay retired"
);
assert!(new.is_current(), "the new handle should be live");
}
#[test]
fn tasks_cannot_be_created_during_a_teardown() {
let _order = SERIALISE.lock().unwrap_or_else(PoisonError::into_inner);
teardown(|| {
let error = AuxiliaryTask::new("report", TASK_PRIORITY, || {}).unwrap_err();
assert_eq!(
error,
Error::TaskCreateWhileStopping,
"creating a task during a teardown must fail"
);
});
let error = AuxiliaryTask::new("report", TASK_PRIORITY, || {}).unwrap_err();
assert_ne!(
error,
Error::TaskCreateWhileStopping,
"creation should be accepted again once the teardown is over"
);
}
#[test]
fn creating_a_task_never_blocks_on_a_teardown() {
let _order = SERIALISE.lock().unwrap_or_else(PoisonError::into_inner);
teardown(|| {
let (sender, receiver) = mpsc::channel();
thread::spawn(move || {
let error = AuxiliaryTask::new("report", TASK_PRIORITY, || {}).unwrap_err();
let _ = sender.send(error);
});
let error = receiver
.recv_timeout(Duration::from_secs(5))
.expect("creating a task blocked while an audio system was being torn down");
assert_eq!(error, Error::TaskCreateWhileStopping);
});
}
#[test]
fn a_panicking_teardown_still_reopens_creation() {
let _order = SERIALISE.lock().unwrap_or_else(PoisonError::into_inner);
let panicked = catch_unwind(|| teardown(|| panic!("teardown blew up")));
assert!(panicked.is_err(), "the panic should propagate");
let error = AuxiliaryTask::new("report", TASK_PRIORITY, || {}).unwrap_err();
assert_ne!(
error,
Error::TaskCreateWhileStopping,
"the window must not stay closed after a panic"
);
}
#[test]
fn a_name_with_an_interior_nul_is_rejected() {
let _order = SERIALISE.lock().unwrap_or_else(PoisonError::into_inner);
let error = AuxiliaryTask::new("no\0pe", TASK_PRIORITY, || {}).unwrap_err();
assert_eq!(error, Error::TaskName, "expected the name to be rejected");
}
#[test]
#[cfg(not(bela_device))]
fn tasks_cannot_be_created_off_device() {
let _order = SERIALISE.lock().unwrap_or_else(PoisonError::into_inner);
let error = AuxiliaryTask::new("report", TASK_PRIORITY, || {}).unwrap_err();
assert_eq!(
error,
Error::TaskUnavailable,
"off-device there is no audio system to create a task in"
);
}
#[test]
fn a_missing_library_and_a_refused_task_do_not_read_alike() {
let unavailable = Error::TaskUnavailable.to_string();
let refused = Error::TaskCreate.to_string();
assert_ne!(unavailable, refused);
assert!(
unavailable.contains("libbela"),
"a build with no library should name the library: {unavailable}"
);
assert!(
!refused.contains("libbela"),
"a board refusing a task should not read like a missing library: {refused}"
);
}
#[test]
fn priority_accepts_exactly_belas_range() {
assert_eq!(Priority::new(0), Some(Priority(0)));
assert_eq!(Priority::new(99), Some(Priority(99)));
assert_eq!(Priority::new(100), None);
}
#[test]
fn audio_priority_matches_bela() {
assert_eq!(u32::from(Priority::AUDIO.0), bela_sys::BELA_AUDIO_PRIORITY);
}
}