Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- **ALSA**: Fix a remaining timestamp segfault on 32-bit platforms with a 64-bit kernel `time_t`.
- **ASIO**: Fix a deadlock when dropping a `Stream` that owns another ASIO `Stream`.
- **ASIO**: Fix loading a driver while a previous driver was still unloading.
- **WASAPI**: Device enumeration no longer panics if the COM enumerator fails to initialize.

## [0.18.2] - 2026-08-16
Expand Down
73 changes: 64 additions & 9 deletions asio-sys/src/bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod errors;
#[cfg(target_os = "windows")]
use std::os::raw::c_long;
use std::{
cell::{Cell, RefCell},
ffi::{CStr, CString},
os::raw::{c_char, c_double, c_void},
ptr::null_mut,
Expand All @@ -31,10 +32,8 @@ use self::asio_import as ai;
/// There should only be one instance of this type at any point in time.
#[derive(Debug, Default)]
pub struct Asio {
// Keeps track of whether or not a driver is already loaded.
//
// This is necessary as ASIO only supports one `Driver` at a time.
loaded_driver: Mutex<Weak<DriverInner>>,
// Guards driver load and teardown. ASIO only supports one driver at a time.
loaded_driver: Arc<Mutex<Weak<DriverInner>>>,
}

/// A handle to a single ASIO driver.
Expand Down Expand Up @@ -74,6 +73,9 @@ struct DriverInner {
// In the case that the driver has been manually destroyed this flag will be set to `true`
// indicating to the `drop` implementation that there is nothing to be done.
destroyed: bool,
// Shared with the owning `Asio`; locked during teardown so a concurrent `load_driver`
// can't start `ASIOInit` before this driver's `ASIOExit` finishes.
loaded_driver: Arc<Mutex<Weak<DriverInner>>>,
}

/// All possible states of an ASIO `Driver` instance.
Expand Down Expand Up @@ -366,6 +368,14 @@ pub struct BufferCallbackId(usize);
/// parameters.
static BUFFER_CALLBACK: Mutex<Vec<(BufferCallbackId, BufferCallback)>> = Mutex::new(Vec::new());

thread_local! {
// Set while buffer_switch_time_info is running callbacks on this thread.
static RUNNING_CALLBACKS: Cell<bool> = const { Cell::new(false) };
// Removals deferred because remove_callback was called reentrantly (a callback dropped a
// Stream it owns) while RUNNING_CALLBACKS was set, to avoid deadlocking on BUFFER_CALLBACK.
static PENDING_REMOVALS: RefCell<Vec<BufferCallbackId>> = const { RefCell::new(Vec::new()) };
}

/// Used to identify when to clear buffers.
static CALLBACK_FLAG: AtomicU32 = AtomicU32::new(0);

Expand Down Expand Up @@ -486,6 +496,7 @@ impl Asio {
state,
streams,
destroyed,
loaded_driver: self.loaded_driver.clone(),
});
*loaded = Arc::downgrade(&inner);
let driver = Driver { inner };
Expand Down Expand Up @@ -900,8 +911,25 @@ impl Driver {

/// Remove the callback with the given ID.
pub fn remove_callback(&self, rem_id: BufferCallbackId) {
if RUNNING_CALLBACKS.with(Cell::get) {
// Reentrant call from within buffer_switch_time_info on this thread (a callback
// dropped a Stream it owns). BUFFER_CALLBACK is already locked by this thread, so
// defer the removal until buffer_switch_time_info returns.
PENDING_REMOVALS.with_borrow_mut(|pending| pending.push(rem_id));
return;
}

let mut bc = BUFFER_CALLBACK.lock().unwrap();
bc.retain(|&(id, _)| id != rem_id);
// `remove` (not `swap_remove`) to preserve the insertion order that
// `add_callback` relies on for generating the next ID.
let removed = bc
.iter()
.position(|&(id, _)| id == rem_id)
.map(|pos| bc.remove(pos));
// the lock must be dropped first, as the removed callback could
// be owning another stream, which would result in a deadlock
drop(bc);
drop(removed);
}

/// Consumes and destroys the `Driver`, stopping the streams if they are running and releasing
Expand Down Expand Up @@ -1016,13 +1044,24 @@ impl DriverInner {

fn destroy_inner(&mut self) -> Result<(), AsioError> {
{
// Held so a concurrent `load_driver` can't start ASIOInit before this
// driver's ASIOExit finishes.
let _loaded_driver_guard = self
.loaded_driver
.lock()
.expect("failed to acquire loaded driver lock");

let mut state = self.lock_state();
state.destroy()?;

// Clear any existing stream callbacks.
if let Ok(mut bcs) = BUFFER_CALLBACK.lock() {
bcs.clear();
}
// Clear any existing stream callbacks. Take the callbacks out and drop the
// lock before dropping them, as a callback could be owning another stream,
// which would result in a deadlock.
let cleared = BUFFER_CALLBACK
.lock()
.ok()
.map(|mut bcs| std::mem::take(&mut *bcs));
drop(cleared);
}

// Signal that the driver has been destroyed.
Expand Down Expand Up @@ -1236,9 +1275,25 @@ extern "C" fn buffer_switch_time_info(
system_time: asio_timestamp_to_nanos(asio_time.time_info.system_time),
callback_flag,
};
RUNNING_CALLBACKS.with(|flag| flag.set(true));
for &mut (_, ref mut bc) in bcs.iter_mut() {
bc.run(&callback_info);
}
RUNNING_CALLBACKS.with(|flag| flag.set(false));

// Apply any removals a callback above deferred by dropping a Stream it owns mid-run.
let removed = PENDING_REMOVALS.with_borrow_mut(|pending| {
pending
.drain(..)
.filter_map(|id| {
bcs.iter()
.position(|&(bc_id, _)| bc_id == id)
.map(|pos| bcs.remove(pos))
})
.collect::<Vec<_>>()
});
drop(bcs);
drop(removed);

if CALL_OUTPUT_READY.load(Ordering::Acquire) {
unsafe { ai::ASIOOutputReady() };
Expand Down