Skip to content
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

[Merged by Bors] - Panic on dropping NonSend in non-origin thread. #6534

Closed
wants to merge 57 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
bbb7e85
Abort on dropping NonSend in non-origin thread.
james7132 Nov 10, 2022
0f4b73d
Fix safety comments
james7132 Nov 10, 2022
e83e8e3
Invert is_send
james7132 Nov 10, 2022
e46ec0c
Factor out drop-abort
james7132 Nov 10, 2022
0898a54
Fix access, replace MainThreadValidator
james7132 Nov 16, 2022
e4d61c1
Update docs
james7132 Nov 16, 2022
944b83c
Switch to always panic, use ManuallyDrop
james7132 Nov 16, 2022
9b712df
Formatting
james7132 Nov 16, 2022
776ffaf
Merge branch 'main' into abort-on-nonsend-drop
james7132 Dec 1, 2022
ba27136
Split out into NonSendResources
james7132 Dec 2, 2022
c3708d4
Cleanup docs and type definitions
james7132 Dec 2, 2022
16ae491
Fix CI
james7132 Dec 2, 2022
cc4c611
Merge branch 'main' into abort-on-nonsend-drop
james7132 Dec 2, 2022
1a09486
Update safety docs.
james7132 Dec 2, 2022
f935aa2
Actually fix CI
james7132 Dec 2, 2022
f4281e6
Small cleanup
james7132 Dec 2, 2022
1692da8
Fix WorldCell's use of NonSend
james7132 Dec 2, 2022
2b11ea3
Fix bevy_window
james7132 Dec 2, 2022
638e95c
Use const generics to split on Send/!Send
james7132 Dec 2, 2022
c37f26e
Use const generics instead of duplicating code
james7132 Dec 2, 2022
ee5de65
Use the right branch
james7132 Dec 2, 2022
d195cb6
Add panic sections to World's API docs
james7132 Dec 3, 2022
93334a0
Apply suggestions from code review
james7132 Dec 3, 2022
8f72292
Remove non_send_scope
james7132 Dec 3, 2022
b3fffd9
Split insert_non_send_by_id from insert_resource_by_id
james7132 Dec 3, 2022
54e5a50
Fix safety comment and tests
james7132 Dec 3, 2022
5f6e91b
Exhaustively decompose Storages in check_change_ticks
james7132 Dec 3, 2022
cfb8173
Remove spurious is_send in docs
james7132 Dec 3, 2022
5900696
Update safety docs on initialize_resource_internal
james7132 Dec 3, 2022
95e575f
Check that Send resources have correct metadata on initialization
james7132 Dec 3, 2022
2bff28c
Formatting
james7132 Dec 3, 2022
6af4c4e
Undo changes to BlobVec, how the hell did this get in here
james7132 Dec 3, 2022
e056626
Fix docs link
james7132 Dec 3, 2022
58bcb66
Apply suggestions from code review
james7132 Dec 4, 2022
9db8041
Stronger assert
james7132 Dec 4, 2022
d713d84
Fix CI
james7132 Dec 4, 2022
314a86e
Apply suggestions from code review
james7132 Dec 5, 2022
e06677a
Small cleanup
james7132 Dec 5, 2022
de05d57
Update crates/bevy_ecs/src/world/mod.rs
james7132 Dec 5, 2022
b4d6303
Merge branch 'main' into abort-on-nonsend-drop
james7132 Dec 6, 2022
a9895c0
Merge branch 'abort-on-nonsend-drop' of github.com:james7132/bevy int…
james7132 Dec 7, 2022
db4c6d7
Apply suggestions from code review
james7132 Dec 8, 2022
7578306
Merge branch 'main' into abort-on-nonsend-drop
james7132 Dec 8, 2022
5e182fc
Move std::thread::panicking check to Drop impl
james7132 Dec 8, 2022
263ed85
Update safety comment about validate_access in Drop
james7132 Dec 8, 2022
e4a0a9a
Add the missing get_non_send_by_id APIs
james7132 Dec 8, 2022
5be8450
Formatting
james7132 Dec 8, 2022
587ea78
Add test for distinct storage and missing panic comments
james7132 Dec 8, 2022
68de55f
Make the safety comments on _by_id APIs more accurate
james7132 Dec 9, 2022
4e8412f
Reword panic docs.
james7132 Dec 12, 2022
ebe33dd
Merge branch 'main' into abort-on-nonsend-drop
james7132 Dec 12, 2022
87c88ab
Fix CI
james7132 Dec 12, 2022
6d39cd5
Merge branch 'main' into abort-on-nonsend-drop
james7132 Dec 19, 2022
8b8a735
Revert changes with NonSendMarker
james7132 Dec 19, 2022
6c8e988
Merge branch 'main' into abort-on-nonsend-drop
james7132 Jan 2, 2023
2c5be6d
Formatting
james7132 Jan 2, 2023
b236f9d
Merge branch 'main' into abort-on-nonsend-drop
james7132 Jan 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 20 additions & 17 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,15 @@ mod tests {
assert_eq!(*world.non_send_resource_mut::<i64>(), 456);
}

#[test]
fn non_send_resource_points_to_distinct_data() {
let mut world = World::default();
world.insert_resource(A(123));
world.insert_non_send_resource(A(456));
assert_eq!(*world.resource::<A>(), A(123));
assert_eq!(*world.non_send_resource::<A>(), A(456));
}

#[test]
#[should_panic]
fn non_send_resource_panic() {
Expand Down Expand Up @@ -1406,38 +1415,32 @@ mod tests {
assert_eq!(world.resource::<A>().0, 1);
}

#[test]
fn non_send_resource_scope() {
let mut world = World::default();
world.insert_non_send_resource(NonSendA::default());
world.resource_scope(|world: &mut World, mut value: Mut<NonSendA>| {
value.0 += 1;
assert!(!world.contains_resource::<NonSendA>());
});
assert_eq!(world.non_send_resource::<NonSendA>().0, 1);
}

#[test]
#[should_panic(
expected = "attempted to access NonSend resource bevy_ecs::tests::NonSendA off of the main thread"
expected = "Attempted to access or drop non-send resource bevy_ecs::tests::NonSendA from thread"
)]
fn non_send_resource_scope_from_different_thread() {
fn non_send_resource_drop_from_different_thread() {
let mut world = World::default();
world.insert_non_send_resource(NonSendA::default());

let thread = std::thread::spawn(move || {
// Accessing the non-send resource on a different thread
// Dropping the non-send resource on a different thread
// Should result in a panic
world.resource_scope(|_: &mut World, mut value: Mut<NonSendA>| {
value.0 += 1;
});
drop(world);
});

if let Err(err) = thread.join() {
std::panic::resume_unwind(err);
}
}

#[test]
fn non_send_resource_drop_from_same_thread() {
let mut world = World::default();
world.insert_non_send_resource(NonSendA::default());
drop(world);
}

#[test]
fn insert_overwrite_drop() {
let (dropck1, dropped1) = DropCk::new_pair();
Expand Down
4 changes: 4 additions & 0 deletions crates/bevy_ecs/src/schedule/ambiguity_detection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ mod tests {
fn read_only() {
let mut world = World::new();
world.insert_resource(R);
world.insert_non_send_resource(R);
world.spawn(A);
world.init_resource::<Events<E>>();

Expand Down Expand Up @@ -394,6 +395,7 @@ mod tests {
fn nonsend() {
let mut world = World::new();
world.insert_resource(R);
world.insert_non_send_resource(R);

let mut test_stage = SystemStage::parallel();
test_stage
Expand Down Expand Up @@ -497,6 +499,7 @@ mod tests {
fn ignore_all_ambiguities() {
let mut world = World::new();
world.insert_resource(R);
world.insert_non_send_resource(R);

let mut test_stage = SystemStage::parallel();
test_stage
Expand All @@ -513,6 +516,7 @@ mod tests {
fn ambiguous_with_label() {
let mut world = World::new();
world.insert_resource(R);
world.insert_non_send_resource(R);

#[derive(SystemLabel)]
struct IgnoreMe;
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_ecs/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ pub use table::*;
pub struct Storages {
pub sparse_sets: SparseSets,
pub tables: Tables,
pub resources: Resources,
pub resources: Resources<true>,
pub non_send_resources: Resources<false>,
}
152 changes: 111 additions & 41 deletions crates/bevy_ecs/src/storage/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,61 @@ use crate::archetype::ArchetypeComponentId;
use crate::component::{ComponentId, ComponentTicks, Components, TickCells};
use crate::storage::{Column, SparseSet, TableRow};
use bevy_ptr::{OwningPtr, Ptr, UnsafeCellDeref};
use std::{mem::ManuallyDrop, thread::ThreadId};

/// The type-erased backing storage and metadata for a single resource within a [`World`].
///
/// If `SEND` is false, values of this type will panic if dropped from a different thread.
///
/// [`World`]: crate::world::World
pub struct ResourceData {
column: Column,
pub struct ResourceData<const SEND: bool> {
column: ManuallyDrop<Column>,
type_name: String,
id: ArchetypeComponentId,
origin_thread_id: Option<ThreadId>,
}

impl<const SEND: bool> Drop for ResourceData<SEND> {
fn drop(&mut self) {
if self.is_present() {
// If this thread is already panicking, panicking again will cause
// the entire process to abort. In this case we choose to avoid
// dropping or checking this altogether and just leak the column.
if std::thread::panicking() {
return;
}
self.validate_access();
}
// SAFETY: Drop is only called once upon dropping the ResourceData
// and is inaccessible after this as the parent ResourceData has
// been dropped. The validate_access call above will check that the
// data is dropped on the thread it was inserted from.
unsafe {
ManuallyDrop::drop(&mut self.column);
}
}
}

impl ResourceData {
impl<const SEND: bool> ResourceData<SEND> {
/// The only row in the underlying column.
const ROW: TableRow = TableRow::new(0);

#[inline]
fn validate_access(&self) {
if SEND {
return;
}
if self.origin_thread_id != Some(std::thread::current().id()) {
// Panic in tests, as testing for aborting is nearly impossible
panic!(
"Attempted to access or drop non-send resource {} from thread {:?} on a thread {:?}. This is not allowed. Aborting.",
self.type_name,
self.origin_thread_id,
std::thread::current().id()
);
}
}

/// Returns true if the resource is populated.
#[inline]
pub fn is_present(&self) -> bool {
Expand All @@ -28,9 +70,16 @@ impl ResourceData {
}

/// Gets a read-only pointer to the underlying resource, if available.
///
/// # Panics
/// If `SEND` is false, this will panic if a value is present and is not accessed from the
/// original thread it was inserted from.
#[inline]
pub fn get_data(&self) -> Option<Ptr<'_>> {
self.column.get_data(Self::ROW)
self.column.get_data(Self::ROW).map(|res| {
self.validate_access();
res
})
}

/// Gets a read-only reference to the change ticks of the underlying resource, if available.
Expand All @@ -39,83 +88,98 @@ impl ResourceData {
self.column.get_ticks(Self::ROW)
}

/// # Panics
/// If `SEND` is false, this will panic if a value is present and is not accessed from the
/// original thread it was inserted in.
#[inline]
pub(crate) fn get_with_ticks(&self) -> Option<(Ptr<'_>, TickCells<'_>)> {
self.column.get(Self::ROW)
self.column.get(Self::ROW).map(|res| {
self.validate_access();
res
})
}

/// Inserts a value into the resource. If a value is already present
/// it will be replaced.
///
/// # Safety
/// `value` must be valid for the underlying type for the resource.
///
/// The underlying type must be [`Send`] or be inserted from the main thread.
/// This can be validated with [`World::validate_non_send_access_untyped`].
/// # Panics
/// If `SEND` is false, this will panic if a value is present and is not replaced from
/// the original thread it was inserted in.
///
/// [`World::validate_non_send_access_untyped`]: crate::world::World::validate_non_send_access_untyped
/// # Safety
/// - `value` must be valid for the underlying type for the resource.
#[inline]
pub(crate) unsafe fn insert(&mut self, value: OwningPtr<'_>, change_tick: u32) {
if self.is_present() {
self.validate_access();
self.column.replace(Self::ROW, value, change_tick);
} else {
if !SEND {
self.origin_thread_id = Some(std::thread::current().id());
}
self.column.push(value, ComponentTicks::new(change_tick));
}
}

/// Inserts a value into the resource with a pre-existing change tick. If a
/// value is already present it will be replaced.
///
/// # Safety
/// `value` must be valid for the underlying type for the resource.
///
/// The underlying type must be [`Send`] or be inserted from the main thread.
/// This can be validated with [`World::validate_non_send_access_untyped`].
/// # Panics
/// If `SEND` is false, this will panic if a value is present and is not replaced from
/// the original thread it was inserted in.
///
/// [`World::validate_non_send_access_untyped`]: crate::world::World::validate_non_send_access_untyped
/// # Safety
/// - `value` must be valid for the underlying type for the resource.
#[inline]
pub(crate) unsafe fn insert_with_ticks(
&mut self,
value: OwningPtr<'_>,
change_ticks: ComponentTicks,
) {
if self.is_present() {
self.validate_access();
self.column.replace_untracked(Self::ROW, value);
*self.column.get_added_ticks_unchecked(Self::ROW).deref_mut() = change_ticks.added;
*self
.column
.get_changed_ticks_unchecked(Self::ROW)
.deref_mut() = change_ticks.changed;
} else {
if !SEND {
self.origin_thread_id = Some(std::thread::current().id());
}
self.column.push(value, change_ticks);
}
}

/// Removes a value from the resource, if present.
///
/// # Safety
/// The underlying type must be [`Send`] or be removed from the main thread.
/// This can be validated with [`World::validate_non_send_access_untyped`].
///
/// The removed value must be used or dropped.
///
/// [`World::validate_non_send_access_untyped`]: crate::world::World::validate_non_send_access_untyped
/// # Panics
/// If `SEND` is false, this will panic if a value is present and is not removed from the
/// original thread it was inserted from.
#[inline]
#[must_use = "The returned pointer to the removed component should be used or dropped"]
pub(crate) unsafe fn remove(&mut self) -> Option<(OwningPtr<'_>, ComponentTicks)> {
self.column.swap_remove_and_forget(Self::ROW)
pub(crate) fn remove(&mut self) -> Option<(OwningPtr<'_>, ComponentTicks)> {
if SEND {
self.column.swap_remove_and_forget(Self::ROW)
} else {
self.is_present()
.then(|| self.validate_access())
.and_then(|_| self.column.swap_remove_and_forget(Self::ROW))
}
}

/// Removes a value from the resource, if present, and drops it.
///
/// # Safety
/// The underlying type must be [`Send`] or be removed from the main thread.
/// This can be validated with [`World::validate_non_send_access_untyped`].
///
/// [`World::validate_non_send_access_untyped`]: crate::world::World::validate_non_send_access_untyped
/// # Panics
/// If `SEND` is false, this will panic if a value is present and is not
/// accessed from the original thread it was inserted in.
#[inline]
pub(crate) unsafe fn remove_and_drop(&mut self) {
self.column.clear();
pub(crate) fn remove_and_drop(&mut self) {
if self.is_present() {
self.validate_access();
self.column.clear();
}
}
}

Expand All @@ -124,11 +188,11 @@ impl ResourceData {
/// [`Resource`]: crate::system::Resource
/// [`World`]: crate::world::World
#[derive(Default)]
pub struct Resources {
resources: SparseSet<ComponentId, ResourceData>,
pub struct Resources<const SEND: bool> {
resources: SparseSet<ComponentId, ResourceData<SEND>>,
}

impl Resources {
impl<const SEND: bool> Resources<SEND> {
/// The total number of resources stored in the [`World`]
///
/// [`World`]: crate::world::World
Expand All @@ -138,7 +202,7 @@ impl Resources {
}

/// Iterate over all resources that have been initialized, i.e. given a [`ComponentId`]
pub fn iter(&self) -> impl Iterator<Item = (ComponentId, &ResourceData)> {
pub fn iter(&self) -> impl Iterator<Item = (ComponentId, &ResourceData<SEND>)> {
self.resources.iter().map(|(id, data)| (*id, data))
}

Expand All @@ -153,31 +217,37 @@ impl Resources {

/// Gets read-only access to a resource, if it exists.
#[inline]
pub fn get(&self, component_id: ComponentId) -> Option<&ResourceData> {
pub fn get(&self, component_id: ComponentId) -> Option<&ResourceData<SEND>> {
self.resources.get(component_id)
}

/// Gets mutable access to a resource, if it exists.
#[inline]
pub(crate) fn get_mut(&mut self, component_id: ComponentId) -> Option<&mut ResourceData> {
pub(crate) fn get_mut(&mut self, component_id: ComponentId) -> Option<&mut ResourceData<SEND>> {
self.resources.get_mut(component_id)
}

/// Fetches or initializes a new resource and returns back it's underlying column.
///
/// # Panics
/// Will panic if `component_id` is not valid for the provided `components`
/// If `SEND` is false, this will panic if `component_id`'s `ComponentInfo` is not registered as being `Send` + `Sync`.
pub(crate) fn initialize_with(
&mut self,
component_id: ComponentId,
components: &Components,
f: impl FnOnce() -> ArchetypeComponentId,
) -> &mut ResourceData {
) -> &mut ResourceData<SEND> {
james7132 marked this conversation as resolved.
Show resolved Hide resolved
self.resources.get_or_insert_with(component_id, || {
let component_info = components.get_info(component_id).unwrap();
if SEND {
assert!(component_info.is_send_and_sync());
}
ResourceData {
column: Column::with_capacity(component_info, 1),
column: ManuallyDrop::new(Column::with_capacity(component_info, 1)),
type_name: String::from(component_info.name()),
id: f(),
origin_thread_id: None,
}
})
}
Expand Down
Loading