diff --git a/crates/bevy_ecs/src/bundle/info.rs b/crates/bevy_ecs/src/bundle/info.rs index 7a278e3c4d627..abb874e59dcba 100644 --- a/crates/bevy_ecs/src/bundle/info.rs +++ b/crates/bevy_ecs/src/bundle/info.rs @@ -5,7 +5,11 @@ use bevy_platform::{ }; use bevy_ptr::{MovingPtr, OwningPtr}; use bevy_utils::TypeIdMap; -use core::{any::TypeId, ptr::NonNull}; +use core::{ + any::{Any, TypeId}, + panic::AssertUnwindSafe, + ptr::NonNull, +}; use indexmap::{IndexMap, IndexSet}; use crate::{ @@ -212,6 +216,8 @@ impl BundleInfo { /// This writes components from a given [`Bundle`] to the given entity. /// + /// If overwritten components panic during drop, the panic payload is returned. + /// /// # Safety /// /// `bundle_component_status` must return the "correct" [`ComponentStatus`] for each component @@ -248,7 +254,9 @@ impl BundleInfo { bundle: MovingPtr<'_, T>, insert_mode: InsertMode, caller: MaybeLocation, - ) { + ) -> Option> { + let mut panic = None; + // NOTE: get_components calls this closure on each component in "bundle order". // bundle_info.component_ids are also in "bundle order" let mut bundle_component = 0; @@ -258,7 +266,7 @@ impl BundleInfo { .get_unchecked(bundle_component); // SAFETY: bundle_component is a valid index for this bundle let status = unsafe { bundle_component_status.get_status(bundle_component) }; - match storage_type { + let f = || match storage_type { StorageType::Table => { let column = // SAFETY: If component_id is in self.component_ids, BundleInfo::new ensures that @@ -294,7 +302,13 @@ impl BundleInfo { } } } + }; + + let maybe_panic = bevy_utils::catch_unwind_if_available(AssertUnwindSafe(f)).err(); + if panic.is_none() { + panic = maybe_panic; } + bundle_component += 1; }); @@ -308,6 +322,8 @@ impl BundleInfo { caller, ); } + + panic } /// Internal method to initialize a required component from an [`OwningPtr`]. This should ultimately be called diff --git a/crates/bevy_ecs/src/bundle/insert.rs b/crates/bevy_ecs/src/bundle/insert.rs index 7eb2f64ac9845..ffb1e8afa000b 100644 --- a/crates/bevy_ecs/src/bundle/insert.rs +++ b/crates/bevy_ecs/src/bundle/insert.rs @@ -1,6 +1,7 @@ +use alloc::boxed::Box; use alloc::vec::Vec; use bevy_ptr::{ConstNonNull, MovingPtr}; -use core::ptr::NonNull; +use core::{any::Any, ptr::NonNull}; use crate::{ archetype::{ @@ -137,6 +138,7 @@ impl<'w> BundleInserter<'w> { EntityLocation, &'a mut SparseSets, &'a mut Table, + Option>, ) { // SAFETY: All components in the bundle are guaranteed to exist in the World // as they must be initialized before creating the BundleInfo. @@ -191,7 +193,7 @@ impl<'w> BundleInserter<'w> { ) }; - (&*archetype, location, sparse_sets, table) + (&*archetype, location, sparse_sets, table, None) } ArchetypeMoveType::NewArchetypeSameTable { new_archetype } => { let new_archetype = new_archetype.as_mut(); @@ -224,7 +226,7 @@ impl<'w> BundleInserter<'w> { let new_location = new_archetype.allocate(entity, result.table_row); entities.update_existing_location(entity.index(), Some(new_location)); - (&*new_archetype, new_location, sparse_sets, table) + (&*new_archetype, new_location, sparse_sets, table, None) } ArchetypeMoveType::NewArchetypeNewTable { new_archetype } => { let new_archetype = new_archetype.as_mut(); @@ -310,11 +312,14 @@ impl<'w> BundleInserter<'w> { new_location, sparse_sets, move_result.new_table, + move_result.panic, ) } } } + /// Returns the entity's new location and potentially a caught panic. + /// /// # Safety /// - `entity` must currently exist in the source archetype for this inserter. /// - `location` must be `entity`'s location in the archetype. @@ -334,24 +339,27 @@ impl<'w> BundleInserter<'w> { insert_mode: InsertMode, caller: MaybeLocation, relationship_hook_mode: RelationshipHookMode, - ) -> EntityLocation { + ) -> (EntityLocation, Option>) { let archetype_after_insert = self.archetype_after_insert.as_ref(); + let panic_payload; + let (new_archetype, new_location) = { // Non-generic prelude extracted to improve compile time by minimizing monomorphized code. - let (new_archetype, new_location, sparse_sets, table) = Self::before_insert( - entity, - location, - insert_mode, - caller, - relationship_hook_mode, - self.archetype, - archetype_after_insert, - &self.world, - &mut self.archetype_move_type, - ); + let (new_archetype, new_location, sparse_sets, table, archetype_move_panic_payload) = + Self::before_insert( + entity, + location, + insert_mode, + caller, + relationship_hook_mode, + self.archetype, + archetype_after_insert, + &self.world, + &mut self.archetype_move_type, + ); - self.bundle_info.as_ref().write_components( + let maybe_panic = self.bundle_info.as_ref().write_components( table, sparse_sets, archetype_after_insert, @@ -364,6 +372,8 @@ impl<'w> BundleInserter<'w> { caller, ); + panic_payload = archetype_move_panic_payload.or(maybe_panic); + (new_archetype, new_location) }; @@ -382,7 +392,7 @@ impl<'w> BundleInserter<'w> { deferred_world, ); - new_location + (new_location, panic_payload) } // A non-generic postlude to insert used to minimize duplicated monomorphized code. diff --git a/crates/bevy_ecs/src/bundle/remove.rs b/crates/bevy_ecs/src/bundle/remove.rs index 6b9f59186a835..c0e0c493445bc 100644 --- a/crates/bevy_ecs/src/bundle/remove.rs +++ b/crates/bevy_ecs/src/bundle/remove.rs @@ -1,5 +1,6 @@ use alloc::vec::Vec; use bevy_ptr::ConstNonNull; +use core::panic::AssertUnwindSafe; use core::ptr::NonNull; use crate::{ @@ -26,6 +27,12 @@ pub(crate) struct BundleRemover<'w> { pub(crate) relationship_hook_mode: RelationshipHookMode, } +pub(crate) struct BundleRemoveResult { + pub new_location: EntityLocation, + pub data: T, + pub panic_payload: Option>, +} + impl<'w> BundleRemover<'w> { /// Creates a new [`BundleRemover`], if such a remover would do anything. /// @@ -133,7 +140,7 @@ impl<'w> BundleRemover<'w> { &Components, &[ComponentId], ) -> (bool, T), - ) -> (EntityLocation, T) { + ) -> BundleRemoveResult { // Hooks // SAFETY: all bundle components exist in World unsafe { @@ -202,6 +209,11 @@ impl<'w> BundleRemover<'w> { self.bundle_info.as_ref().explicit_components(), ); + // Component's drop functions may panic. + // We mustn't leave the world in an inconsistent state if that happens. + // Catch any such panics, finish the removal, and rethrow the first one. + let mut panic_payload = None; + // Handle sparse set removes for component_id in self.bundle_info.as_ref().iter_explicit_components() { if self.old_archetype.as_ref().contains(component_id) { @@ -212,14 +224,20 @@ impl<'w> BundleRemover<'w> { if let Some(StorageType::SparseSet) = self.old_archetype.as_ref().get_storage_type(component_id) { - world + let sparse_set = world .storages .sparse_sets .get_mut(component_id) // Set exists because the component existed on the entity - .unwrap() - // If it was already forgotten, it would not be in the set. - .remove(entity); + .unwrap(); + + let maybe_panic = + bevy_utils::catch_unwind_if_available(AssertUnwindSafe(|| { + sparse_set.remove(entity) + })) + .err(); + + panic_payload = panic_payload.or(maybe_panic); } } } @@ -278,6 +296,12 @@ impl<'w> BundleRemover<'w> { } }; + if let Some(payload) = move_result.panic + && panic_payload.is_none() + { + panic_payload = Some(payload); + } + // SAFETY: move_result.new_row is a valid position in new_archetype's table let new_location = unsafe { self.new_archetype @@ -317,7 +341,11 @@ impl<'w> BundleRemover<'w> { .update_existing_location(entity.index(), Some(new_location)); } - (new_location, pre_remove_result) + BundleRemoveResult { + new_location, + data: pre_remove_result, + panic_payload, + } } } diff --git a/crates/bevy_ecs/src/bundle/writer.rs b/crates/bevy_ecs/src/bundle/writer.rs index 7b7d9635d7033..18ed936e53e63 100644 --- a/crates/bevy_ecs/src/bundle/writer.rs +++ b/crates/bevy_ecs/src/bundle/writer.rs @@ -6,7 +6,7 @@ use crate::{ use alloc::vec::Vec; use bevy_ptr::OwningPtr; use bumpalo::Bump; -use core::{alloc::Layout, ptr::NonNull}; +use core::{alloc::Layout, panic::AssertUnwindSafe, ptr::NonNull}; /// Enables pushing components to internal scratch space (uses a bump allocator), which can then be /// written as a dynamic bundle. The contents are cleared after each write and the allocated scratch @@ -123,6 +123,9 @@ impl<'a> BundleWriter<'a> { /// Writes the current contents of the bundle to the given `entity` and clears the scratch space. /// + /// # Panics + /// Panics if any of the overwritten components panic while being dropped. + /// /// # Safety /// /// `entity` must be from the same world that all [`Self::push_component`] calls since the last @@ -132,17 +135,20 @@ impl<'a> BundleWriter<'a> { // - All `component_ids` are from the same world as `entity` // - All `component_data_ptrs` are valid types represented by `component_ids` unsafe { - entity.insert_by_ids_internal( - &self.0.component_ids, - self.0 - .component_ptrs - .drain(..) - .map(|ptr| OwningPtr::new(ptr)), - RelationshipHookMode::Run, - ); + let maybe_panic = bevy_utils::catch_unwind_if_available(AssertUnwindSafe(|| { + entity.insert_by_ids_internal( + &self.0.component_ids, + self.0 + .component_ptrs + .drain(..) + .map(|ptr| OwningPtr::new(ptr)), + RelationshipHookMode::Run, + ); + })); + self.0.component_ids.clear(); + self.0.alloc.reset(); + bevy_utils::resume_caught_unwind(maybe_panic.err()); } - self.0.component_ids.clear(); - self.0.alloc.reset(); } /// Returns true if there are currently no components. diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 6be0a4dd13b6b..b75fc85b1cf52 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -131,7 +131,7 @@ impl SystemExecutor for SingleThreadedExecutor { continue; } - let f = AssertUnwindSafe(|| { + let maybe_panic = bevy_utils::catch_unwind_if_available(AssertUnwindSafe(|| { if let Err(RunSystemError::Failed(err)) = __rust_begin_short_backtrace::run_without_applying_deferred(system, world) { @@ -143,21 +143,15 @@ impl SystemExecutor for SingleThreadedExecutor { }, ); } - }); + })) + .err(); #[cfg(feature = "std")] #[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")] - { - if let Err(payload) = std::panic::catch_unwind(f) { - eprintln!("Encountered a panic in system `{}`!", system.name()); - std::panic::resume_unwind(payload); - } - } - - #[cfg(not(feature = "std"))] - { - (f)(); + if maybe_panic.is_some() { + eprintln!("Encountered a panic in system `{}`!", system.name()); } + bevy_utils::resume_caught_unwind(maybe_panic); self.unapplied_systems.insert(system_index); } diff --git a/crates/bevy_ecs/src/storage/blob_array.rs b/crates/bevy_ecs/src/storage/blob_array.rs index 03182b4ddcb12..95749fa3c1dd9 100644 --- a/crates/bevy_ecs/src/storage/blob_array.rs +++ b/crates/bevy_ecs/src/storage/blob_array.rs @@ -179,6 +179,9 @@ impl BlobArray { /// /// Note that this method will behave exactly the same as [`Vec::clear`]. /// + /// # Panics + /// Panics if the stored drop function panics. + /// /// # Safety /// - For every element with index `i`, if `i` < `len`: It must be safe to call [`Self::get_unchecked_mut`] with `i`. /// (If the safety requirements of every method that has been used on `Self` have been fulfilled, the caller just needs to ensure that `len` is correct.) @@ -188,9 +191,6 @@ impl BlobArray { #[cfg(debug_assertions)] debug_assert!(self.capacity >= len); if let Some(drop) = self.drop { - // We set `self.drop` to `None` before dropping elements for unwind safety. This ensures we don't - // accidentally drop elements twice in the event of a drop impl panicking. - self.drop = None; let size = self.item_layout.size(); for i in 0..len { // SAFETY: @@ -202,13 +202,15 @@ impl BlobArray { // SAFETY: `item` was obtained from this `BlobArray`, so its underlying type must match `drop`. unsafe { drop(item) }; } - self.drop = Some(drop); } } /// Because this method needs parameters, it can't be the implementation of the `Drop` trait. /// The owner of this [`BlobArray`] must call this method with the correct information. /// + /// # Panics + /// Panics if the stored drop function panics. + /// /// # Safety /// - `cap` and `len` are indeed the capacity and length of this [`BlobArray`] /// - This [`BlobArray`] mustn't be used after calling this method. @@ -231,6 +233,9 @@ impl BlobArray { /// Drops the last element in this [`BlobArray`]. /// + /// # Panics + /// Panics if the stored drop function panics. + /// /// # Safety // - `last_element_index` must correspond to the last element in the array. // - After this method is called, the last element must not be used @@ -239,14 +244,12 @@ impl BlobArray { #[cfg(debug_assertions)] debug_assert!(self.capacity > last_element_index); if let Some(drop) = self.drop { - // We set `self.drop` to `None` before dropping elements for unwind safety. This ensures we don't - // accidentally drop elements twice in the event of a drop impl panicking. - self.drop = None; - // SAFETY: - let item = self.get_unchecked_mut(last_element_index).promote(); // SAFETY: + // - index in bounds per precondition + // - element is being removed + let item = unsafe { self.get_unchecked_mut(last_element_index).promote() }; + // SAFETY: Drop function belongs to this component unsafe { drop(item) }; - self.drop = Some(drop); } } @@ -335,6 +338,9 @@ impl BlobArray { /// Replaces the value at `index` with `value`. This function does not do any bounds checking. /// + /// # Panics + /// Panics if the drop function panics. + /// /// # Safety /// - Index must be in-bounds (`index` < `len`) /// - `value`'s [`Layout`] must match this [`BlobArray`]'s `item_layout`, @@ -348,47 +354,35 @@ impl BlobArray { let destination = NonNull::from(unsafe { self.get_unchecked_mut(index) }); let source = value.as_ptr(); - if let Some(drop) = self.drop { - // We set `self.drop` to `None` before dropping elements for unwind safety. This ensures we don't - // accidentally drop elements twice in the event of a drop impl panicking. - self.drop = None; + let _finally = OnDrop::new(|| { + // Copy the new value into the vector, overwriting the previous value. + // This needs tp happen even if the drop function panics. + // SAFETY: + // - `source` and `destination` were obtained from `OwningPtr`s, which ensures they are + // valid for both reads and writes. + // - The value behind `source` will only be dropped if the above branch panics, + // so it must still be initialized and it is safe to transfer ownership into the vector. + // - `source` and `destination` were obtained from different memory locations, + // both of which we have exclusive access to, so they are guaranteed not to overlap. + unsafe { + core::ptr::copy_nonoverlapping::( + source, + destination.as_ptr(), + self.item_layout.size(), + ); + } + }); + if let Some(drop) = self.drop { // Transfer ownership of the old value out of the vector, so it can be dropped. // SAFETY: // - `destination` was obtained from a `PtrMut` in this vector, which ensures it is non-null, // well-aligned for the underlying type, and has proper provenance. // - The storage location will get overwritten with `value` later, which ensures // that the element will not get observed or double dropped later. - // - If a panic occurs, `self.len` will remain `0`, which ensures a double-drop - // does not occur. Instead, all elements will be forgotten. let old_value = unsafe { OwningPtr::new(destination) }; - // This closure will run in case `drop()` panics, - // which ensures that `value` does not get forgotten. - let on_unwind = OnDrop::new(|| drop(value)); - drop(old_value); - - // If the above code does not panic, make sure that `value` doesn't get dropped. - core::mem::forget(on_unwind); - - self.drop = Some(drop); - } - - // Copy the new value into the vector, overwriting the previous value. - // SAFETY: - // - `source` and `destination` were obtained from `OwningPtr`s, which ensures they are - // valid for both reads and writes. - // - The value behind `source` will only be dropped if the above branch panics, - // so it must still be initialized and it is safe to transfer ownership into the vector. - // - `source` and `destination` were obtained from different memory locations, - // both of which we have exclusive access to, so they are guaranteed not to overlap. - unsafe { - core::ptr::copy_nonoverlapping::( - source, - destination.as_ptr(), - self.item_layout.size(), - ); } } @@ -455,6 +449,9 @@ impl BlobArray { /// This method will call [`Self::swap_remove_unchecked`] and drop the result. /// + /// # Panics + /// Panics if the stored drop function panics. + /// /// # Safety /// - `index_to_keep` must be safe to access (within the bounds of the length of the array). /// - `index_to_remove` must be safe to access (within the bounds of the length of the array). @@ -481,6 +478,9 @@ impl BlobArray { /// The same as [`Self::swap_remove_and_drop_unchecked`] but the two elements must non-overlapping. /// + /// # Panics + /// Panics if the stored drop function panics. The data will still have been successfully swapped. + /// /// # Safety /// - `index_to_keep` must be safe to access (within the bounds of the length of the array). /// - `index_to_remove` must be safe to access (within the bounds of the length of the array). diff --git a/crates/bevy_ecs/src/storage/non_send.rs b/crates/bevy_ecs/src/storage/non_send.rs index 436508d321b57..70a5aa22d8846 100644 --- a/crates/bevy_ecs/src/storage/non_send.rs +++ b/crates/bevy_ecs/src/storage/non_send.rs @@ -220,14 +220,16 @@ impl NonSendData { /// Removes a value from the data, if present, and drops it. /// /// # Panics - /// This will panic if a value is present and is not accessed from the original thread it was inserted in. + /// This will panic if a value is present and is not accessed from the original thread it was inserted in, + /// or if the drop function panics. #[inline] pub(crate) fn remove_and_drop(&mut self) { if self.is_present() { self.validate_access(); + // Mark as empty before dropping to prevent double drop in case of panic + self.is_present = false; // SAFETY: There is only one element, and it's always allocated. unsafe { self.data.drop_last_element(Self::ROW) }; - self.is_present = false; } } diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index 13460e2defd1e..58f93a4dffc58 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -180,11 +180,17 @@ impl ComponentSparseSet { } /// Removes all of the values stored within. + /// + /// # Panics + /// Panics if the component drop function panics. pub(crate) fn clear(&mut self) { - // SAFETY: This is using the size of the ComponentSparseSet. - unsafe { self.dense.clear(self.len()) }; + let len = self.len(); self.entities.clear(); self.sparse.clear(); + // In case dropping a component panics, we have already marked this set as empty. + // This means we skip dropping the remaining components, but do not drop any twice. + // SAFETY: This is using the size of the ComponentSparseSet. + unsafe { self.dense.clear(len) }; } /// Returns the number of component values in the sparse set. @@ -202,6 +208,9 @@ impl ComponentSparseSet { /// Inserts the `entity` key and component `value` pair into this sparse /// set. /// + /// # Panics + /// Panics if a value was already present and the drop function panics. + /// /// # Aborts /// - Aborts the process if the insertion forces a reallocation, and any of the new capacity overflows `isize::MAX` bytes. /// - Aborts the process if the insertion forces a reallocation, and any of the new the reallocations causes an out-of-memory error. @@ -417,6 +426,9 @@ impl ComponentSparseSet { /// Removes (and drops) the entity's component value from the sparse set. /// + /// # Panics + /// Panics if the component's drop function panics. + /// /// Returns `true` if `entity` had a component value in the sparse set. pub(crate) fn remove(&mut self, entity: Entity) -> bool { self.sparse diff --git a/crates/bevy_ecs/src/storage/table/column.rs b/crates/bevy_ecs/src/storage/table/column.rs index 98727e862400f..8478dea9f8839 100644 --- a/crates/bevy_ecs/src/storage/table/column.rs +++ b/crates/bevy_ecs/src/storage/table/column.rs @@ -47,6 +47,9 @@ impl Column { /// Swap-remove and drop the removed element, but the component at `row` must not be the last element. /// + /// # Panics + /// Panics if the component's drop function panics. The data will still have been successfully swapped. + /// /// # Safety /// - `row.as_usize()` < `len` /// - `last_element_index` = `len - 1` @@ -57,8 +60,6 @@ impl Column { last_element_index: usize, row: TableRow, ) { - self.data - .swap_remove_and_drop_unchecked_nonoverlapping(row.index(), last_element_index); self.added_ticks .swap_remove_unchecked_nonoverlapping(row.index(), last_element_index); self.changed_ticks @@ -66,6 +67,8 @@ impl Column { self.changed_by.as_mut().map(|changed_by| { changed_by.swap_remove_unchecked_nonoverlapping(row.index(), last_element_index); }); + self.data + .swap_remove_and_drop_unchecked_nonoverlapping(row.index(), last_element_index); } /// Swap-remove the provided row. @@ -74,6 +77,9 @@ impl Column { /// /// If `DROP` is `false`, the removed element will be forgotten. /// + /// # Panics + /// Panics if the component drop function panics. + /// /// # Safety /// - `last_element_index` must be the index of the last element. /// - `row.index()` <= `last_element_index` @@ -83,6 +89,13 @@ impl Column { last_element_index: usize, row: TableRow, ) { + self.added_ticks + .swap_remove_unchecked(row.index(), last_element_index); + self.changed_ticks + .swap_remove_unchecked(row.index(), last_element_index); + self.changed_by.as_mut().map(|changed_by| { + changed_by.swap_remove_unchecked(row.index(), last_element_index); + }); if DROP { self.data .swap_remove_and_drop_unchecked(row.index(), last_element_index); @@ -91,13 +104,6 @@ impl Column { .data .swap_remove_unchecked(row.index(), last_element_index); } - self.added_ticks - .swap_remove_unchecked(row.index(), last_element_index); - self.changed_ticks - .swap_remove_unchecked(row.index(), last_element_index); - self.changed_by.as_mut().map(|changed_by| { - changed_by.swap_remove_unchecked(row.index(), last_element_index); - }); } /// Swap-remove and forgets the removed element, but the component at `row` must not be the last element. @@ -188,6 +194,9 @@ impl Column { /// Overwrites component data to the column at given row. The previous value is dropped. /// + /// # Panics + /// Panics if the drop function panics. + /// /// # Safety /// - There must be a valid initialized value stored at `row`. /// - `row.as_usize()` must be in bounds. @@ -200,12 +209,12 @@ impl Column { change_tick: Tick, caller: MaybeLocation, ) { - self.data.replace_unchecked(row.index(), data); *self.changed_ticks.get_unchecked_mut(row.index()).get_mut() = change_tick; self.changed_by .as_mut() .map(|changed_by| changed_by.get_unchecked_mut(row.index()).get_mut()) .assign(caller); + self.data.replace_unchecked(row.index(), data); } /// Removes the element from `other` at `src_row` and inserts it @@ -277,6 +286,9 @@ impl Column { /// Clear all the components from this column. /// + /// # Panics + /// Panics if the component's drop function panics. + /// /// # Safety /// - `len` must match the actual length of the column /// - The caller must not use the elements this column's data until [`initializing`](Self::initialize) it again (set `len` to 0). @@ -292,6 +304,9 @@ impl Column { /// Because this method needs parameters, it can't be the implementation of the `Drop` trait. /// The owner of this [`Column`] must call this method with the correct information. /// + /// # Panics + /// Panics if the component's drop function panics. + /// /// # Safety /// - `len` is indeed the length of the column /// - `cap` is indeed the capacity of the column @@ -299,14 +314,17 @@ impl Column { pub(crate) unsafe fn drop(&mut self, cap: usize, len: usize) { self.added_ticks.drop(cap, len); self.changed_ticks.drop(cap, len); - self.data.drop(cap, len); self.changed_by .as_mut() .map(|changed_by| changed_by.drop(cap, len)); + self.data.drop(cap, len); } /// Drops the last component in this column. /// + /// # Panics + /// Panics if the component's drop function panics. + /// /// # Safety /// - `last_element_index` is indeed the index of the last element /// - the data stored in `last_element_index` will never be used unless properly initialized again. diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index 9b3a0c86c097d..6b3cf40a8ffcb 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -10,9 +10,11 @@ use bevy_platform::collections::HashMap; use bevy_ptr::{OwningPtr, Ptr, UnsafeCellDeref}; pub use column::*; use core::{ + any::Any, cell::UnsafeCell, num::NonZeroUsize, ops::{Index, IndexMut}, + panic::AssertUnwindSafe, panic::Location, }; use nonmax::NonMaxU32; @@ -219,13 +221,24 @@ impl Table { } /// Removes the entity at the given row and returns the entity swapped in to replace it (if an - /// entity was swapped in) + /// entity was swapped in), as well as a panic payload if a component drop panicked. + /// + /// # Panics + /// Panics if the component's drop function panics. /// /// # Safety /// `row` must be in-bounds (`row.as_usize()` < `self.len()`) - pub(crate) unsafe fn swap_remove_unchecked(&mut self, row: TableRow) -> Option { + pub(crate) unsafe fn swap_remove_unchecked( + &mut self, + row: TableRow, + ) -> (Option, Option>) { debug_assert!(row.index_u32() < self.entity_count()); - let last_element_index = self.entity_count() - 1; + self.entities.swap_remove(row.index()); + let last_element_index = self.entity_count(); + let is_last = row.index_u32() == last_element_index; + + let mut panic = None; + if row.index_u32() != last_element_index { // Instead of checking this condition on every `swap_remove` call, we // check it here and use `swap_remove_nonoverlapping`. @@ -235,28 +248,35 @@ impl Table { // - `last_element_index` = `len` - 1 // - `row` != `last_element_index` // - the `len` is kept within `self.entities`, it will update accordingly. - unsafe { - col.swap_remove_and_drop_unchecked_nonoverlapping( - last_element_index as usize, - row, - ); - }; + let maybe_panic = + bevy_utils::catch_unwind_if_available(AssertUnwindSafe(|| unsafe { + col.swap_remove_and_drop_unchecked_nonoverlapping( + last_element_index as usize, + row, + ); + })); + panic = panic.or(maybe_panic.err()); } } else { - // If `row.as_usize()` == `last_element_index` than there's no point in removing the component + // If `row.as_usize()` == `last_element_index` then there's no point in removing the component // at `row`, but we still need to drop it. for col in self.columns.values_mut() { - col.drop_last_component(last_element_index as usize); + // SAFETY: + // this was the last element, and is being removed + let maybe_panic = + bevy_utils::catch_unwind_if_available(AssertUnwindSafe(|| unsafe { + col.drop_last_component(last_element_index as usize); + })); + panic = panic.or(maybe_panic.err()); } } - let is_last = row.index_u32() == last_element_index; - self.entities.swap_remove(row.index()); - if is_last { + let swapped = if is_last { None } else { // SAFETY: This was swap removed and was not last, so it must be in bounds. unsafe { Some(*self.entities.get_unchecked(row.index())) } - } + }; + (swapped, panic) } /// Get the data of the column matching `component_id` as a slice. @@ -635,6 +655,8 @@ pub(crate) struct TableMoveResult<'a> { pub swapped_entity: Option, pub new_table: &'a mut Table, pub new_row: TableRow, + /// Records if a component drop function has panicked + pub panic: Option>, } impl Tables { @@ -772,6 +794,8 @@ impl Tables { let mut dst_iter = dst_table.columns.iter_mut().peekable(); + let mut panic = None; + for (src_component_id, src_column) in src_table.columns.iter_mut() { // Skip past any destination columns that don't exist in the source table. // The caller is responsible for initializing those columns. @@ -796,14 +820,16 @@ impl Tables { dst_column.initialize_from_unchecked(src_column, last_index, row, dst_row); } } else { - // SAFETY: - // - `last_index` is the index of the last element. - // - The caller ensures `row` <= `last_index`. - // - The length of `src_column` is given by the length of `src_table.entities`, - // which has been updated. - unsafe { - src_column.swap_remove_unchecked::(last_index, row); - } + let maybe_panic = bevy_utils::catch_unwind_if_available(AssertUnwindSafe(|| + // SAFETY: + // - `last_index` is the index of the last element. + // - The caller ensures `row` <= `last_index`. + // - The length of `src_column` is given by the length of `src_table.entities`, + // which has been updated. + unsafe { + src_column.swap_remove_unchecked::(last_index, row); + })); + panic = panic.or(maybe_panic.err()); } } @@ -819,6 +845,7 @@ impl Tables { // SAFETY: This was swap-removed and was not last, so it must be in-bounds. unsafe { Some(*src_table.entities.get_unchecked(row.index())) } }, + panic, } } } diff --git a/crates/bevy_ecs/src/world/command_queue.rs b/crates/bevy_ecs/src/world/command_queue.rs index 55942fe36a2fc..ed80d31ff1aec 100644 --- a/crates/bevy_ecs/src/world/command_queue.rs +++ b/crates/bevy_ecs/src/world/command_queue.rs @@ -265,7 +265,8 @@ impl RawCommandQueue { self.bytes.as_mut().as_mut_ptr().add(local_cursor).cast(), )) }; - let f = AssertUnwindSafe(|| { + + let maybe_panic = bevy_utils::catch_unwind_if_available(AssertUnwindSafe(|| { // SAFETY: The data underneath the cursor must correspond to the type erased in metadata, // since they were stored next to each other by `.push()`. // For ZSTs, the type doesn't matter as long as the pointer is non-null. @@ -273,41 +274,33 @@ impl RawCommandQueue { // At this point, it will either point to the next `CommandMeta`, // or the cursor will be out of bounds and the loop will end. unsafe { (meta.consume_command_and_get_size)(cmd, world, &mut local_cursor) }; - }); - - #[cfg(feature = "std")] - { - let result = std::panic::catch_unwind(f); - - if let Err(payload) = result { - // local_cursor now points to the location _after_ the panicked command. - // Add the remaining commands that _would have_ been applied to the - // panic_recovery queue. - // - // This uses `current_stop` instead of `stop` to account for any commands - // that were queued _during_ this panic. - // - // This is implemented in such a way that if apply_or_drop_queued() are nested recursively in, - // an applied Command, the correct command order will be retained. - let panic_recovery = self.panic_recovery.as_mut(); - let bytes = self.bytes.as_mut(); - let current_stop = bytes.len(); - panic_recovery.extend_from_slice(&bytes[local_cursor..current_stop]); - bytes.set_len(start); - *self.cursor.as_mut() = start; - - // This was the "top of the apply stack". If we are _not_ at the top of the apply stack, - // when we call`resume_unwind" the caller "closer to the top" will catch the unwind and do this check, - // until we reach the top. - if start == 0 { - bytes.append(panic_recovery); - } - std::panic::resume_unwind(payload); + })); + + if maybe_panic.is_err() { + // local_cursor now points to the location _after_ the panicked command. + // Add the remaining commands that _would have_ been applied to the + // panic_recovery queue. + // + // This uses `current_stop` instead of `stop` to account for any commands + // that were queued _during_ this panic. + // + // This is implemented in such a way that if apply_or_drop_queued() are nested recursively in, + // an applied Command, the correct command order will be retained. + let panic_recovery = self.panic_recovery.as_mut(); + let bytes = self.bytes.as_mut(); + let current_stop = bytes.len(); + panic_recovery.extend_from_slice(&bytes[local_cursor..current_stop]); + bytes.set_len(start); + *self.cursor.as_mut() = start; + + // This was the "top of the apply stack". If we are _not_ at the top of the apply stack, + // when we call`resume_unwind" the caller "closer to the top" will catch the unwind and do this check, + // until we reach the top. + if start == 0 { + bytes.append(panic_recovery); } + bevy_utils::resume_caught_unwind(maybe_panic.err()); } - - #[cfg(not(feature = "std"))] - (f)(); } // Reset the buffer: all commands past the original `start` cursor have been applied. diff --git a/crates/bevy_ecs/src/world/entity_access/world_mut.rs b/crates/bevy_ecs/src/world/entity_access/world_mut.rs index 658cc161482da..5e2040f45daaa 100644 --- a/crates/bevy_ecs/src/world/entity_access/world_mut.rs +++ b/crates/bevy_ecs/src/world/entity_access/world_mut.rs @@ -25,9 +25,15 @@ use crate::{ }, }; +use alloc::boxed::Box; use alloc::vec::Vec; use bevy_ptr::{move_as_ptr, MovingPtr, OwningPtr}; -use core::{any::TypeId, marker::PhantomData, mem::MaybeUninit}; +use core::{ + any::{Any, TypeId}, + marker::PhantomData, + mem::MaybeUninit, + panic::AssertUnwindSafe, +}; /// A mutable reference to a particular [`Entity`], and the entire world. /// @@ -1082,7 +1088,7 @@ impl<'w> EntityWorldMut<'w> { // - The value pointed at by `bundle` is not accessed for anything other than `apply_effect` // and the caller ensures that the value is not accessed or dropped after this function // returns. - let (bundle, location) = bundle.partial_move(|bundle| unsafe { + let (bundle, (new_location, panic)) = bundle.partial_move(|bundle| unsafe { bundle_inserter.insert( self.entity, location, @@ -1092,13 +1098,16 @@ impl<'w> EntityWorldMut<'w> { relationship_hook_mode, ) }); - self.location = Some(location); + self.location = Some(new_location); self.world.flush(); self.update_location(); // SAFETY: // - This is called exactly once after the `BundleInsert::insert` call before returning to safe code. // - `bundle` points to the same `B` that `BundleInsert::insert` was called on. unsafe { T::apply_effect(bundle, self) }; + + bevy_utils::resume_caught_unwind(panic); + self } @@ -1159,7 +1168,7 @@ impl<'w> EntityWorldMut<'w> { let bundle_inserter = BundleInserter::new_with_id(self.world, location.archetype_id, bundle_id, change_tick); - self.location = Some(insert_dynamic_bundle( + let (new_location, panic) = insert_dynamic_bundle( bundle_inserter, self.entity, location, @@ -1168,9 +1177,11 @@ impl<'w> EntityWorldMut<'w> { mode, caller, relationship_hook_insert_mode, - )); + ); + self.location = Some(new_location); self.world.flush(); self.update_location(); + bevy_utils::resume_caught_unwind(panic); self } @@ -1199,6 +1210,8 @@ impl<'w> EntityWorldMut<'w> { self.insert_by_ids_internal(component_ids, iter_components, RelationshipHookMode::Run) } + /// # Panics + /// Panics if any of the overwritten components panic while being dropped. #[track_caller] pub(crate) unsafe fn insert_by_ids_internal<'a, I: Iterator>>( &mut self, @@ -1218,7 +1231,7 @@ impl<'w> EntityWorldMut<'w> { let bundle_inserter = BundleInserter::new_with_id(self.world, location.archetype_id, bundle_id, change_tick); - self.location = Some(insert_dynamic_bundle( + let (new_location, panic) = insert_dynamic_bundle( bundle_inserter, self.entity, location, @@ -1227,10 +1240,13 @@ impl<'w> EntityWorldMut<'w> { InsertMode::Replace, MaybeLocation::caller(), relationship_hook_insert_mode, - )); + ); + + self.location = Some(new_location); *self.world.bundles.get_storages_unchecked(bundle_id) = core::mem::take(&mut storage_types); self.world.flush(); self.update_location(); + bevy_utils::resume_caught_unwind(panic); self } @@ -1254,7 +1270,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: // - The passed location has the same archetype as the remover, since they came from the same location. // - `location` was obtained from a valid `Self`. - let (new_location, result) = unsafe { + let result = unsafe { remover.remove( entity, location, @@ -1287,11 +1303,11 @@ impl<'w> EntityWorldMut<'w> { }, ) }; - self.location = Some(new_location); + self.location = Some(result.new_location); self.world.flush(); self.update_location(); - Some(result) + Some(result.data) } /// Removes any components in the [`Bundle`] from the entity. @@ -1319,19 +1335,21 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: // - The remover archetype came from the passed location and the removal can not fail. // - `location` was obtained from a valid `Self`. - let new_location = unsafe { + let result = unsafe { remover.remove( self.entity, location, caller, BundleRemover::empty_pre_remove, ) - } - .0; + }; - self.location = Some(new_location); + self.location = Some(result.new_location); self.world.flush(); self.update_location(); + + bevy_utils::resume_caught_unwind(result.panic_payload); + self } @@ -1361,19 +1379,21 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: // - The remover archetype came from the passed location and the removal can not fail. // - `location` was obtained from a valid `Self`. - let new_location = unsafe { + let result = unsafe { remover.remove( self.entity, location, caller, BundleRemover::empty_pre_remove, ) - } - .0; + }; - self.location = Some(new_location); + self.location = Some(result.new_location); self.world.flush(); self.update_location(); + + bevy_utils::resume_caught_unwind(result.panic_payload); + self } @@ -1419,19 +1439,21 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: // - The remover archetype came from the passed location and the removal can not fail. // - `old_location` was obtained from a valid `Self`. - let new_location = unsafe { + let result = unsafe { remover.remove( self.entity, old_location, caller, BundleRemover::empty_pre_remove, ) - } - .0; + }; - self.location = Some(new_location); + self.location = Some(result.new_location); self.world.flush(); self.update_location(); + + bevy_utils::resume_caught_unwind(result.panic_payload); + self } @@ -1472,19 +1494,21 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: // - The remover archetype came from the passed location and the removal can not fail. // - `location` was obtained from a valid `Self`. - let new_location = unsafe { + let result = unsafe { remover.remove( self.entity, location, caller, BundleRemover::empty_pre_remove, ) - } - .0; + }; - self.location = Some(new_location); + self.location = Some(result.new_location); self.world.flush(); self.update_location(); + + bevy_utils::resume_caught_unwind(result.panic_payload); + self } @@ -1538,11 +1562,14 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: // - The remover archetype came from the passed location and the removal can not fail. // - `location` was obtained from a valid `Self`. - let new_location = unsafe { remover.remove(self.entity, location, caller, pre_remove) }.0; + let result = unsafe { remover.remove(self.entity, location, caller, pre_remove) }; - self.location = Some(new_location); + self.location = Some(result.new_location); self.world.flush(); self.update_location(); + + bevy_utils::resume_caught_unwind(result.panic_payload); + self } @@ -1578,19 +1605,21 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: // - The remover archetype came from the passed location and the removal can not fail. // - `location` was obtained from a valid `Self`. - let new_location = unsafe { + let result = unsafe { remover.remove( self.entity, location, caller, BundleRemover::empty_pre_remove, ) - } - .0; + }; - self.location = Some(new_location); + self.location = Some(result.new_location); self.world.flush(); self.update_location(); + + bevy_utils::resume_caught_unwind(result.panic_payload); + self } @@ -1627,6 +1656,9 @@ impl<'w> EntityWorldMut<'w> { } /// This despawns this entity if it is currently spawned, storing the new [`EntityGeneration`](crate::entity::EntityGeneration) in [`Self::entity`] but not freeing it. + /// + /// # Panics + /// Panics if any of the dropped components panic while dropping pub(crate) fn despawn_no_free_with_caller(&mut self, caller: MaybeLocation) { // setup let Some(location) = self.location else { @@ -1729,9 +1761,14 @@ impl<'w> EntityWorldMut<'w> { .mark_spawned_or_despawned(self.entity.index(), caller, change_tick); } + // If one of the hooks/observers above panicked, the world is in a safe state because + // the despawn is effectively cancelled. + // Now that we're editing metadata and moving data around in tables, we need to make + // sure that we complete these changes even if dropping a component panicks. + let mut panic = None; + let table_row; - let moved_entity; - { + let moved_entity = { let archetype = &mut self.world.archetypes[location.archetype_id]; let remove_result = archetype.swap_remove(location.archetype_row); if let Some(swapped_entity) = remove_result.swapped_entity { @@ -1760,12 +1797,20 @@ impl<'w> EntityWorldMut<'w> { .sparse_sets .get_mut(component_id) .unwrap(); - sparse_set.remove(self.entity); + let maybe_panic = bevy_utils::catch_unwind_if_available(AssertUnwindSafe(|| { + sparse_set.remove(self.entity) + })) + .err(); + panic = panic.or(maybe_panic); } + // SAFETY: table rows stored in archetypes always exist - moved_entity = unsafe { + let (moved_entity, maybe_panic) = unsafe { self.world.storages.tables[archetype.table_id()].swap_remove_unchecked(table_row) }; + panic = panic.or(maybe_panic); + + moved_entity }; // Handle displaced entity @@ -1792,6 +1837,8 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: We just despawned it. self.entity = unsafe { self.world.entities.mark_free(self.entity.index(), 1) }; self.world.flush(); + + bevy_utils::resume_caught_unwind(panic); } /// Despawns the current entity. @@ -2358,6 +2405,9 @@ impl<'a> From<&'a mut EntityWorldMut<'_>> for FilteredEntityMut<'a, 'static> { /// Inserts a dynamic [`Bundle`] into the entity. /// +/// # Panics +/// Panics if any of the overwritten components panic while being dropped. +/// /// # Safety /// /// - [`OwningPtr`] and [`StorageType`] iterators must correspond to the @@ -2376,7 +2426,7 @@ unsafe fn insert_dynamic_bundle< mode: InsertMode, caller: MaybeLocation, relationship_hook_insert_mode: RelationshipHookMode, -) -> EntityLocation { +) -> (EntityLocation, Option>) { struct DynamicInsertBundle<'a, I: Iterator)>> { components: I, } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 89ece4a89b1c9..e37e7807ba72f 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -2504,7 +2504,7 @@ impl World { }; move_as_ptr!(first_bundle); // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter - unsafe { + let (_, panic) = unsafe { cache.inserter.insert( first_entity, first_location, @@ -2514,6 +2514,7 @@ impl World { RelationshipHookMode::Run, ) }; + bevy_utils::resume_caught_unwind(panic); for (entity, bundle) in batch_iter { match cache.inserter.entities().get_spawned(entity) { @@ -2534,7 +2535,7 @@ impl World { } move_as_ptr!(bundle); // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter - unsafe { + let (_, panic) = unsafe { cache.inserter.insert( entity, location, @@ -2544,6 +2545,7 @@ impl World { RelationshipHookMode::Run, ) }; + bevy_utils::resume_caught_unwind(panic); } Err(err) => { panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {entity} because: {err}. See: https://bevyengine.org/learn/errors/b0003", core::any::type_name::()); @@ -2656,7 +2658,7 @@ impl World { // - `entity` is valid, `location` matches entity, bundle matches inserter // - `apply_effect` is never called on this bundle. // - `first_bundle` is not be accessed or dropped after this. - unsafe { + let (_, panic) = unsafe { cache.inserter.insert( first_entity, first_location, @@ -2666,6 +2668,8 @@ impl World { RelationshipHookMode::Run, ) }; + bevy_utils::resume_caught_unwind(panic); + break Some(cache); } invalid_entities.push(first_entity); @@ -2698,7 +2702,7 @@ impl World { // - `entity` is valid, `location` matches entity, bundle matches inserter // - `apply_effect` is never called on this bundle. // - `bundle` is not be accessed or dropped after this. - unsafe { + let (_, panic) = unsafe { cache.inserter.insert( entity, location, @@ -2708,6 +2712,7 @@ impl World { RelationshipHookMode::Run, ) }; + bevy_utils::resume_caught_unwind(panic); } else { invalid_entities.push(entity); } @@ -2871,7 +2876,7 @@ impl World { // - The value pointed at by `bundle` is not accessed for anything other than `apply_effect` // and the caller ensures that the value is not accessed or dropped after this function // returns. - let (bundle, _) = value.partial_move(|bundle| unsafe { + let (bundle, (_, panic)) = value.partial_move(|bundle| unsafe { bundle_inserter.insert( self.entity, location, @@ -2891,6 +2896,8 @@ impl World { // - This is called exactly once after the `BundleInsert::insert` call before returning to safe code. // - `bundle` points to the same `B` that `BundleInsert::insert` was called on. unsafe { R::apply_effect(bundle, &mut entity_mut) }; + + bevy_utils::resume_caught_unwind(panic); } } } @@ -3942,7 +3949,7 @@ mod tests { use bevy_utils::prelude::DebugName; use core::{ any::TypeId, - panic, + panic::AssertUnwindSafe, sync::atomic::{AtomicBool, AtomicU32, Ordering}, }; use std::{println, sync::Mutex}; @@ -4061,6 +4068,67 @@ mod tests { ); } + #[test] + fn panic_in_component_drop() { + let helper = DropTestHelper::new(); + + let mut world = World::new(); + + let e1 = world.spawn(helper.make_component(true, 0)).id(); + let e2 = world.spawn(helper.make_component(true, 1)).id(); + + // Panic despawning component not last in table + std::panic::catch_unwind(AssertUnwindSafe(|| { + world.despawn(e1); + })) + .unwrap_err(); + + // Panic despawning component last in table + std::panic::catch_unwind(AssertUnwindSafe(|| { + world.despawn(e2); + })) + .unwrap_err(); + + let mut e3 = world.spawn(helper.make_component(true, 2)); + let e3_id = e3.id(); + let old_loc = e3.location(); + // Drop old component through override + std::panic::catch_unwind(AssertUnwindSafe(|| { + e3.insert((helper.make_component(true, 3), Foo)); + })) + .unwrap_err(); + assert_ne!(e3.location(), old_loc); + assert_eq!(e3.location(), world.entity(e3_id).location()); + std::panic::catch_unwind(AssertUnwindSafe(|| { + world.entity_mut(e3_id).remove::(); + })) + .unwrap_err(); + + world.spawn(helper.make_component(true, 4)); + // Panic during world drop + let res = std::panic::catch_unwind(AssertUnwindSafe(|| { + drop(world); + })); + + let drop_log = helper.finish(res); + + assert_eq!( + &*drop_log, + [ + DropLogItem::Create(0), + DropLogItem::Create(1), + DropLogItem::Drop(0), + DropLogItem::Drop(1), + DropLogItem::Create(2), + DropLogItem::Create(3), + DropLogItem::Drop(2), + DropLogItem::Drop(3), + DropLogItem::Create(4), + DropLogItem::Drop(4), + ] + ); + } + #[derive(Resource)] struct TestResource(u32); diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index 5d8af3ecc335c..bfec1422e69b2 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -70,7 +70,8 @@ pub use once::OnceFlag; pub use debug_info::DebugName; pub use default::default; -use core::mem::ManuallyDrop; +use alloc::boxed::Box; +use core::{any::Any, mem::ManuallyDrop, panic::UnwindSafe}; /// A type which calls a function when dropped. /// This can be used to ensure that cleanup code is run even in case of a panic. @@ -130,3 +131,31 @@ impl Drop for OnDrop { callback(); } } + +/// Tries to catch a panic, if `std` is enabled and unwinding on panics is enabled. +pub fn catch_unwind_if_available( + f: impl UnwindSafe + FnOnce() -> T, +) -> Result> { + cfg::switch! { + cfg::std => { + std::panic::catch_unwind(f) + } + _ => { + Ok(f()) + } + } +} + +/// Tries to rethrow a panic, if `std` is enabled and the payload is present. +pub fn resume_caught_unwind(panic: Option>) { + cfg::switch! { + cfg::std => { + if let Some(payload) = panic { + std::panic::resume_unwind(payload) + } + } + _ => { + let _ = panic; + } + } +}