Skip to content

Commit 3129fd7

Browse files
authored
Rollup merge of rust-lang#92598 - Badel2:panic-update-hook, r=yaahc
Implement `panic::update_hook` Add a new function `panic::update_hook` to allow creating panic hooks that forward the call to the previously set panic hook, without race conditions. It works by taking a closure that transforms the old panic hook into a new one, while ensuring that during the execution of the closure no other thread can modify the panic hook. This is a small function so I hope it can be discussed here without a formal RFC, however if you prefer I can write one. Consider the following example: ```rust let prev = panic::take_hook(); panic::set_hook(Box::new(move |info| { println!("panic handler A"); prev(info); })); ``` This is a common pattern in libraries that need to do something in case of panic: log panic to a file, record code coverage, send panic message to a monitoring service, print custom message with link to github to open a new issue, etc. However it is impossible to avoid race conditions with the current API, because two threads can execute in this order: * Thread A calls `panic::take_hook()` * Thread B calls `panic::take_hook()` * Thread A calls `panic::set_hook()` * Thread B calls `panic::set_hook()` And the result is that the original panic hook has been lost, as well as the panic hook set by thread A. The resulting panic hook will be the one set by thread B, which forwards to the default panic hook. This is not considered a big issue because the panic handler setup is usually run during initialization code, probably before spawning any other threads. Using the new `panic::update_hook` function, this race condition is impossible, and the result will be either `A, B, original` or `B, A, original`. ```rust panic::update_hook(|prev| { Box::new(move |info| { println!("panic handler A"); prev(info); }) }); ``` I found one real world use case here: https://github.com/dtolnay/proc-macro2/blob/988cf403e741aadfd5340bbf67e35e1062a526aa/src/detection.rs#L32 the workaround is to detect the race condition and panic in that case. The pattern of `take_hook` + `set_hook` is very common, you can see some examples in this pull request, so I think it's natural to have a function that combines them both. Also using `update_hook` instead of `take_hook` + `set_hook` reduces the number of calls to `HOOK_LOCK.write()` from 2 to 1, but I don't expect this to make any difference in performance. ### Unresolved questions: * `panic::update_hook` takes a closure, if that closure panics the error message is "panicked while processing panic" which is not nice. This is a consequence of holding the `HOOK_LOCK` while executing the closure. Could be avoided using `catch_unwind`? * Reimplement `panic::set_hook` as `panic::update_hook(|_prev| hook)`?
2 parents 8a93aef + 0c58586 commit 3129fd7

File tree

7 files changed

+124
-6
lines changed

7 files changed

+124
-6
lines changed

library/alloc/tests/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
#![feature(const_trait_impl)]
3939
#![feature(const_str_from_utf8)]
4040
#![feature(nonnull_slice_from_raw_parts)]
41+
#![feature(panic_update_hook)]
4142

4243
use std::collections::hash_map::DefaultHasher;
4344
use std::hash::{Hash, Hasher};

library/alloc/tests/slice.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -1783,12 +1783,11 @@ thread_local!(static SILENCE_PANIC: Cell<bool> = Cell::new(false));
17831783
#[test]
17841784
#[cfg_attr(target_os = "emscripten", ignore)] // no threads
17851785
fn panic_safe() {
1786-
let prev = panic::take_hook();
1787-
panic::set_hook(Box::new(move |info| {
1786+
panic::update_hook(move |prev, info| {
17881787
if !SILENCE_PANIC.with(|s| s.get()) {
17891788
prev(info);
17901789
}
1791-
}));
1790+
});
17921791

17931792
let mut rng = thread_rng();
17941793

library/proc_macro/src/bridge/client.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -310,16 +310,15 @@ impl Bridge<'_> {
310310
// NB. the server can't do this because it may use a different libstd.
311311
static HIDE_PANICS_DURING_EXPANSION: Once = Once::new();
312312
HIDE_PANICS_DURING_EXPANSION.call_once(|| {
313-
let prev = panic::take_hook();
314-
panic::set_hook(Box::new(move |info| {
313+
panic::update_hook(move |prev, info| {
315314
let show = BridgeState::with(|state| match state {
316315
BridgeState::NotConnected => true,
317316
BridgeState::Connected(_) | BridgeState::InUse => force_show_panics,
318317
});
319318
if show {
320319
prev(info)
321320
}
322-
}));
321+
});
323322
});
324323

325324
BRIDGE_STATE.with(|state| state.set(BridgeState::Connected(self), f))

library/proc_macro/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#![feature(restricted_std)]
3131
#![feature(rustc_attrs)]
3232
#![feature(min_specialization)]
33+
#![feature(panic_update_hook)]
3334
#![recursion_limit = "256"]
3435

3536
#[unstable(feature = "proc_macro_internals", issue = "27812")]

library/std/src/panic.rs

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ pub use core::panic::panic_2021;
3636
#[stable(feature = "panic_hooks", since = "1.10.0")]
3737
pub use crate::panicking::{set_hook, take_hook};
3838

39+
#[unstable(feature = "panic_update_hook", issue = "92649")]
40+
pub use crate::panicking::update_hook;
41+
3942
#[stable(feature = "panic_hooks", since = "1.10.0")]
4043
pub use core::panic::{Location, PanicInfo};
4144

library/std/src/panicking.rs

+79
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ enum Hook {
7676
Custom(*mut (dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send)),
7777
}
7878

79+
impl Hook {
80+
fn custom(f: impl Fn(&PanicInfo<'_>) + 'static + Sync + Send) -> Self {
81+
Self::Custom(Box::into_raw(Box::new(f)))
82+
}
83+
}
84+
7985
static HOOK_LOCK: StaticRWLock = StaticRWLock::new();
8086
static mut HOOK: Hook = Hook::Default;
8187

@@ -118,6 +124,11 @@ pub fn set_hook(hook: Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send>) {
118124
panic!("cannot modify the panic hook from a panicking thread");
119125
}
120126

127+
// SAFETY:
128+
//
129+
// - `HOOK` can only be modified while holding write access to `HOOK_LOCK`.
130+
// - The argument of `Box::from_raw` is always a valid pointer that was created using
131+
// `Box::into_raw`.
121132
unsafe {
122133
let guard = HOOK_LOCK.write();
123134
let old_hook = HOOK;
@@ -167,6 +178,11 @@ pub fn take_hook() -> Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send> {
167178
panic!("cannot modify the panic hook from a panicking thread");
168179
}
169180

181+
// SAFETY:
182+
//
183+
// - `HOOK` can only be modified while holding write access to `HOOK_LOCK`.
184+
// - The argument of `Box::from_raw` is always a valid pointer that was created using
185+
// `Box::into_raw`.
170186
unsafe {
171187
let guard = HOOK_LOCK.write();
172188
let hook = HOOK;
@@ -180,6 +196,69 @@ pub fn take_hook() -> Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send> {
180196
}
181197
}
182198

199+
/// Atomic combination of [`take_hook`] and [`set_hook`]. Use this to replace the panic handler with
200+
/// a new panic handler that does something and then executes the old handler.
201+
///
202+
/// [`take_hook`]: ./fn.take_hook.html
203+
/// [`set_hook`]: ./fn.set_hook.html
204+
///
205+
/// # Panics
206+
///
207+
/// Panics if called from a panicking thread.
208+
///
209+
/// # Examples
210+
///
211+
/// The following will print the custom message, and then the normal output of panic.
212+
///
213+
/// ```should_panic
214+
/// #![feature(panic_update_hook)]
215+
/// use std::panic;
216+
///
217+
/// // Equivalent to
218+
/// // let prev = panic::take_hook();
219+
/// // panic::set_hook(move |info| {
220+
/// // println!("...");
221+
/// // prev(info);
222+
/// // );
223+
/// panic::update_hook(move |prev, info| {
224+
/// println!("Print custom message and execute panic handler as usual");
225+
/// prev(info);
226+
/// });
227+
///
228+
/// panic!("Custom and then normal");
229+
/// ```
230+
#[unstable(feature = "panic_update_hook", issue = "92649")]
231+
pub fn update_hook<F>(hook_fn: F)
232+
where
233+
F: Fn(&(dyn Fn(&PanicInfo<'_>) + Send + Sync + 'static), &PanicInfo<'_>)
234+
+ Sync
235+
+ Send
236+
+ 'static,
237+
{
238+
if thread::panicking() {
239+
panic!("cannot modify the panic hook from a panicking thread");
240+
}
241+
242+
// SAFETY:
243+
//
244+
// - `HOOK` can only be modified while holding write access to `HOOK_LOCK`.
245+
// - The argument of `Box::from_raw` is always a valid pointer that was created using
246+
// `Box::into_raw`.
247+
unsafe {
248+
let guard = HOOK_LOCK.write();
249+
let old_hook = HOOK;
250+
HOOK = Hook::Default;
251+
252+
let prev = match old_hook {
253+
Hook::Default => Box::new(default_hook),
254+
Hook::Custom(ptr) => Box::from_raw(ptr),
255+
};
256+
257+
HOOK = Hook::custom(move |info| hook_fn(&prev, info));
258+
drop(guard);
259+
}
260+
}
261+
183262
fn default_hook(info: &PanicInfo<'_>) {
184263
// If this is a double panic, make sure that we print a backtrace
185264
// for this panic. Otherwise only print it if logging is enabled.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// run-pass
2+
// needs-unwind
3+
#![allow(stable_features)]
4+
5+
// ignore-emscripten no threads support
6+
7+
#![feature(std_panic)]
8+
#![feature(panic_update_hook)]
9+
10+
use std::sync::atomic::{AtomicUsize, Ordering};
11+
use std::panic;
12+
use std::thread;
13+
14+
static A: AtomicUsize = AtomicUsize::new(0);
15+
static B: AtomicUsize = AtomicUsize::new(0);
16+
static C: AtomicUsize = AtomicUsize::new(0);
17+
18+
fn main() {
19+
panic::set_hook(Box::new(|_| { A.fetch_add(1, Ordering::SeqCst); }));
20+
panic::update_hook(|prev, info| {
21+
B.fetch_add(1, Ordering::SeqCst);
22+
prev(info);
23+
});
24+
panic::update_hook(|prev, info| {
25+
C.fetch_add(1, Ordering::SeqCst);
26+
prev(info);
27+
});
28+
29+
let _ = thread::spawn(|| {
30+
panic!();
31+
}).join();
32+
33+
assert_eq!(1, A.load(Ordering::SeqCst));
34+
assert_eq!(1, B.load(Ordering::SeqCst));
35+
assert_eq!(1, C.load(Ordering::SeqCst));
36+
}

0 commit comments

Comments
 (0)