From 4f01f673b3f8f104534ac07e615ada913e78c73f Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Sun, 26 Apr 2026 09:54:50 +0200 Subject: [PATCH] Introduce `MainloopWaker` for thread-safe `pa_mainloop_wakeup` the standard `Mainloop` is `!Send` (it's wrapped in `Rc`), so today the only way for downstream code to interrupt a blocking `Mainloop::iterate(true)` from another thread is to reach into `_inner`, grab the raw `*mut pa_mainloop`, and invoke `pa_mainloop_wakeup` via hand-rolled `unsafe extern "C"`. that single C function is documented as thread-safe (it just writes a byte to the mainloop's internal self-pipe), so a small safe abstraction is worthwhile. * `MainloopWaker`: a `Send + Sync + Clone` handle wrapping the call. * `Mainloop::waker()`: hands out a waker tied to this mainloop. the handle stores the mainloop pointer behind a `Mutex<*mut MainloopInternal>`. `MainloopWaker::wakeup` holds the mutex across `pa_mainloop_wakeup`; `Mainloop::drop` nulls the pointer under the same mutex before the underlying `pa_mainloop` is freed. wakers therefore never observe a freed pointer and become no-ops once the mainloop is gone. `Rc` is left unchanged: the rest of the standard mainloop API is `!Send` by design, so the `Arc`-backed sidecar is scoped to the one operation that genuinely is cross-thread. Came up in https://github.com/JakeStanger/ironbar/issues/875 --- pulse-binding/src/mainloop/standard.rs | 96 +++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/pulse-binding/src/mainloop/standard.rs b/pulse-binding/src/mainloop/standard.rs index 514a12ca..137bb720 100644 --- a/pulse-binding/src/mainloop/standard.rs +++ b/pulse-binding/src/mainloop/standard.rs @@ -199,6 +199,7 @@ use std::os::raw::{c_ulong, c_void}; use std::rc::Rc; +use std::sync::{Arc, Mutex}; #[cfg(not(windows))] use libc::pollfd; #[cfg(windows)] @@ -268,6 +269,85 @@ impl IterateResult { pub struct Mainloop { /// The ref-counted inner data. pub _inner: Rc>, + + /// Shared with [`MainloopWaker`] clones; invalidated on drop. + waker_handle: Arc, +} + +/// Storage shared between a [`Mainloop`] and its [`MainloopWaker`] clones. The +/// pointer is nulled by `Mainloop::drop` under the mutex, before the underlying +/// main loop is freed, so wakers can never observe a freed pointer. +struct MainloopWakerHandle { + ptr: Mutex<*mut MainloopInternal>, +} + +// SAFETY: `pa_mainloop_wakeup` is documented as thread safe; the pointer is +// never used outside that call, serialised by the mutex. +unsafe impl Send for MainloopWakerHandle {} +unsafe impl Sync for MainloopWakerHandle {} + +/// A `Send + Sync + Clone` handle for waking a [`Mainloop`] from any thread. +/// +/// Obtain one from [`Mainloop::waker()`]. [`wakeup()`](Self::wakeup) wraps the +/// thread safe `pa_mainloop_wakeup` C function, and is a no-op once the owning +/// [`Mainloop`] has been dropped. +/// +/// # Example +/// +/// ```rust,no_run +/// use std::thread; +/// use std::time::Duration; +/// use libpulse_binding::mainloop::standard::Mainloop; +/// +/// let mut mainloop = Mainloop::new().unwrap(); +/// let waker = mainloop.waker(); +/// +/// thread::spawn(move || { +/// thread::sleep(Duration::from_secs(1)); +/// waker.wakeup(); +/// }); +/// +/// mainloop.iterate(true); +/// ``` +#[derive(Clone)] +pub struct MainloopWaker { + handle: Arc, +} + +impl std::fmt::Debug for MainloopWaker { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MainloopWaker").finish_non_exhaustive() + } +} + +impl MainloopWaker { + /// Interrupts a running poll inside the owning [`Mainloop`]. + /// + /// Safe to call from any thread. Becomes a no-op once the owning `Mainloop` + /// has been dropped. + #[inline] + pub fn wakeup(&self) { + // poisoning is not reachable in practice (only FFI and a pointer write + // run under this lock), but treat it as a no-op for consistency. + if let Ok(guard) = self.handle.ptr.lock() { + if !guard.is_null() { + // SAFETY: thread safe per PulseAudio docs; the pointer is kept + // valid for the duration of this call by the mutex. + unsafe { capi::pa_mainloop_wakeup(*guard); } + } + } + } +} + +impl Drop for Mainloop { + fn drop(&mut self) { + // invalidate outstanding wakers before the inner `Rc` drop frees the + // main loop. concurrent `wakeup` calls hold this lock for the duration + // of `pa_mainloop_wakeup`, so no stale pointer survives this release. + if let Ok(mut guard) = self.waker_handle.ptr.lock() { + *guard = std::ptr::null_mut(); + } + } } impl MainloopTrait for Mainloop { @@ -301,7 +381,18 @@ impl Mainloop { MainloopInner::::new(ptr, std::mem::transmute(api_ptr), MainloopInner::::drop_actual, true) }; - Some(Self { _inner: Rc::new(ml_inner) }) + Some(Self { + _inner: Rc::new(ml_inner), + waker_handle: Arc::new(MainloopWakerHandle { ptr: Mutex::new(ptr) }), + }) + } + + /// Returns a [`Send`] handle for interrupting a blocking + /// [`iterate()`](Self::iterate) / [`poll()`](Self::poll) call from another + /// thread. See [`MainloopWaker`]. + #[inline] + pub fn waker(&self) -> MainloopWaker { + MainloopWaker { handle: Arc::clone(&self.waker_handle) } } /// Prepares for a single iteration of the main loop. @@ -420,6 +511,9 @@ impl Mainloop { } /// Interrupts a running poll (for threaded systems). + /// + /// To wake the main loop from a different thread, use [`waker()`](Self::waker) + /// to obtain a [`Send`] handle. #[inline] pub fn wakeup(&mut self) { unsafe { capi::pa_mainloop_wakeup(self._inner.get_ptr()); }