Skip to content

Commit e5694d8

Browse files
authored
Store client reference in lifetime Arc (#235)
1 parent 8b53877 commit e5694d8

9 files changed

Lines changed: 310 additions & 116 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
## Unreleased
44

5-
- Failed client activation now closes the client and frees both callback handlers instead of
6-
leaking them ([#232](https://github.com/RustAudio/rust-jack/issues/232)).
5+
- Ports and transport objects now hold strong references to their JACK client and remain
6+
usable after `Client` is dropped.
7+
- Added `ClientHandle` with an `as_ptr` method. `Port::from_raw` now takes
8+
`Arc<ClientHandle>` instead of `Weak<()>` and no longer takes a separate client pointer.
9+
- Callback cleanup now explicitly stops processing and detaches callbacks before freeing
10+
their handlers, including after failed activation, even if ports or transport keep the
11+
client open ([#232](https://github.com/RustAudio/rust-jack/issues/232)).
712
- `Error` is now `#[non_exhaustive]`. `match`es on it outside of this crate need a wildcard arm.
813
- Added `Error::PortUnregistrationError`.
914
- `Client::unregister_port` now returns `Err(Error::PortUnregistrationError)` on failure instead

‎src/client/async_client.rs‎

Lines changed: 87 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use crate::Error;
3131
/// eprintln!("Error deactivating client: {err}");
3232
/// };
3333
/// ```
34-
#[must_use = "The jack client is shut down when the AsyncClient is dropped. You most likely want to keep this alive and manually tear down with `AsyncClient::deactivate`."]
34+
#[must_use = "Processing stops when the AsyncClient is dropped. You most likely want to keep this alive and manually tear down with `AsyncClient::deactivate`."]
3535
pub struct AsyncClient<N, P> {
3636
callback: Option<Box<CallbackContext<N, P>>>,
3737
}
@@ -48,8 +48,9 @@ where
4848
/// methods specified by the `NotificationHandler` and `ProcessHandler` objects.
4949
///
5050
/// On failure, either `Err(Error::CallbackRegistrationError)` or
51-
/// `Err(Error::ClientActivationError)` is returned. The client is closed and both handlers
52-
/// are dropped.
51+
/// `Err(Error::ClientActivationError)` is returned. Callbacks are detached and both handlers
52+
/// are dropped. Ports and transport objects may keep the JACK client open.
53+
/// If callback cleanup also fails, the callback context is retained to keep JACK's pointers valid.
5354
///
5455
/// `notification_handler` and `process_handler` are consumed, but they are returned when
5556
/// `Client::deactivate` is called.
@@ -78,13 +79,26 @@ where
7879
callback_lock: std::sync::Mutex::new(()),
7980
});
8081
// Declare the guard after the context so it is released before error cleanup:
81-
// Client::drop also acquires this mutex.
82+
// ClientHandle::drop also acquires this mutex.
8283
let _m = CREATE_OR_DESTROY_CLIENT_MUTEX.lock().ok();
83-
unsafe {
84-
CallbackContext::register_callbacks(&mut callback_context)?;
85-
}
86-
if activate(&callback_context.client) != 0 {
87-
return Err(Error::ClientActivationError);
84+
let registration = unsafe { CallbackContext::register_callbacks(&mut callback_context) };
85+
let result = registration.and_then(|()| {
86+
if activate(&callback_context.client) == 0 {
87+
Ok(())
88+
} else {
89+
Err(Error::ClientActivationError)
90+
}
91+
});
92+
if let Err(error) = result {
93+
if Self::detach_callbacks(&callback_context).is_err() {
94+
crate::logging::log_error(
95+
b"Failed to detach callbacks after activation error. Leaking callback context.\0",
96+
);
97+
// JACK may still hold pointers into the context. Retain it rather
98+
// than free memory that a callback could still access.
99+
Box::leak(callback_context);
100+
}
101+
return Err(error);
88102
}
89103
Ok(AsyncClient {
90104
callback: Some(callback_context),
@@ -106,8 +120,9 @@ impl<N, P> AsyncClient<N, P> {
106120
/// The `handler` that was used for `Client::activate` is returned on success. Its state may
107121
/// have changed due to JACK calling its methods.
108122
///
109-
/// In the case of error, the `Client` is destroyed because its state is unknown, and it is
110-
/// therefore unsafe to continue using.
123+
/// In the case of error, the `Client` is dropped. Ports and transport objects may keep the
124+
/// underlying JACK client open.
125+
/// If callback cleanup fails, the callback context is retained to keep JACK's pointers valid.
111126
pub fn deactivate(self) -> Result<(Client, N, P), Error> {
112127
let mut c = self;
113128
c.maybe_deactivate()
@@ -123,29 +138,40 @@ impl<N, P> AsyncClient<N, P> {
123138
return Err(Error::ClientIsNoLongerAlive);
124139
}
125140
let cb = self.callback.take().ok_or(Error::ClientIsNoLongerAlive)?;
126-
drop(cb.callback_lock.lock());
127-
cb.is_valid_for_callback
128-
.store(false, std::sync::atomic::Ordering::Relaxed);
129-
// deactivate
130-
if unsafe { j::jack_deactivate(cb.client.raw()) } != 0 {
141+
if let Err(error) = Self::detach_callbacks(&cb) {
142+
crate::logging::log_error(
143+
b"Failed to detach callbacks on deactivation. Leaking callback context.\0",
144+
);
145+
// Keep the callback data alive if JACK could still use it.
146+
Box::leak(cb);
131147
drop(m);
132-
return Err(Error::ClientDeactivationError);
148+
return Err(error);
133149
}
134150

135-
// clear the callbacks
136-
unsafe { clear_callbacks(cb.client.raw()) }?;
137151
// done, take ownership of callback
138152
if cb.has_panic.load(std::sync::atomic::Ordering::Relaxed) {
139153
drop(m);
140154
return Err(Error::ClientPanicked);
141155
}
142156
Ok(cb)
143157
}
158+
159+
fn detach_callbacks(cb: &CallbackContext<N, P>) -> Result<(), Error> {
160+
{
161+
let _guard = cb.callback_lock.lock();
162+
cb.is_valid_for_callback
163+
.store(false, std::sync::atomic::Ordering::Relaxed);
164+
}
165+
if unsafe { j::jack_deactivate(cb.client.raw()) } != 0 {
166+
return Err(Error::ClientDeactivationError);
167+
}
168+
unsafe { clear_callbacks(cb.client.raw()) }
169+
}
144170
}
145171

146-
/// Closes the client.
172+
/// Stops processing and releases this reference to the client.
147173
impl<N, P> Drop for AsyncClient<N, P> {
148-
// Deactivate and close the client.
174+
// Deactivate and release the client.
149175
fn drop(&mut self) {
150176
let _ = self.maybe_deactivate();
151177
}
@@ -195,15 +221,17 @@ mod tests {
195221

196222
impl Drop for Handler {
197223
fn drop(&mut self) {
198-
// The client must be closed before either handler is destroyed.
199-
assert_eq!(self.port.name(), Err(Error::ClientIsNoLongerAlive));
224+
// Each handler's port keeps the client alive during destruction.
225+
assert!(self.port.name().is_ok());
200226
self.drops.fetch_add(1, Ordering::SeqCst);
201227
}
202228
}
203229

204230
fn check_activation_failure(start_processing: bool) {
205231
let (client, _) = Client::new("", ClientOptions::default()).unwrap();
206232
let port = client.register_port("input", AudioIn::default()).unwrap();
233+
let transport = client.transport();
234+
let life = Arc::downgrade(&transport.client);
207235
let notification_drops = Arc::new(AtomicUsize::new(0));
208236
let process_drops = Arc::new(AtomicUsize::new(0));
209237
let (send, recv) = sync_channel(1);
@@ -229,16 +257,49 @@ mod tests {
229257
assert_eq!(result.err(), Some(Error::ClientActivationError));
230258
assert_eq!(notification_drops.load(Ordering::SeqCst), 1);
231259
assert_eq!(process_drops.load(Ordering::SeqCst), 1);
232-
assert_eq!(port.name(), Err(Error::ClientIsNoLongerAlive));
260+
assert!(port.name().is_ok());
261+
// The retained client must be safe to activate after its original
262+
// callback context and handlers have been freed.
263+
assert_eq!(unsafe { j::jack_activate(port.client_ptr()) }, 0);
264+
assert_eq!(unsafe { j::jack_deactivate(port.client_ptr()) }, 0);
265+
drop(port);
266+
assert!(life.upgrade().is_some());
267+
drop(transport);
268+
assert!(life.upgrade().is_none());
233269
}
234270

235271
#[test]
236-
fn activation_failure_closes_client_and_drops_handlers() {
272+
fn activation_failure_detaches_callbacks_and_drops_handlers() {
237273
check_activation_failure(false);
238274
}
239275

240276
#[test]
241-
fn activation_failure_after_start_closes_client_and_drops_handlers() {
277+
fn activation_failure_after_start_detaches_callbacks_and_drops_handlers() {
242278
check_activation_failure(true);
243279
}
280+
281+
#[test]
282+
fn dropping_async_client_detaches_callbacks_while_port_keeps_client_alive() {
283+
let (client, _) = Client::new("", ClientOptions::default()).unwrap();
284+
let port = client.register_port("input", AudioIn::default()).unwrap();
285+
let transport = client.transport();
286+
let life = Arc::downgrade(&transport.client);
287+
let drops = Arc::new(AtomicUsize::new(0));
288+
let handler = || Handler {
289+
drops: drops.clone(),
290+
port: port.clone_unowned(),
291+
processed: None,
292+
};
293+
let active = client.activate_async(handler(), handler()).unwrap();
294+
295+
drop(active);
296+
assert_eq!(drops.load(Ordering::SeqCst), 2);
297+
assert!(port.name().is_ok());
298+
assert_eq!(unsafe { j::jack_activate(port.client_ptr()) }, 0);
299+
assert_eq!(unsafe { j::jack_deactivate(port.client_ptr()) }, 0);
300+
301+
drop(port);
302+
drop(transport);
303+
assert!(life.upgrade().is_none());
304+
}
244305
}

‎src/client/callbacks.rs‎

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -496,30 +496,45 @@ where
496496
}
497497
}
498498

499+
unsafe extern "C" fn noop_thread_init_callback(_: *mut libc::c_void) {}
500+
499501
/// Unsafe ffi wrapper that clears the callbacks registered to `client`.
500502
///
501503
/// This is mostly for use within the jack crate itself.
502504
///
503505
/// Returns `Err(Error::CallbackDeregistrationError)` on failure.
504506
///
505-
/// # Unsafe
506-
///
507-
/// * Uses ffi calls, be careful.
508-
///
509-
/// # TODO
510-
///
511-
/// * Implement correctly. Freezes on my system. Maybe this makes sense now? it doesn't disturb my program
507+
/// # Safety
508+
/// The pointer must refer to a valid, inactive JACK client.
512509
pub unsafe fn clear_callbacks(client: *mut j::jack_client_t) -> Result<(), Error> {
513-
j::jack_set_thread_init_callback(client, None, std::ptr::null_mut());
514-
j::jack_set_process_callback(client, None, std::ptr::null_mut());
515-
Ok(())
510+
let data = std::ptr::null_mut();
511+
j::jack_on_info_shutdown(client, None, data);
512+
let results = [
513+
// jackd2 rejects a null thread init callback, so install a no-op instead.
514+
j::jack_set_thread_init_callback(client, Some(noop_thread_init_callback), data),
515+
j::jack_set_process_callback(client, None, data),
516+
j::jack_set_sync_callback(client, None, data),
517+
j::jack_set_freewheel_callback(client, None, data),
518+
j::jack_set_buffer_size_callback(client, None, data),
519+
j::jack_set_sample_rate_callback(client, None, data),
520+
j::jack_set_client_registration_callback(client, None, data),
521+
j::jack_set_port_registration_callback(client, None, data),
522+
j::jack_set_port_connect_callback(client, None, data),
523+
j::jack_set_graph_order_callback(client, None, data),
524+
j::jack_set_xrun_callback(client, None, data),
525+
];
526+
if results.iter().any(|&result| result != 0) {
527+
Err(Error::CallbackDeregistrationError)
528+
} else {
529+
Ok(())
530+
}
516531
}
517532

518533
/// The information used by JACK to process data.
519534
pub struct CallbackContext<N, P> {
520535
/// The underlying JACK client.
521-
/// Keep this field first so the client is closed, stopping callbacks, before the handlers
522-
/// and the rest of the context are dropped.
536+
/// Callbacks must be stopped and detached before this context is dropped:
537+
/// ports and transport may keep the JACK client open afterwards.
523538
pub client: Client,
524539
/// The handler for notifications.
525540
pub notification: N,

0 commit comments

Comments
 (0)