Skip to content

Commit fe852fd

Browse files
authored
Fix boxed labels (#8436)
# Objective Label traits such as `ScheduleLabel` currently have a major footgun: the trait is implemented for `Box<dyn ScheduleLabel>`, but the implementation does not function as one would expect since `Box<T>` is considered to be a distinct type from `T`. This is because the behavior of the `ScheduleLabel` trait is specified mainly through blanket implementations, which prevents `Box<dyn ScheduleLabel>` from being properly special-cased. ## Solution Replace the blanket-implemented behavior with a series of methods defined on `ScheduleLabel`. This allows us to fully special-case `Box<dyn ScheduleLabel>` . --- ## Changelog Fixed a bug where boxed label types (such as `Box<dyn ScheduleLabel>`) behaved incorrectly when compared with concretely-typed labels. ## Migration Guide The `ScheduleLabel` trait has been refactored to no longer depend on the traits `std::any::Any`, `bevy_utils::DynEq`, and `bevy_utils::DynHash`. Any manual implementations will need to implement new trait methods in their stead. ```rust impl ScheduleLabel for MyType { // Before: fn dyn_clone(&self) -> Box<dyn ScheduleLabel> { ... } // After: fn dyn_clone(&self) -> Box<dyn ScheduleLabel> { ... } fn as_dyn_eq(&self) -> &dyn DynEq { self } // No, `mut state: &mut` is not a typo. fn dyn_hash(&self, mut state: &mut dyn Hasher) { self.hash(&mut state); // Hashing the TypeId isn't strictly necessary, but it prevents collisions. TypeId::of::<Self>().hash(&mut state); } } ```
1 parent 5ed6b62 commit fe852fd

File tree

3 files changed

+85
-6
lines changed

3 files changed

+85
-6
lines changed

crates/bevy_ecs/src/schedule/set.rs

+37
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,40 @@ where
172172
SystemTypeSet::new()
173173
}
174174
}
175+
176+
#[cfg(test)]
177+
mod tests {
178+
use crate::{
179+
schedule::{tests::ResMut, Schedule},
180+
system::Resource,
181+
};
182+
183+
use super::*;
184+
185+
#[test]
186+
fn test_boxed_label() {
187+
use crate::{self as bevy_ecs, world::World};
188+
189+
#[derive(Resource)]
190+
struct Flag(bool);
191+
192+
#[derive(ScheduleLabel, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
193+
struct A;
194+
195+
let mut world = World::new();
196+
197+
let mut schedule = Schedule::new();
198+
schedule.add_systems(|mut flag: ResMut<Flag>| flag.0 = true);
199+
world.add_schedule(schedule, A);
200+
201+
let boxed: Box<dyn ScheduleLabel> = Box::new(A);
202+
203+
world.insert_resource(Flag(false));
204+
world.run_schedule_ref(&boxed);
205+
assert!(world.resource::<Flag>().0);
206+
207+
world.insert_resource(Flag(false));
208+
world.run_schedule(boxed);
209+
assert!(world.resource::<Flag>().0);
210+
}
211+
}

crates/bevy_macro_utils/src/lib.rs

+12
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ pub fn ensure_no_collision(value: Ident, haystack: TokenStream) -> Ident {
160160
/// - `input`: The [`syn::DeriveInput`] for struct that is deriving the label trait
161161
/// - `trait_path`: The path [`syn::Path`] to the label trait
162162
pub fn derive_boxed_label(input: syn::DeriveInput, trait_path: &syn::Path) -> TokenStream {
163+
let bevy_utils_path = BevyManifest::default().get_path("bevy_utils");
164+
163165
let ident = input.ident;
164166
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
165167
let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause {
@@ -178,6 +180,16 @@ pub fn derive_boxed_label(input: syn::DeriveInput, trait_path: &syn::Path) -> To
178180
fn dyn_clone(&self) -> std::boxed::Box<dyn #trait_path> {
179181
std::boxed::Box::new(std::clone::Clone::clone(self))
180182
}
183+
184+
fn as_dyn_eq(&self) -> &dyn #bevy_utils_path::label::DynEq {
185+
self
186+
}
187+
188+
fn dyn_hash(&self, mut state: &mut dyn ::std::hash::Hasher) {
189+
let ty_id = #trait_path::inner_type_id(self);
190+
::std::hash::Hash::hash(&ty_id, &mut state);
191+
::std::hash::Hash::hash(self, &mut state);
192+
}
181193
}
182194
})
183195
.into()

crates/bevy_utils/src/label.rs

+36-6
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub trait DynHash: DynEq {
4242

4343
/// Feeds this value into the given [`Hasher`].
4444
///
45-
/// [`Hash`]: std::hash::Hasher
45+
/// [`Hasher`]: std::hash::Hasher
4646
fn dyn_hash(&self, state: &mut dyn Hasher);
4747
}
4848

@@ -72,16 +72,34 @@ where
7272
macro_rules! define_boxed_label {
7373
($label_trait_name:ident) => {
7474
/// A strongly-typed label.
75-
pub trait $label_trait_name:
76-
'static + Send + Sync + ::std::fmt::Debug + ::bevy_utils::label::DynHash
77-
{
78-
#[doc(hidden)]
75+
pub trait $label_trait_name: 'static + Send + Sync + ::std::fmt::Debug {
76+
/// Return's the [TypeId] of this label, or the the ID of the
77+
/// wrappped label type for `Box<dyn
78+
#[doc = stringify!($label_trait_name)]
79+
/// >`
80+
///
81+
/// [TypeId]: std::any::TypeId
82+
fn inner_type_id(&self) -> ::std::any::TypeId {
83+
std::any::TypeId::of::<Self>()
84+
}
85+
86+
/// Clones this `
87+
#[doc = stringify!($label_trait_name)]
88+
/// `
7989
fn dyn_clone(&self) -> Box<dyn $label_trait_name>;
90+
91+
/// Casts this value to a form where it can be compared with other type-erased values.
92+
fn as_dyn_eq(&self) -> &dyn ::bevy_utils::label::DynEq;
93+
94+
/// Feeds this value into the given [`Hasher`].
95+
///
96+
/// [`Hasher`]: std::hash::Hasher
97+
fn dyn_hash(&self, state: &mut dyn ::std::hash::Hasher);
8098
}
8199

82100
impl PartialEq for dyn $label_trait_name {
83101
fn eq(&self, other: &Self) -> bool {
84-
self.dyn_eq(other.as_dyn_eq())
102+
self.as_dyn_eq().dyn_eq(other.as_dyn_eq())
85103
}
86104
}
87105

@@ -100,11 +118,23 @@ macro_rules! define_boxed_label {
100118
}
101119

102120
impl $label_trait_name for Box<dyn $label_trait_name> {
121+
fn inner_type_id(&self) -> ::std::any::TypeId {
122+
(**self).inner_type_id()
123+
}
124+
103125
fn dyn_clone(&self) -> Box<dyn $label_trait_name> {
104126
// Be explicit that we want to use the inner value
105127
// to avoid infinite recursion.
106128
(**self).dyn_clone()
107129
}
130+
131+
fn as_dyn_eq(&self) -> &dyn ::bevy_utils::label::DynEq {
132+
(**self).as_dyn_eq()
133+
}
134+
135+
fn dyn_hash(&self, state: &mut dyn ::std::hash::Hasher) {
136+
(**self).dyn_hash(state);
137+
}
108138
}
109139
};
110140
}

0 commit comments

Comments
 (0)