Backtrace: std and threadsafe bevy_error_panic_hook (#18235)

# Objective

Make `bevy_error_panic_hook` threadsafe. As it relies on a global
variable, it fails when multiple threads panic.

## Solution

Switch from a global variable for storing whether an error message was
printed to a thread-local one.

`thread_local` is in `std`; the `backtrace` already relies on `std`
APIs. It didn't depend on the `std` feature though, so I've added that.
I've also put `bevy_error_panic_hook` behind the `backtrace` feature,
since it relies on the thread local variable, which fixes #18231.

## Testing

The following now loops instead of crashing:

```rust
std:🧵:scope(|s| {
    use bevy_ecs::error::*;

    #[derive(Debug)]
    struct E;
    impl std::fmt::Display for E {
        fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            todo!()
        }
    }
    impl std::error::Error for E {}

    std::panic::set_hook(Box::new(bevy_error_panic_hook(|_| {
        unreachable!();
    })));
    for _ in 0..2 {
        s.spawn(|| {
            loop {
                let _ = std::panic::catch_unwind(|| {
                    panic!("{:?}", BevyError::from(E));
                });
            }
        });
    }
});
```
This commit is contained in:
SpecificProtagonist 2025-03-10 22:16:14 +01:00 committed by GitHub
parent b054d116a6
commit cdef139710
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 8 additions and 7 deletions

View File

@ -37,7 +37,7 @@ reflect_functions = ["bevy_reflect", "bevy_reflect/functions"]
configurable_error_handler = [] configurable_error_handler = []
## Enables automatic backtrace capturing in BevyError ## Enables automatic backtrace capturing in BevyError
backtrace = [] backtrace = ["std"]
# Debugging Features # Debugging Features

View File

@ -117,7 +117,7 @@ impl Debug for BevyError {
} }
if !full_backtrace { if !full_backtrace {
if std::thread::panicking() { if std::thread::panicking() {
SKIP_NORMAL_BACKTRACE.store(1, core::sync::atomic::Ordering::Relaxed); SKIP_NORMAL_BACKTRACE.set(true);
} }
writeln!(f, "{FILTER_MESSAGE}")?; writeln!(f, "{FILTER_MESSAGE}")?;
} }
@ -132,22 +132,23 @@ impl Debug for BevyError {
const FILTER_MESSAGE: &str = "note: Some \"noisy\" backtrace lines have been filtered out. Run with `BEVY_BACKTRACE=full` for a verbose backtrace."; const FILTER_MESSAGE: &str = "note: Some \"noisy\" backtrace lines have been filtered out. Run with `BEVY_BACKTRACE=full` for a verbose backtrace.";
#[cfg(feature = "backtrace")] #[cfg(feature = "backtrace")]
static SKIP_NORMAL_BACKTRACE: core::sync::atomic::AtomicUsize = std::thread_local! {
core::sync::atomic::AtomicUsize::new(0); static SKIP_NORMAL_BACKTRACE: core::cell::Cell<bool> =
const { core::cell::Cell::new(false) };
}
/// When called, this will skip the currently configured panic hook when a [`BevyError`] backtrace has already been printed. /// When called, this will skip the currently configured panic hook when a [`BevyError`] backtrace has already been printed.
#[cfg(feature = "std")] #[cfg(feature = "backtrace")]
pub fn bevy_error_panic_hook( pub fn bevy_error_panic_hook(
current_hook: impl Fn(&std::panic::PanicHookInfo), current_hook: impl Fn(&std::panic::PanicHookInfo),
) -> impl Fn(&std::panic::PanicHookInfo) { ) -> impl Fn(&std::panic::PanicHookInfo) {
move |info| { move |info| {
if SKIP_NORMAL_BACKTRACE.load(core::sync::atomic::Ordering::Relaxed) > 0 { if SKIP_NORMAL_BACKTRACE.replace(false) {
if let Some(payload) = info.payload().downcast_ref::<&str>() { if let Some(payload) = info.payload().downcast_ref::<&str>() {
std::println!("{payload}"); std::println!("{payload}");
} else if let Some(payload) = info.payload().downcast_ref::<alloc::string::String>() { } else if let Some(payload) = info.payload().downcast_ref::<alloc::string::String>() {
std::println!("{payload}"); std::println!("{payload}");
} }
SKIP_NORMAL_BACKTRACE.store(0, core::sync::atomic::Ordering::Relaxed);
return; return;
} }