-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Remove configurable_error_handler feature #18801
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b9c7c84
3e678e2
1ebe009
d5304dc
fb625e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,3 @@ | ||
#[cfg(feature = "configurable_error_handler")] | ||
use bevy_platform::sync::OnceLock; | ||
use core::fmt::Display; | ||
|
||
use crate::{component::Tick, error::BevyError}; | ||
|
@@ -77,53 +75,83 @@ impl ErrorContext { | |
} | ||
} | ||
|
||
/// A global error handler. This can be set at startup, as long as it is set before | ||
/// any uses. This should generally be configured _before_ initializing the app. | ||
/// | ||
/// This should be set inside of your `main` function, before initializing the Bevy app. | ||
/// The value of this error handler can be accessed using the [`default_error_handler`] function, | ||
/// which calls [`OnceLock::get_or_init`] to get the value. | ||
/// | ||
/// **Note:** this is only available when the `configurable_error_handler` feature of `bevy_ecs` (or `bevy`) is enabled! | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// # use bevy_ecs::error::{GLOBAL_ERROR_HANDLER, warn}; | ||
/// GLOBAL_ERROR_HANDLER.set(warn).expect("The error handler can only be set once, globally."); | ||
/// // initialize Bevy App here | ||
/// ``` | ||
/// | ||
/// To use this error handler in your app for custom error handling logic: | ||
/// | ||
/// ```rust | ||
/// use bevy_ecs::error::{default_error_handler, GLOBAL_ERROR_HANDLER, BevyError, ErrorContext, panic}; | ||
/// | ||
/// fn handle_errors(error: BevyError, ctx: ErrorContext) { | ||
/// let error_handler = default_error_handler(); | ||
/// error_handler(error, ctx); | ||
/// } | ||
/// ``` | ||
/// | ||
/// # Warning | ||
/// | ||
/// As this can *never* be overwritten, library code should never set this value. | ||
#[cfg(feature = "configurable_error_handler")] | ||
pub static GLOBAL_ERROR_HANDLER: OnceLock<fn(BevyError, ErrorContext)> = OnceLock::new(); | ||
|
||
/// The default error handler. This defaults to [`panic()`], | ||
/// but if set, the [`GLOBAL_ERROR_HANDLER`] will be used instead, enabling error handler customization. | ||
/// The `configurable_error_handler` feature must be enabled to change this from the panicking default behavior, | ||
/// as there may be runtime overhead. | ||
#[inline] | ||
pub fn default_error_handler() -> fn(BevyError, ErrorContext) { | ||
#[cfg(not(feature = "configurable_error_handler"))] | ||
return panic; | ||
mod global_error_handler { | ||
use super::{panic, BevyError, ErrorContext}; | ||
use bevy_platform::sync::atomic::{ | ||
AtomicBool, AtomicPtr, | ||
Ordering::{AcqRel, Acquire, Relaxed}, | ||
}; | ||
|
||
/// The default global error handler, cast to a data pointer as Rust doesn't | ||
/// currently have a way to express atomic function pointers. | ||
/// Should we add support for a platform on which function pointers and data pointers | ||
/// have different sizes, the transmutation back will fail to compile. In that case, | ||
/// we can replace the atomic pointer with a regular pointer protected by a `OnceLock` | ||
/// on only those platforms. | ||
/// SAFETY: Only accessible from within this module. | ||
static HANDLER: AtomicPtr<()> = AtomicPtr::new(panic as *mut ()); | ||
|
||
/// Set the global error handler. | ||
/// | ||
/// If used, this should be called [before] any uses of [`default_error_handler`], | ||
/// generally inside your `main` function before initializing the app. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// # use bevy_ecs::error::{set_global_default_error_handler, warn}; | ||
/// set_global_default_error_handler(warn); | ||
/// // initialize Bevy App here | ||
/// ``` | ||
/// | ||
/// To use this error handler in your app for custom error handling logic: | ||
/// | ||
/// ```rust | ||
/// use bevy_ecs::error::{default_error_handler, BevyError, ErrorContext}; | ||
/// | ||
/// fn handle_errors(error: BevyError, ctx: ErrorContext) { | ||
/// let error_handler = default_error_handler(); | ||
/// error_handler(error, ctx); | ||
/// } | ||
/// ``` | ||
/// | ||
/// # Warning | ||
/// | ||
/// As this can *never* be overwritten, library code should never set this value. | ||
/// | ||
/// [before]: https://doc.rust-lang.org/nightly/core/sync/atomic/index.html#memory-model-for-atomic-accesses | ||
/// [`default_error_handler`]: super::default_error_handler | ||
pub fn set_global_default_error_handler(handler: fn(BevyError, ErrorContext)) { | ||
// Prevent the handler from being set multiple times. | ||
// We use a separate atomic instead of trying `compare_exchange` on `HANDLER_ADDRESS` | ||
// because Rust doesn't guarantee that function addresses are unique. | ||
static INITIALIZED: AtomicBool = AtomicBool::new(false); | ||
if INITIALIZED | ||
.compare_exchange(false, true, AcqRel, Acquire) | ||
.is_err() | ||
{ | ||
panic!("Global error handler set multiple times"); | ||
} | ||
HANDLER.store(handler as *mut (), Relaxed); | ||
} | ||
|
||
#[cfg(feature = "configurable_error_handler")] | ||
return *GLOBAL_ERROR_HANDLER.get_or_init(|| panic); | ||
/// The default error handler. This defaults to [`panic`], | ||
/// but you can override this behavior via [`set_global_default_error_handler`]. | ||
/// | ||
/// [`panic`]: super::panic | ||
#[inline] | ||
pub fn default_error_handler() -> fn(BevyError, ErrorContext) { | ||
// The error handler must have been already set from the perspective of this thread, | ||
// otherwise we will panic. It will never be updated after this point. | ||
// We therefore only need a relaxed load. | ||
let ptr = HANDLER.load(Relaxed); | ||
// SAFETY: We only ever store valid handler functions. | ||
unsafe { core::mem::transmute(ptr) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The rust docs have this to say on the topic of raw-pointer to function-pointer transmutation:
Is this something we are concerned about? If not, it would probably be good to mention it in or around the safety comment there. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment. I haven't added a workaround for those platforms yet because that would need a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a crate called |
||
} | ||
} | ||
|
||
pub use global_error_handler::{default_error_handler, set_global_default_error_handler}; | ||
|
||
macro_rules! inner { | ||
($call:path, $e:ident, $c:ident) => { | ||
$call!( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
Relaxed
operations on a pointer makes me nervous, but I think this is okay for anything but edge cases.The danger would be that a user wants some run-time configuration for their global error handler, and writes to a global variable before calling
set
, which then doesn't get synchronized with the load. But writes to a global variable will require their own synchronization, so it's hard to come up with a situation where that leads to a data race, and doing so would requireunsafe
code elsewhere. (It might also be possible to get into trouble using runtime code generation? But nobody would ever generate an error handler function that way.)I'd still be inclined to use
Acquire
here (andRelease
for the store) to be safer. Acquire loads aren't usually any slower than relaxed ones. In particular, all loads on x86 have Acquire semantics, so they'll compile to the same machine instruction.