Skip to content

Commit e312da8

Browse files
authored
Reduce runtime panics through SystemParam validation (#15276)
# Objective The goal of this PR is to introduce `SystemParam` validation in order to reduce runtime panics. Fixes #15265 ## Solution `SystemParam` now has a new method `validate_param(...) -> bool`, which takes immutable variants of `get_param` arguments. The returned value indicates whether the parameter can be acquired from the world. If parameters cannot be acquired for a system, it won't be executed, similarly to run conditions. This reduces panics when using params like `Res`, `ResMut`, etc. as well as allows for new, ergonomic params like #15264 or #15302. Param validation happens at the level of executors. All validation happens directly before executing a system, in case of normal systems they are skipped, in case of conditions they return false. Warning about system skipping is primitive and subject to change in subsequent PRs. ## Testing Two executor tests check that all executors: - skip systems which have invalid parameters: - piped systems get skipped together, - dependent systems still run correctly, - skip systems with invalid run conditions: - system conditions have invalid parameters, - system set conditions have invalid parameters.
1 parent 4d0961c commit e312da8

File tree

17 files changed

+474
-36
lines changed

17 files changed

+474
-36
lines changed

crates/bevy_ecs/macros/src/lib.rs

+19
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,15 @@ pub fn impl_param_set(_input: TokenStream) -> TokenStream {
271271
<(#(#param,)*) as SystemParam>::apply(state, system_meta, world);
272272
}
273273

274+
#[inline]
275+
unsafe fn validate_param<'w, 's>(
276+
state: &'s Self::State,
277+
system_meta: &SystemMeta,
278+
world: UnsafeWorldCell<'w>,
279+
) -> bool {
280+
<(#(#param,)*) as SystemParam>::validate_param(state, system_meta, world)
281+
}
282+
274283
#[inline]
275284
unsafe fn get_param<'w, 's>(
276285
state: &'s mut Self::State,
@@ -512,6 +521,16 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream {
512521
<#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::queue(&mut state.state, system_meta, world);
513522
}
514523

524+
#[inline]
525+
unsafe fn validate_param<'w, 's>(
526+
state: &'s Self::State,
527+
system_meta: &#path::system::SystemMeta,
528+
world: #path::world::unsafe_world_cell::UnsafeWorldCell<'w>,
529+
) -> bool {
530+
<(#(#tuple_types,)*) as #path::system::SystemParam>::validate_param(&state.state, system_meta, world)
531+
}
532+
533+
#[inline]
515534
unsafe fn get_param<'w, 's>(
516535
state: &'s mut Self::State,
517536
system_meta: &#path::system::SystemMeta,

crates/bevy_ecs/src/schedule/condition.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ pub trait Condition<Marker, In = ()>: sealed::Condition<Marker, In> {
7979
///
8080
/// # Examples
8181
///
82-
/// ```should_panic
82+
/// ```
8383
/// use bevy_ecs::prelude::*;
8484
///
8585
/// #[derive(Resource, PartialEq)]
@@ -89,7 +89,7 @@ pub trait Condition<Marker, In = ()>: sealed::Condition<Marker, In> {
8989
/// # let mut world = World::new();
9090
/// # fn my_system() {}
9191
/// app.add_systems(
92-
/// // The `resource_equals` run condition will panic since we don't initialize `R`,
92+
/// // The `resource_equals` run condition will fail since we don't initialize `R`,
9393
/// // just like if we used `Res<R>` in a system.
9494
/// my_system.run_if(resource_equals(R(0))),
9595
/// );
@@ -130,7 +130,7 @@ pub trait Condition<Marker, In = ()>: sealed::Condition<Marker, In> {
130130
///
131131
/// # Examples
132132
///
133-
/// ```should_panic
133+
/// ```
134134
/// use bevy_ecs::prelude::*;
135135
///
136136
/// #[derive(Resource, PartialEq)]
@@ -140,7 +140,7 @@ pub trait Condition<Marker, In = ()>: sealed::Condition<Marker, In> {
140140
/// # let mut world = World::new();
141141
/// # fn my_system() {}
142142
/// app.add_systems(
143-
/// // The `resource_equals` run condition will panic since we don't initialize `R`,
143+
/// // The `resource_equals` run condition will fail since we don't initialize `R`,
144144
/// // just like if we used `Res<R>` in a system.
145145
/// my_system.run_if(resource_equals(R(0))),
146146
/// );

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

+97
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,100 @@ mod __rust_begin_short_backtrace {
176176
black_box(system.run((), world))
177177
}
178178
}
179+
180+
#[macro_export]
181+
/// Emits a warning about system being skipped.
182+
macro_rules! warn_system_skipped {
183+
($ty:literal, $sys:expr) => {
184+
bevy_utils::tracing::warn!(
185+
"{} {} was skipped due to inaccessible system parameters.",
186+
$ty,
187+
$sys
188+
)
189+
};
190+
}
191+
192+
#[cfg(test)]
193+
mod tests {
194+
use crate::{
195+
self as bevy_ecs,
196+
prelude::{IntoSystemConfigs, IntoSystemSetConfigs, Resource, Schedule, SystemSet},
197+
schedule::ExecutorKind,
198+
system::{Commands, In, IntoSystem, Res},
199+
world::World,
200+
};
201+
202+
#[derive(Resource)]
203+
struct R1;
204+
205+
#[derive(Resource)]
206+
struct R2;
207+
208+
const EXECUTORS: [ExecutorKind; 3] = [
209+
ExecutorKind::Simple,
210+
ExecutorKind::SingleThreaded,
211+
ExecutorKind::MultiThreaded,
212+
];
213+
214+
#[test]
215+
fn invalid_system_param_skips() {
216+
for executor in EXECUTORS {
217+
invalid_system_param_skips_core(executor);
218+
}
219+
}
220+
221+
fn invalid_system_param_skips_core(executor: ExecutorKind) {
222+
let mut world = World::new();
223+
let mut schedule = Schedule::default();
224+
schedule.set_executor_kind(executor);
225+
schedule.add_systems(
226+
(
227+
// Combined systems get skipped together.
228+
(|mut commands: Commands| {
229+
commands.insert_resource(R1);
230+
})
231+
.pipe(|_: In<()>, _: Res<R1>| {}),
232+
// This system depends on a system that is always skipped.
233+
|mut commands: Commands| {
234+
commands.insert_resource(R2);
235+
},
236+
)
237+
.chain(),
238+
);
239+
schedule.run(&mut world);
240+
assert!(world.get_resource::<R1>().is_none());
241+
assert!(world.get_resource::<R2>().is_some());
242+
}
243+
244+
#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)]
245+
struct S1;
246+
247+
#[test]
248+
fn invalid_condition_param_skips_system() {
249+
for executor in EXECUTORS {
250+
invalid_condition_param_skips_system_core(executor);
251+
}
252+
}
253+
254+
fn invalid_condition_param_skips_system_core(executor: ExecutorKind) {
255+
let mut world = World::new();
256+
let mut schedule = Schedule::default();
257+
schedule.set_executor_kind(executor);
258+
schedule.configure_sets(S1.run_if(|_: Res<R1>| true));
259+
schedule.add_systems((
260+
// System gets skipped if system set run conditions fail validation.
261+
(|mut commands: Commands| {
262+
commands.insert_resource(R1);
263+
})
264+
.in_set(S1),
265+
// System gets skipped if run conditions fail validation.
266+
(|mut commands: Commands| {
267+
commands.insert_resource(R2);
268+
})
269+
.run_if(|_: Res<R2>| true),
270+
));
271+
schedule.run(&mut world);
272+
assert!(world.get_resource::<R1>().is_none());
273+
assert!(world.get_resource::<R2>().is_none());
274+
}
275+
}

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

+29-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::{
1919
query::Access,
2020
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
2121
system::BoxedSystem,
22+
warn_system_skipped,
2223
world::{unsafe_world_cell::UnsafeWorldCell, World},
2324
};
2425

@@ -519,15 +520,16 @@ impl ExecutorState {
519520
/// the system's conditions: this includes conditions for the system
520521
/// itself, and conditions for any of the system's sets.
521522
/// * `update_archetype_component` must have been called with `world`
522-
/// for each run condition in `conditions`.
523+
/// for the system as well as system and system set's run conditions.
523524
unsafe fn should_run(
524525
&mut self,
525526
system_index: usize,
526-
_system: &BoxedSystem,
527+
system: &BoxedSystem,
527528
conditions: &mut Conditions,
528529
world: UnsafeWorldCell,
529530
) -> bool {
530531
let mut should_run = !self.skipped_systems.contains(system_index);
532+
531533
for set_idx in conditions.sets_with_conditions_of_systems[system_index].ones() {
532534
if self.evaluated_sets.contains(set_idx) {
533535
continue;
@@ -566,6 +568,19 @@ impl ExecutorState {
566568

567569
should_run &= system_conditions_met;
568570

571+
// SAFETY:
572+
// - The caller ensures that `world` has permission to read any data
573+
// required by the system.
574+
// - `update_archetype_component_access` has been called for system.
575+
let valid_params = unsafe { system.validate_param_unsafe(world) };
576+
577+
if !valid_params {
578+
warn_system_skipped!("System", system.name());
579+
self.skipped_systems.insert(system_index);
580+
}
581+
582+
should_run &= valid_params;
583+
569584
should_run
570585
}
571586

@@ -731,8 +746,18 @@ unsafe fn evaluate_and_fold_conditions(
731746
conditions
732747
.iter_mut()
733748
.map(|condition| {
734-
// SAFETY: The caller ensures that `world` has permission to
735-
// access any data required by the condition.
749+
// SAFETY:
750+
// - The caller ensures that `world` has permission to read any data
751+
// required by the condition.
752+
// - `update_archetype_component_access` has been called for condition.
753+
if !unsafe { condition.validate_param_unsafe(world) } {
754+
warn_system_skipped!("Condition", condition.name());
755+
return false;
756+
}
757+
// SAFETY:
758+
// - The caller ensures that `world` has permission to read any data
759+
// required by the condition.
760+
// - `update_archetype_component_access` has been called for condition.
736761
unsafe { __rust_begin_short_backtrace::readonly_run_unsafe(&mut **condition, world) }
737762
})
738763
.fold(true, |acc, res| acc && res)

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

+17-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
schedule::{
88
executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule,
99
},
10+
warn_system_skipped,
1011
world::World,
1112
};
1213

@@ -79,6 +80,15 @@ impl SystemExecutor for SimpleExecutor {
7980

8081
should_run &= system_conditions_met;
8182

83+
let system = &mut schedule.systems[system_index];
84+
let valid_params = system.validate_param(world);
85+
86+
if !valid_params {
87+
warn_system_skipped!("System", system.name());
88+
}
89+
90+
should_run &= valid_params;
91+
8292
#[cfg(feature = "trace")]
8393
should_run_span.exit();
8494

@@ -89,7 +99,6 @@ impl SystemExecutor for SimpleExecutor {
8999
continue;
90100
}
91101

92-
let system = &mut schedule.systems[system_index];
93102
if is_apply_deferred(system) {
94103
continue;
95104
}
@@ -128,7 +137,13 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W
128137
#[allow(clippy::unnecessary_fold)]
129138
conditions
130139
.iter_mut()
131-
.map(|condition| __rust_begin_short_backtrace::readonly_run(&mut **condition, world))
140+
.map(|condition| {
141+
if !condition.validate_param(world) {
142+
warn_system_skipped!("Condition", condition.name());
143+
return false;
144+
}
145+
__rust_begin_short_backtrace::readonly_run(&mut **condition, world)
146+
})
132147
.fold(true, |acc, res| acc && res)
133148
}
134149

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

+17-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::panic::AssertUnwindSafe;
55

66
use crate::{
77
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
8+
warn_system_skipped,
89
world::World,
910
};
1011

@@ -85,6 +86,15 @@ impl SystemExecutor for SingleThreadedExecutor {
8586

8687
should_run &= system_conditions_met;
8788

89+
let system = &mut schedule.systems[system_index];
90+
let valid_params = system.validate_param(world);
91+
92+
if !valid_params {
93+
warn_system_skipped!("System", system.name());
94+
}
95+
96+
should_run &= valid_params;
97+
8898
#[cfg(feature = "trace")]
8999
should_run_span.exit();
90100

@@ -95,7 +105,6 @@ impl SystemExecutor for SingleThreadedExecutor {
95105
continue;
96106
}
97107

98-
let system = &mut schedule.systems[system_index];
99108
if is_apply_deferred(system) {
100109
self.apply_deferred(schedule, world);
101110
continue;
@@ -160,6 +169,12 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W
160169
#[allow(clippy::unnecessary_fold)]
161170
conditions
162171
.iter_mut()
163-
.map(|condition| __rust_begin_short_backtrace::readonly_run(&mut **condition, world))
172+
.map(|condition| {
173+
if !condition.validate_param(world) {
174+
warn_system_skipped!("Condition", condition.name());
175+
return false;
176+
}
177+
__rust_begin_short_backtrace::readonly_run(&mut **condition, world)
178+
})
164179
.fold(true, |acc, res| acc && res)
165180
}

crates/bevy_ecs/src/system/adapter_system.rs

+5
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ where
132132
self.system.queue_deferred(world);
133133
}
134134

135+
#[inline]
136+
unsafe fn validate_param_unsafe(&self, world: UnsafeWorldCell) -> bool {
137+
self.system.validate_param_unsafe(world)
138+
}
139+
135140
fn initialize(&mut self, world: &mut crate::prelude::World) {
136141
self.system.initialize(world);
137142
}

crates/bevy_ecs/src/system/combinator.rs

+6
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ where
193193
)
194194
}
195195

196+
#[inline]
196197
fn apply_deferred(&mut self, world: &mut World) {
197198
self.a.apply_deferred(world);
198199
self.b.apply_deferred(world);
@@ -204,6 +205,11 @@ where
204205
self.b.queue_deferred(world);
205206
}
206207

208+
#[inline]
209+
unsafe fn validate_param_unsafe(&self, world: UnsafeWorldCell) -> bool {
210+
self.a.validate_param_unsafe(world) && self.b.validate_param_unsafe(world)
211+
}
212+
207213
fn initialize(&mut self, world: &mut World) {
208214
self.a.initialize(world);
209215
self.b.initialize(world);

0 commit comments

Comments
 (0)