Skip to content

Commit 85a918a

Browse files
Improve safety for the multi-threaded executor using UnsafeWorldCell (#8292)
# Objective Fix #7833. Safety comments in the multi-threaded executor don't really talk about system world accesses, which makes it unclear if the code is actually valid. ## Solution Update the `System` trait to use `UnsafeWorldCell`. This type's API is written in a way that makes it much easier to cleanly maintain safety invariants. Use this type throughout the multi-threaded executor, with a liberal use of safety comments. --- ## Migration Guide The `System` trait now uses `UnsafeWorldCell` instead of `&World`. This type provides a robust API for interior mutable world access. - The method `run_unsafe` uses this type to manage world mutations across multiple threads. - The method `update_archetype_component_access` uses this type to ensure that only world metadata can be used. ```rust let mut system = IntoSystem::into_system(my_system); system.initialize(&mut world); // Before: system.update_archetype_component_access(&world); unsafe { system.run_unsafe(&world) } // After: system.update_archetype_component_access(world.as_unsafe_world_cell_readonly()); unsafe { system.run_unsafe(world.as_unsafe_world_cell()) } ``` --------- Co-authored-by: James Liu <contact@jamessliu.com>
1 parent 4465f25 commit 85a918a

File tree

12 files changed

+103
-69
lines changed

12 files changed

+103
-69
lines changed

benches/benches/bevy_ecs/iteration/heavy_compute.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub fn heavy_compute(c: &mut Criterion) {
4545

4646
let mut system = IntoSystem::into_system(sys);
4747
system.initialize(&mut world);
48-
system.update_archetype_component_access(&world);
48+
system.update_archetype_component_access(world.as_unsafe_world_cell());
4949

5050
b.iter(move || system.run((), &mut world));
5151
});

benches/benches/bevy_ecs/iteration/iter_simple_system.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ impl Benchmark {
3737

3838
let mut system = IntoSystem::into_system(query_system);
3939
system.initialize(&mut world);
40-
system.update_archetype_component_access(&world);
40+
system.update_archetype_component_access(world.as_unsafe_world_cell());
4141
Self(world, Box::new(system))
4242
}
4343

benches/benches/bevy_ecs/world/world_get.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ pub fn query_get_component_simple(criterion: &mut Criterion) {
291291

292292
let mut system = IntoSystem::into_system(query_system);
293293
system.initialize(&mut world);
294-
system.update_archetype_component_access(&world);
294+
system.update_archetype_component_access(world.as_unsafe_world_cell());
295295

296296
bencher.iter(|| system.run(entity, &mut world));
297297
});

crates/bevy_ecs/src/schedule/condition.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::ops::Not;
55
use crate::component::{self, ComponentId};
66
use crate::query::Access;
77
use crate::system::{CombinatorSystem, Combine, IntoSystem, ReadOnlySystem, System};
8+
use crate::world::unsafe_world_cell::UnsafeWorldCell;
89
use crate::world::World;
910

1011
pub type BoxedCondition = Box<dyn ReadOnlySystem<In = (), Out = bool>>;
@@ -990,7 +991,7 @@ where
990991
self.condition.is_exclusive()
991992
}
992993

993-
unsafe fn run_unsafe(&mut self, input: Self::In, world: &World) -> Self::Out {
994+
unsafe fn run_unsafe(&mut self, input: Self::In, world: UnsafeWorldCell) -> Self::Out {
994995
// SAFETY: The inner condition system asserts its own safety.
995996
!self.condition.run_unsafe(input, world)
996997
}
@@ -1007,7 +1008,7 @@ where
10071008
self.condition.initialize(world);
10081009
}
10091010

1010-
fn update_archetype_component_access(&mut self, world: &World) {
1011+
fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) {
10111012
self.condition.update_archetype_component_access(world);
10121013
}
10131014

crates/bevy_ecs/src/schedule/executor/multi_threaded.rs

Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::{
2121
is_apply_system_buffers, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule,
2222
},
2323
system::BoxedSystem,
24-
world::World,
24+
world::{unsafe_world_cell::UnsafeWorldCell, World},
2525
};
2626

2727
use crate as bevy_ecs;
@@ -184,7 +184,6 @@ impl SystemExecutor for MultiThreadedExecutor {
184184
.map(|e| e.0.clone());
185185
let thread_executor = thread_executor.as_deref();
186186

187-
let world = SyncUnsafeCell::from_mut(world);
188187
let SyncUnsafeSchedule {
189188
systems,
190189
mut conditions,
@@ -197,10 +196,13 @@ impl SystemExecutor for MultiThreadedExecutor {
197196
// the executor itself is a `Send` future so that it can run
198197
// alongside systems that claim the local thread
199198
let executor = async {
199+
let world_cell = world.as_unsafe_world_cell();
200200
while self.num_completed_systems < self.num_systems {
201-
// SAFETY: self.ready_systems does not contain running systems
201+
// SAFETY:
202+
// - self.ready_systems does not contain running systems.
203+
// - `world_cell` has mutable access to the entire world.
202204
unsafe {
203-
self.spawn_system_tasks(scope, systems, &mut conditions, world);
205+
self.spawn_system_tasks(scope, systems, &mut conditions, world_cell);
204206
}
205207

206208
if self.num_running_systems > 0 {
@@ -231,7 +233,7 @@ impl SystemExecutor for MultiThreadedExecutor {
231233
if self.apply_final_buffers {
232234
// Do one final apply buffers after all systems have completed
233235
// Commands should be applied while on the scope's thread, not the executor's thread
234-
let res = apply_system_buffers(&self.unapplied_systems, systems, world.get_mut());
236+
let res = apply_system_buffers(&self.unapplied_systems, systems, world);
235237
if let Err(payload) = res {
236238
let mut panic_payload = self.panic_payload.lock().unwrap();
237239
*panic_payload = Some(payload);
@@ -283,14 +285,16 @@ impl MultiThreadedExecutor {
283285
}
284286

285287
/// # Safety
286-
/// Caller must ensure that `self.ready_systems` does not contain any systems that
287-
/// have been mutably borrowed (such as the systems currently running).
288+
/// - Caller must ensure that `self.ready_systems` does not contain any systems that
289+
/// have been mutably borrowed (such as the systems currently running).
290+
/// - `world_cell` must have permission to access all world data (not counting
291+
/// any world data that is claimed by systems currently running on this executor).
288292
unsafe fn spawn_system_tasks<'scope>(
289293
&mut self,
290294
scope: &Scope<'_, 'scope, ()>,
291295
systems: &'scope [SyncUnsafeCell<BoxedSystem>],
292296
conditions: &mut Conditions,
293-
cell: &'scope SyncUnsafeCell<World>,
297+
world_cell: UnsafeWorldCell<'scope>,
294298
) {
295299
if self.exclusive_running {
296300
return;
@@ -307,10 +311,7 @@ impl MultiThreadedExecutor {
307311
// Therefore, no other reference to this system exists and there is no aliasing.
308312
let system = unsafe { &mut *systems[system_index].get() };
309313

310-
// SAFETY: No exclusive system is running.
311-
// Therefore, there is no existing mutable reference to the world.
312-
let world = unsafe { &*cell.get() };
313-
if !self.can_run(system_index, system, conditions, world) {
314+
if !self.can_run(system_index, system, conditions, world_cell) {
314315
// NOTE: exclusive systems with ambiguities are susceptible to
315316
// being significantly displaced here (compared to single-threaded order)
316317
// if systems after them in topological order can run
@@ -320,9 +321,10 @@ impl MultiThreadedExecutor {
320321

321322
self.ready_systems.set(system_index, false);
322323

323-
// SAFETY: Since `self.can_run` returned true earlier, it must have called
324-
// `update_archetype_component_access` for each run condition.
325-
if !self.should_run(system_index, system, conditions, world) {
324+
// SAFETY: `can_run` returned true, which means that:
325+
// - It must have called `update_archetype_component_access` for each run condition.
326+
// - There can be no systems running whose accesses would conflict with any conditions.
327+
if !self.should_run(system_index, system, conditions, world_cell) {
326328
self.skip_system_and_signal_dependents(system_index);
327329
continue;
328330
}
@@ -331,20 +333,23 @@ impl MultiThreadedExecutor {
331333
self.num_running_systems += 1;
332334

333335
if self.system_task_metadata[system_index].is_exclusive {
334-
// SAFETY: `can_run` confirmed that no systems are running.
335-
// Therefore, there is no existing reference to the world.
336+
// SAFETY: `can_run` returned true for this system, which means
337+
// that no other systems currently have access to the world.
338+
let world = unsafe { world_cell.world_mut() };
339+
// SAFETY: `can_run` returned true for this system,
340+
// which means no systems are currently borrowed.
336341
unsafe {
337-
let world = &mut *cell.get();
338342
self.spawn_exclusive_system_task(scope, system_index, systems, world);
339343
}
340344
break;
341345
}
342346

343347
// SAFETY:
344348
// - No other reference to this system exists.
345-
// - `self.can_run` has been called, which calls `update_archetype_component_access` with this system.
349+
// - `can_run` has been called, which calls `update_archetype_component_access` with this system.
350+
// - `can_run` returned true, so no systems with conflicting world access are running.
346351
unsafe {
347-
self.spawn_system_task(scope, system_index, systems, world);
352+
self.spawn_system_task(scope, system_index, systems, world_cell);
348353
}
349354
}
350355

@@ -357,7 +362,7 @@ impl MultiThreadedExecutor {
357362
system_index: usize,
358363
system: &mut BoxedSystem,
359364
conditions: &mut Conditions,
360-
world: &World,
365+
world: UnsafeWorldCell,
361366
) -> bool {
362367
let system_meta = &self.system_task_metadata[system_index];
363368
if system_meta.is_exclusive && self.num_running_systems > 0 {
@@ -413,15 +418,17 @@ impl MultiThreadedExecutor {
413418
}
414419

415420
/// # Safety
416-
///
417-
/// `update_archetype_component` must have been called with `world`
418-
/// for each run condition in `conditions`.
421+
/// * `world` must have permission to read any world data required by
422+
/// the system's conditions: this includes conditions for the system
423+
/// itself, and conditions for any of the system's sets.
424+
/// * `update_archetype_component` must have been called with `world`
425+
/// for each run condition in `conditions`.
419426
unsafe fn should_run(
420427
&mut self,
421428
system_index: usize,
422429
_system: &BoxedSystem,
423430
conditions: &mut Conditions,
424-
world: &World,
431+
world: UnsafeWorldCell,
425432
) -> bool {
426433
let mut should_run = !self.skipped_systems.contains(system_index);
427434
for set_idx in conditions.sets_with_conditions_of_systems[system_index].ones() {
@@ -430,7 +437,10 @@ impl MultiThreadedExecutor {
430437
}
431438

432439
// Evaluate the system set's conditions.
433-
// SAFETY: `update_archetype_component_access` has been called for each run condition.
440+
// SAFETY:
441+
// - The caller ensures that `world` has permission to read any data
442+
// required by the conditions.
443+
// - `update_archetype_component_access` has been called for each run condition.
434444
let set_conditions_met =
435445
evaluate_and_fold_conditions(&mut conditions.set_conditions[set_idx], world);
436446

@@ -444,7 +454,10 @@ impl MultiThreadedExecutor {
444454
}
445455

446456
// Evaluate the system's conditions.
447-
// SAFETY: `update_archetype_component_access` has been called for each run condition.
457+
// SAFETY:
458+
// - The caller ensures that `world` has permission to read any data
459+
// required by the conditions.
460+
// - `update_archetype_component_access` has been called for each run condition.
448461
let system_conditions_met =
449462
evaluate_and_fold_conditions(&mut conditions.system_conditions[system_index], world);
450463

@@ -459,14 +472,16 @@ impl MultiThreadedExecutor {
459472

460473
/// # Safety
461474
/// - Caller must not alias systems that are running.
475+
/// - `world` must have permission to access the world data
476+
/// used by the specified system.
462477
/// - `update_archetype_component_access` must have been called with `world`
463478
/// on the system assocaited with `system_index`.
464479
unsafe fn spawn_system_task<'scope>(
465480
&mut self,
466481
scope: &Scope<'_, 'scope, ()>,
467482
system_index: usize,
468483
systems: &'scope [SyncUnsafeCell<BoxedSystem>],
469-
world: &'scope World,
484+
world: UnsafeWorldCell<'scope>,
470485
) {
471486
// SAFETY: this system is not running, no other reference exists
472487
let system = unsafe { &mut *systems[system_index].get() };
@@ -483,7 +498,8 @@ impl MultiThreadedExecutor {
483498
let system_guard = system_span.enter();
484499
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
485500
// SAFETY:
486-
// - Access: TODO.
501+
// - The caller ensures that we have permission to
502+
// access the world data used by the system.
487503
// - `update_archetype_component_access` has been called.
488504
unsafe { system.run_unsafe((), world) };
489505
}));
@@ -688,18 +704,23 @@ fn apply_system_buffers(
688704
}
689705

690706
/// # Safety
691-
///
692-
/// `update_archetype_component_access` must have been called
693-
/// with `world` for each condition in `conditions`.
694-
unsafe fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &World) -> bool {
707+
/// - `world` must have permission to read any world data
708+
/// required by `conditions`.
709+
/// - `update_archetype_component_access` must have been called
710+
/// with `world` for each condition in `conditions`.
711+
unsafe fn evaluate_and_fold_conditions(
712+
conditions: &mut [BoxedCondition],
713+
world: UnsafeWorldCell,
714+
) -> bool {
695715
// not short-circuiting is intentional
696716
#[allow(clippy::unnecessary_fold)]
697717
conditions
698718
.iter_mut()
699719
.map(|condition| {
700720
#[cfg(feature = "trace")]
701721
let _condition_span = info_span!("condition", name = &*condition.name()).entered();
702-
// SAFETY: caller ensures system access is compatible
722+
// SAFETY: The caller ensures that `world` has permission to
723+
// access any data required by the condition.
703724
unsafe { condition.run_unsafe((), world) }
704725
})
705726
.fold(true, |acc, res| acc && res)

crates/bevy_ecs/src/system/combinator.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
component::{ComponentId, Tick},
88
prelude::World,
99
query::Access,
10+
world::unsafe_world_cell::UnsafeWorldCell,
1011
};
1112

1213
use super::{ReadOnlySystem, System};
@@ -157,7 +158,7 @@ where
157158
self.a.is_exclusive() || self.b.is_exclusive()
158159
}
159160

160-
unsafe fn run_unsafe(&mut self, input: Self::In, world: &World) -> Self::Out {
161+
unsafe fn run_unsafe(&mut self, input: Self::In, world: UnsafeWorldCell) -> Self::Out {
161162
Func::combine(
162163
input,
163164
// SAFETY: The world accesses for both underlying systems have been registered,
@@ -198,7 +199,7 @@ where
198199
self.component_access.extend(self.b.component_access());
199200
}
200201

201-
fn update_archetype_component_access(&mut self, world: &World) {
202+
fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) {
202203
self.a.update_archetype_component_access(world);
203204
self.b.update_archetype_component_access(world);
204205

crates/bevy_ecs/src/system/exclusive_function_system.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
check_system_change_tick, ExclusiveSystemParam, ExclusiveSystemParamItem, In, IntoSystem,
77
System, SystemMeta,
88
},
9-
world::World,
9+
world::{unsafe_world_cell::UnsafeWorldCell, World},
1010
};
1111

1212
use bevy_utils::all_tuples;
@@ -86,7 +86,7 @@ where
8686
}
8787

8888
#[inline]
89-
unsafe fn run_unsafe(&mut self, _input: Self::In, _world: &World) -> Self::Out {
89+
unsafe fn run_unsafe(&mut self, _input: Self::In, _world: UnsafeWorldCell) -> Self::Out {
9090
panic!("Cannot run exclusive systems with a shared World reference");
9191
}
9292

@@ -134,7 +134,7 @@ where
134134
self.param_state = Some(F::Param::init(world, &mut self.system_meta));
135135
}
136136

137-
fn update_archetype_component_access(&mut self, _world: &World) {}
137+
fn update_archetype_component_access(&mut self, _world: UnsafeWorldCell) {}
138138

139139
#[inline]
140140
fn check_change_tick(&mut self, change_tick: Tick) {

crates/bevy_ecs/src/system/function_system.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::{
44
prelude::FromWorld,
55
query::{Access, FilteredAccessSet},
66
system::{check_system_change_tick, ReadOnlySystemParam, System, SystemParam, SystemParamItem},
7-
world::{World, WorldId},
7+
world::{unsafe_world_cell::UnsafeWorldCell, World, WorldId},
88
};
99

1010
use bevy_utils::all_tuples;
@@ -417,7 +417,7 @@ where
417417
}
418418

419419
#[inline]
420-
unsafe fn run_unsafe(&mut self, input: Self::In, world: &World) -> Self::Out {
420+
unsafe fn run_unsafe(&mut self, input: Self::In, world: UnsafeWorldCell) -> Self::Out {
421421
let change_tick = world.increment_change_tick();
422422

423423
// SAFETY:
@@ -428,7 +428,7 @@ where
428428
let params = F::Param::get_param(
429429
self.param_state.as_mut().expect(Self::PARAM_MESSAGE),
430430
&self.system_meta,
431-
world.as_unsafe_world_cell_migration_internal(),
431+
world,
432432
change_tick,
433433
);
434434
let out = self.func.run(input, params);
@@ -457,7 +457,7 @@ where
457457
self.param_state = Some(F::Param::init_state(world, &mut self.system_meta));
458458
}
459459

460-
fn update_archetype_component_access(&mut self, world: &World) {
460+
fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) {
461461
assert!(self.world_id == Some(world.id()), "Encountered a mismatched World. A System cannot be used with Worlds other than the one it was initialized with.");
462462
let archetypes = world.archetypes();
463463
let new_generation = archetypes.generation();

crates/bevy_ecs/src/system/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,7 +1610,7 @@ mod tests {
16101610

16111611
// set up system and verify its access is empty
16121612
system.initialize(&mut world);
1613-
system.update_archetype_component_access(&world);
1613+
system.update_archetype_component_access(world.as_unsafe_world_cell());
16141614
assert_eq!(
16151615
system
16161616
.archetype_component_access()
@@ -1640,7 +1640,7 @@ mod tests {
16401640
world.spawn((B, C));
16411641

16421642
// update system and verify its accesses are correct
1643-
system.update_archetype_component_access(&world);
1643+
system.update_archetype_component_access(world.as_unsafe_world_cell());
16441644
assert_eq!(
16451645
system
16461646
.archetype_component_access()
@@ -1658,7 +1658,7 @@ mod tests {
16581658
.unwrap(),
16591659
);
16601660
world.spawn((A, B, D));
1661-
system.update_archetype_component_access(&world);
1661+
system.update_archetype_component_access(world.as_unsafe_world_cell());
16621662
assert_eq!(
16631663
system
16641664
.archetype_component_access()

0 commit comments

Comments
 (0)