Skip to content

Safer dynamic event handling for observers #14919

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 51 additions & 0 deletions benches/benches/bevy_ecs/observers/dynamic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use bevy_ecs::{
event::Event,
observer::{DynamicEvent, EmitDynamicTrigger, EventData, Observer, Trigger},
world::{Command, World},
};
use criterion::{black_box, Criterion};

pub fn observe_dynamic(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("observe_dynamic");
group.warm_up_time(std::time::Duration::from_millis(500));
group.measurement_time(std::time::Duration::from_secs(4));

group.bench_function("1", |bencher| {
let mut world = World::new();
let event_id_1 = world.init_component::<TestEvent<1>>();
world.spawn(Observer::new(empty_listener_set::<DynamicEvent>).with_event(event_id_1));
bencher.iter(|| {
for _ in 0..10000 {
unsafe {
EmitDynamicTrigger::new_with_id(event_id_1, TestEvent::<1>, ())
.apply(&mut world)
};
}
});
});
group.bench_function("2", |bencher| {
let mut world = World::new();
let event_id_1 = world.init_component::<TestEvent<1>>();
let event_id_2 = world.init_component::<TestEvent<2>>();
world.spawn(
Observer::new(empty_listener_set::<DynamicEvent>)
.with_event(event_id_1)
.with_event(event_id_2),
);
bencher.iter(|| {
for _ in 0..10000 {
unsafe {
EmitDynamicTrigger::new_with_id(event_id_2, TestEvent::<2>, ())
.apply(&mut world)
};
}
});
});
}

#[derive(Event)]
struct TestEvent<const N: usize>;

fn empty_listener_set<Set: EventData>(trigger: Trigger<Set>) {
black_box(trigger);
}
9 changes: 8 additions & 1 deletion benches/benches/bevy_ecs/observers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
use criterion::criterion_group;

mod dynamic;
mod propagation;
mod simple;
use dynamic::*;
use propagation::*;
use simple::*;

criterion_group!(observer_benches, event_propagation, observe_simple);
criterion_group!(
observer_benches,
event_propagation,
observe_simple,
observe_dynamic,
);
3 changes: 2 additions & 1 deletion crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub use bevy_derive::AppLabel;
use bevy_ecs::{
event::{event_update_system, EventCursor},
intern::Interned,
observer::EventData,
prelude::*,
schedule::{ScheduleBuildSettings, ScheduleLabel},
system::{IntoObserverSystem, SystemId},
Expand Down Expand Up @@ -1020,7 +1021,7 @@ impl App {
/// }
/// });
/// ```
pub fn observe<E: Event, B: Bundle, M>(
pub fn observe<E: EventData, B: Bundle, M>(
&mut self,
observer: impl IntoObserverSystem<E, B, M>,
) -> &mut Self {
Expand Down
149 changes: 149 additions & 0 deletions crates/bevy_ecs/src/observer/data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use bevy_ptr::{Ptr, PtrMut};

use crate::component::ComponentId;
use crate::event::Event;
use crate::observer::ObserverTrigger;
use crate::world::World;

/// The event data that an [`Observer`] is triggered with.
/// Ordinarily, this will be a mutable reference to an [`Event`] type.
///
/// The provided implementations of this trait are:
///
/// - All [`Event`] types.
/// - [`DynamicEvent`].
///
/// # Safety
///
/// Implementor must ensure that:
/// - [`EventData::init_components`] must register a [`ComponentId`] for each [`Event`] type used in the output type.
///
/// [`Observer`]: crate::observer::Observer
/// [`Observer::with_event`]: crate::observer::Observer::with_event
pub unsafe trait EventData: 'static {
/// The item returned by this [`EventData`] that will be passed to the observer system function.
/// Most of the time this will be a mutable reference to an [`Event`] type or a [`PtrMut`].
type Item<'trigger>;
/// The read-only variant of the [`Item`](EventData::Item).
type ReadOnlyItem<'trigger>: Copy;

/// Casts a pointer to the output [`Item`](EventData::Item) type.
///
/// # Safety
///
/// Caller must ensure that the given `ptr` can be safely converted to the output [`Item`](EventData::Item) type.
unsafe fn cast<'trigger>(
world: &World,
observer_trigger: &ObserverTrigger,
ptr: PtrMut<'trigger>,
) -> Self::Item<'trigger>;

/// Initialize the components required by this event data.
fn init_components(world: &mut World, ids: impl FnMut(ComponentId));

/// Shrink the [`Item`](EventData::Item) to a shorter lifetime.
fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short>;

/// Shrink the [`Item`](EventData::Item) to a shorter lifetime [`ReadOnlyItem`](EventData::ReadOnlyItem).
fn shrink_readonly<'long: 'short, 'short>(
item: &'short Self::Item<'long>,
) -> Self::ReadOnlyItem<'short>;
}

// SAFETY: The event type has a component id registered in `init_components`.
unsafe impl<E: Event> EventData for E {
type Item<'trigger> = &'trigger mut E;
type ReadOnlyItem<'trigger> = &'trigger E;

unsafe fn cast<'trigger>(
_world: &World,
_observer_trigger: &ObserverTrigger,
ptr: PtrMut<'trigger>,
) -> Self::Item<'trigger> {
// SAFETY: Caller must ensure that ptr can be safely cast to the Item type.
unsafe { ptr.deref_mut() }
}

fn init_components(world: &mut World, mut ids: impl FnMut(ComponentId)) {
let id = world.init_component::<E>();
ids(id);
}

fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short> {
item
}

fn shrink_readonly<'long: 'short, 'short>(
item: &'short Self::Item<'long>,
) -> Self::ReadOnlyItem<'short> {
item
}
}

/// [`EventData`] that matches any event type and performs no casting. Instead, it returns the pointer as is.
/// This is useful for observers that do not need to access the event data, or need to do so dynamically.
///
/// # Example
///
/// ## Listen to [`OnAdd`] and [`OnRemove`] events in the same observer
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # use bevy_ecs_macros::Component;
/// # use bevy_ecs::observer::DynamicEvent;
/// #
/// /// The component type to listen for on add and remove events.
/// #[derive(Component)]
/// struct A;
///
/// let mut world = World::new();
///
/// // Fetch the component ids for the events
/// let on_add = world.init_component::<OnAdd>();
/// let on_remove = world.init_component::<OnRemove>();
///
/// world.spawn(
/// Observer::new(|mut trigger: Trigger<DynamicEvent, A>| {
/// // This observer function is called for both OnAdd and OnRemove events!
/// let ptr_mut = trigger.event_mut();
/// // do something with the PtrMut, if needed
/// })
/// // Safely register the component ids for the events to the observer
/// .with_event(on_add)
/// .with_event(on_remove),
/// );
///
/// // The observer will be called twice for these two function calls:
/// let entity = world.spawn(A).id();
/// world.despawn(entity);
/// ```
///
/// [`OnAdd`]: crate::event::OnAdd
/// [`OnRemove`]: crate::event::OnRemove
pub struct DynamicEvent;

// SAFETY: Performs no unsafe operations, returns the pointer as is.
unsafe impl EventData for DynamicEvent {
type Item<'trigger> = PtrMut<'trigger>;
type ReadOnlyItem<'trigger> = Ptr<'trigger>;

unsafe fn cast<'trigger>(
_world: &World,
_observer_trigger: &ObserverTrigger,
ptr: PtrMut<'trigger>,
) -> Self::Item<'trigger> {
ptr
}

fn init_components(_world: &mut World, _ids: impl FnMut(ComponentId)) {}

fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short> {
item.reborrow()
}

fn shrink_readonly<'long: 'short, 'short>(
item: &'short Self::Item<'long>,
) -> Self::ReadOnlyItem<'short> {
item.as_ref()
}
}
44 changes: 21 additions & 23 deletions crates/bevy_ecs/src/observer/mod.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
//! Types for creating and storing [`Observer`]s

mod data;
mod entity_observer;
mod runner;
mod trigger_event;

pub use data::*;
pub use runner::*;
pub use trigger_event::*;

use crate::observer::entity_observer::ObservedBy;
use crate::{archetype::ArchetypeFlags, system::IntoObserverSystem, world::*};
use crate::{component::ComponentId, prelude::*, world::DeferredWorld};
use bevy_ptr::Ptr;
use bevy_utils::{EntityHashMap, HashMap};
use std::{fmt::Debug, marker::PhantomData};

/// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the
/// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also
/// contains event propagation information. See [`Trigger::propagate`] for more information.
pub struct Trigger<'w, E, B: Bundle = ()> {
event: &'w mut E,
pub struct Trigger<'w, E: EventData, B: Bundle = ()> {
event: E::Item<'w>,
propagate: &'w mut bool,
trigger: ObserverTrigger,
_marker: PhantomData<B>,
}

impl<'w, E, B: Bundle> Trigger<'w, E, B> {
impl<'w, E: EventData, B: Bundle> Trigger<'w, E, B> {
/// Creates a new trigger for the given event and observer information.
pub fn new(event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self {
pub fn new(event: E::Item<'w>, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self {
Self {
event,
propagate,
Expand All @@ -41,18 +42,13 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> {
}

/// Returns a reference to the triggered event.
pub fn event(&self) -> &E {
self.event
pub fn event(&self) -> E::ReadOnlyItem<'_> {
E::shrink_readonly(&self.event)
}

/// Returns a mutable reference to the triggered event.
pub fn event_mut(&mut self) -> &mut E {
self.event
}

/// Returns a pointer to the triggered event.
pub fn event_ptr(&self) -> Ptr {
Ptr::from(&self.event)
pub fn event_mut(&mut self) -> E::Item<'_> {
E::shrink(&mut self.event)
}

/// Returns the entity that triggered the observer, could be [`Entity::PLACEHOLDER`].
Expand Down Expand Up @@ -84,7 +80,10 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> {
}
}

impl<'w, E: Debug, B: Bundle> Debug for Trigger<'w, E, B> {
impl<'w, E: EventData, B: Bundle> Debug for Trigger<'w, E, B>
where
<E as EventData>::Item<'w>: Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Trigger")
.field("event", &self.event)
Expand Down Expand Up @@ -315,7 +314,7 @@ impl Observers {

impl World {
/// Spawns a "global" [`Observer`] and returns its [`Entity`].
pub fn observe<E: Event, B: Bundle, M>(
pub fn observe<E: EventData, B: Bundle, M>(
&mut self,
system: impl IntoObserverSystem<E, B, M>,
) -> EntityWorldMut {
Expand Down Expand Up @@ -444,7 +443,7 @@ mod tests {

use crate as bevy_ecs;
use crate::observer::{
EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState, OnReplace,
DynamicEvent, EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState, OnReplace,
};
use crate::prelude::*;
use crate::traversal::Traversal;
Expand Down Expand Up @@ -612,16 +611,15 @@ mod tests {
}

#[test]
fn observer_multiple_events() {
fn observer_event_data_dynamic() {
let mut world = World::new();
world.init_resource::<R>();
let on_add = world.init_component::<OnAdd>();
let on_remove = world.init_component::<OnRemove>();
world.spawn(
// SAFETY: OnAdd and OnRemove are both unit types, so this is safe
unsafe {
Observer::new(|_: Trigger<OnAdd, A>, mut res: ResMut<R>| res.0 += 1)
.with_event(on_remove)
},
Observer::new(|_: Trigger<DynamicEvent, A>, mut res: ResMut<R>| res.0 += 1)
.with_event(on_add)
.with_event(on_remove),
);

let entity = world.spawn(A).id();
Expand Down
Loading
Loading