Skip to content

Commit 4bca7f1

Browse files
authored
Improved Command Errors (#17215)
# Objective Rework / build on #17043 to simplify the implementation. #17043 should be merged first, and the diff from this PR will get much nicer after it is merged (this PR is net negative LOC). ## Solution 1. Command and EntityCommand have been vastly simplified. No more marker components. Just one function. 2. Command and EntityCommand are now generic on the return type. This enables result-less commands to exist, and allows us to statically distinguish between fallible and infallible commands, which allows us to skip the "error handling overhead" for cases that don't need it. 3. There are now only two command queue variants: `queue` and `queue_fallible`. `queue` accepts commands with no return type. `queue_fallible` accepts commands that return a Result (specifically, one that returns an error that can convert to `bevy_ecs::result::Error`). 4. I've added the concept of the "default error handler", which is used by `queue_fallible`. This is a simple direct call to the `panic()` error handler by default. Users that want to override this can enable the `configurable_error_handler` cargo feature, then initialize the GLOBAL_ERROR_HANDLER value on startup. This is behind a flag because there might be minor overhead with `OnceLock` and I'm guessing this will be a niche feature. We can also do perf testing with OnceLock if someone really wants it to be used unconditionally, but I don't personally feel the need to do that. 5. I removed the "temporary error handler" on Commands (and all code associated with it). It added more branching, made Commands bigger / more expensive to initialize (note that we construct it at high frequencies / treat it like a pointer type), made the code harder to follow, and introduced a bunch of additional functions. We instead rely on the new default error handler used in `queue_fallible` for most things. In the event that a custom handler is required, `handle_error_with` can be used. 6. EntityCommand now _only_ supports functions that take `EntityWorldMut` (and all existing entity commands have been ported). Removing the marker component from EntityCommand hinged on this change, but I strongly believe this is for the best anyway, as this sets the stage for more efficient batched entity commands. 7. I added `EntityWorldMut::resource` and the other variants for more ergonomic resource access on `EntityWorldMut` (removes the need for entity.world_scope, which also incurs entity-lookup overhead). ## Open Questions 1. I believe we could merge `queue` and `queue_fallible` into a single `queue` which accepts both fallible and infallible commands (via the introduction of a `QueueCommand` trait). Is this desirable?
1 parent 145f5f4 commit 4bca7f1

File tree

14 files changed

+520
-612
lines changed

14 files changed

+520
-612
lines changed

benches/benches/bevy_ecs/world/commands.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use core::hint::black_box;
22

33
use bevy_ecs::{
44
component::Component,
5-
result::Result,
65
system::{Command, Commands},
76
world::{CommandQueue, World},
87
};
@@ -137,18 +136,16 @@ struct FakeCommandA;
137136
struct FakeCommandB(u64);
138137

139138
impl Command for FakeCommandA {
140-
fn apply(self, world: &mut World) -> Result {
139+
fn apply(self, world: &mut World) {
141140
black_box(self);
142141
black_box(world);
143-
Ok(())
144142
}
145143
}
146144

147145
impl Command for FakeCommandB {
148-
fn apply(self, world: &mut World) -> Result {
146+
fn apply(self, world: &mut World) {
149147
black_box(self);
150148
black_box(world);
151-
Ok(())
152149
}
153150
}
154151

@@ -183,10 +180,9 @@ pub fn fake_commands(criterion: &mut Criterion) {
183180
struct SizedCommand<T: Default + Send + Sync + 'static>(T);
184181

185182
impl<T: Default + Send + Sync + 'static> Command for SizedCommand<T> {
186-
fn apply(self, world: &mut World) -> Result {
183+
fn apply(self, world: &mut World) {
187184
black_box(self);
188185
black_box(world);
189-
Ok(())
190186
}
191187
}
192188

crates/bevy_ecs/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ bevy_reflect = ["dep:bevy_reflect"]
2828
## Extends reflection support to functions.
2929
reflect_functions = ["bevy_reflect", "bevy_reflect/functions"]
3030

31+
## Use the configurable global error handler as the default error handler
32+
configurable_error_handler = []
33+
3134
# Debugging Features
3235

3336
## Enables `tracing` integration, allowing spans and other metrics to be reported

crates/bevy_ecs/src/reflect/entity_commands.rs

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -169,30 +169,24 @@ pub trait ReflectCommandExt {
169169

170170
impl ReflectCommandExt for EntityCommands<'_> {
171171
fn insert_reflect(&mut self, component: Box<dyn PartialReflect>) -> &mut Self {
172-
self.queue(move |entity: Entity, world: &mut World| {
173-
if let Ok(mut entity) = world.get_entity_mut(entity) {
174-
entity.insert_reflect(component);
175-
}
172+
self.queue(move |mut entity: EntityWorldMut| {
173+
entity.insert_reflect(component);
176174
})
177175
}
178176

179177
fn insert_reflect_with_registry<T: Resource + AsRef<TypeRegistry>>(
180178
&mut self,
181179
component: Box<dyn PartialReflect>,
182180
) -> &mut Self {
183-
self.queue(move |entity: Entity, world: &mut World| {
184-
if let Ok(mut entity) = world.get_entity_mut(entity) {
185-
entity.insert_reflect_with_registry::<T>(component);
186-
}
181+
self.queue(move |mut entity: EntityWorldMut| {
182+
entity.insert_reflect_with_registry::<T>(component);
187183
})
188184
}
189185

190186
fn remove_reflect(&mut self, component_type_path: impl Into<Cow<'static, str>>) -> &mut Self {
191187
let component_type_path: Cow<'static, str> = component_type_path.into();
192-
self.queue(move |entity: Entity, world: &mut World| {
193-
if let Ok(mut entity) = world.get_entity_mut(entity) {
194-
entity.remove_reflect(component_type_path);
195-
}
188+
self.queue(move |mut entity: EntityWorldMut| {
189+
entity.remove_reflect(component_type_path);
196190
})
197191
}
198192

@@ -201,10 +195,8 @@ impl ReflectCommandExt for EntityCommands<'_> {
201195
component_type_path: impl Into<Cow<'static, str>>,
202196
) -> &mut Self {
203197
let component_type_path: Cow<'static, str> = component_type_path.into();
204-
self.queue(move |entity: Entity, world: &mut World| {
205-
if let Ok(mut entity) = world.get_entity_mut(entity) {
206-
entity.remove_reflect_with_registry::<T>(component_type_path);
207-
}
198+
self.queue(move |mut entity: EntityWorldMut| {
199+
entity.remove_reflect_with_registry::<T>(component_type_path);
208200
})
209201
}
210202
}

crates/bevy_ecs/src/system/commands/command.rs

Lines changed: 41 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@ use crate::{
1212
entity::Entity,
1313
event::{Event, Events},
1414
observer::TriggerTargets,
15-
result::Result,
15+
result::{Error, Result},
1616
schedule::ScheduleLabel,
17-
system::{CommandError, IntoSystem, Resource, SystemId, SystemInput},
17+
system::{error_handler, IntoSystem, Resource, SystemId, SystemInput},
1818
world::{FromWorld, SpawnBatchIter, World},
1919
};
2020

2121
/// A [`World`] mutation.
2222
///
2323
/// Should be used with [`Commands::queue`](crate::system::Commands::queue).
2424
///
25+
/// The `Out` generic parameter is the returned "output" of the command.
26+
///
2527
/// # Usage
2628
///
2729
/// ```
@@ -34,102 +36,70 @@ use crate::{
3436
/// struct AddToCounter(u64);
3537
///
3638
/// impl Command for AddToCounter {
37-
/// fn apply(self, world: &mut World) -> Result {
39+
/// fn apply(self, world: &mut World) {
3840
/// let mut counter = world.get_resource_or_insert_with(Counter::default);
3941
/// counter.0 += self.0;
40-
/// Ok(())
4142
/// }
4243
/// }
4344
///
4445
/// fn some_system(mut commands: Commands) {
4546
/// commands.queue(AddToCounter(42));
4647
/// }
4748
/// ```
48-
///
49-
/// # Note on Generic
50-
///
51-
/// The `Marker` generic is necessary to allow multiple blanket implementations
52-
/// of `Command` for closures, like so:
53-
/// ```ignore (This would conflict with the real implementations)
54-
/// impl Command for FnOnce(&mut World)
55-
/// impl Command<Result> for FnOnce(&mut World) -> Result
56-
/// ```
57-
/// Without the generic, Rust would consider the two implementations to be conflicting.
58-
///
59-
/// The type used for `Marker` has no connection to anything else in the implementation.
60-
pub trait Command<Marker = ()>: Send + 'static {
49+
pub trait Command<Out = ()>: Send + 'static {
6150
/// Applies this command, causing it to mutate the provided `world`.
6251
///
6352
/// This method is used to define what a command "does" when it is ultimately applied.
6453
/// Because this method takes `self`, you can store data or settings on the type that implements this trait.
6554
/// This data is set by the system or other source of the command, and then ultimately read in this method.
66-
fn apply(self, world: &mut World) -> Result;
55+
fn apply(self, world: &mut World) -> Out;
56+
}
6757

68-
/// Applies this command and converts any resulting error into a [`CommandError`].
69-
///
70-
/// Overwriting this method allows an implementor to return a `CommandError` directly
71-
/// and avoid erasing the error's type.
72-
fn apply_internal(self, world: &mut World) -> Result<(), CommandError>
73-
where
74-
Self: Sized,
75-
{
76-
match self.apply(world) {
77-
Ok(_) => Ok(()),
78-
Err(error) => Err(CommandError::CommandFailed(error)),
79-
}
58+
impl<F, Out> Command<Out> for F
59+
where
60+
F: FnOnce(&mut World) -> Out + Send + 'static,
61+
{
62+
fn apply(self, world: &mut World) -> Out {
63+
self(world)
8064
}
65+
}
8166

82-
/// Returns a new [`Command`] that, when applied, will apply the original command
83-
/// and pass any resulting error to the provided `error_handler`.
84-
fn with_error_handling(
85-
self,
86-
error_handler: Option<fn(&mut World, CommandError)>,
87-
) -> impl Command
67+
/// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into
68+
/// a [`Command`] that internally handles an error if it occurs and returns `()`.
69+
pub trait HandleError<Out = ()> {
70+
/// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into
71+
/// a [`Command`] that internally handles an error if it occurs and returns `()`.
72+
fn handle_error_with(self, error_handler: fn(&mut World, Error)) -> impl Command;
73+
/// Takes a [`Command`] that returns a Result and uses the default error handler function to convert it into
74+
/// a [`Command`] that internally handles an error if it occurs and returns `()`.
75+
fn handle_error(self) -> impl Command
8876
where
8977
Self: Sized,
9078
{
91-
move |world: &mut World| {
92-
if let Err(error) = self.apply_internal(world) {
93-
// TODO: Pass the error to the global error handler if `error_handler` is `None`.
94-
let error_handler = error_handler.unwrap_or(|_, error| panic!("{error}"));
95-
error_handler(world, error);
96-
}
97-
}
98-
}
99-
}
100-
101-
impl<F> Command for F
102-
where
103-
F: FnOnce(&mut World) + Send + 'static,
104-
{
105-
fn apply(self, world: &mut World) -> Result {
106-
self(world);
107-
Ok(())
79+
self.handle_error_with(error_handler::default())
10880
}
10981
}
11082

111-
impl<F> Command<Result> for F
112-
where
113-
F: FnOnce(&mut World) -> Result + Send + 'static,
114-
{
115-
fn apply(self, world: &mut World) -> Result {
116-
self(world)
83+
impl<C: Command<Result<T, E>>, T, E: Into<Error>> HandleError<Result<T, E>> for C {
84+
fn handle_error_with(self, error_handler: fn(&mut World, Error)) -> impl Command {
85+
move |world: &mut World| match self.apply(world) {
86+
Ok(_) => {}
87+
Err(err) => (error_handler)(world, err.into()),
88+
}
11789
}
11890
}
11991

120-
/// Necessary to avoid erasing the type of the `CommandError` in
121-
/// [`EntityCommand::with_entity`](crate::system::EntityCommand::with_entity).
122-
impl<F> Command<(Result, CommandError)> for F
123-
where
124-
F: FnOnce(&mut World) -> Result<(), CommandError> + Send + 'static,
125-
{
126-
fn apply(self, world: &mut World) -> Result {
127-
self(world)?;
128-
Ok(())
92+
impl<C: Command> HandleError for C {
93+
#[inline]
94+
fn handle_error_with(self, _error_handler: fn(&mut World, Error)) -> impl Command {
95+
self
12996
}
130-
131-
fn apply_internal(self, world: &mut World) -> Result<(), CommandError> {
132-
self(world)
97+
#[inline]
98+
fn handle_error(self) -> impl Command
99+
where
100+
Self: Sized,
101+
{
102+
self
133103
}
134104
}
135105

0 commit comments

Comments
 (0)