diff --git a/benches/benches/bevy_ecs/scheduling/schedule.rs b/benches/benches/bevy_ecs/scheduling/schedule.rs index 49de37079b73b..f4674b42c449f 100644 --- a/benches/benches/bevy_ecs/scheduling/schedule.rs +++ b/benches/benches/bevy_ecs/scheduling/schedule.rs @@ -64,14 +64,17 @@ pub fn build_schedule(criterion: &mut Criterion) { // Use multiple different kinds of label to ensure that dynamic dispatch // doesn't somehow get optimized away. #[derive(Debug, Clone, Copy)] - struct NumLabel(usize); + struct NumLabel(u64); #[derive(Debug, Clone, Copy, SystemLabel)] struct DummyLabel; impl SystemLabel for NumLabel { - fn as_str(&self) -> &'static str { - let s = self.0.to_string(); - Box::leak(s.into_boxed_str()) + #[inline] + fn data(&self) -> u64 { + self.0 + } + fn fmt(data: u64, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_tuple("NumLabel").field(&data).finish() } } @@ -82,10 +85,6 @@ pub fn build_schedule(criterion: &mut Criterion) { // Method: generate a set of `graph_size` systems which have a One True Ordering. // Add system to the stage with full constraints. Hopefully this should be maximimally // difficult for bevy to figure out. - // Also, we are performing the `as_label` operation outside of the loop since that - // requires an allocation and a leak. This is not something that would be necessary in a - // real scenario, just a contrivance for the benchmark. - let labels: Vec<_> = (0..1000).map(|i| NumLabel(i).as_label()).collect(); // Benchmark graphs of different sizes. for graph_size in [100, 500, 1000] { @@ -109,12 +108,12 @@ pub fn build_schedule(criterion: &mut Criterion) { // Build a fully-connected dependency graph describing the One True Ordering. // Not particularly realistic but this can be refined later. for i in 0..graph_size { - let mut sys = empty_system.label(labels[i]).before(DummyLabel); + let mut sys = empty_system.label(NumLabel(i)).before(DummyLabel); for a in 0..i { - sys = sys.after(labels[a]); + sys = sys.after(NumLabel(a)); } for b in i + 1..graph_size { - sys = sys.before(labels[b]); + sys = sys.before(NumLabel(b)); } app.add_system(sys); } diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 7d6c027da0cdf..f32af25e5eba7 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -379,9 +379,9 @@ impl App { stage_label: impl StageLabel, system: impl IntoSystemDescriptor, ) -> &mut Self { - use std::any::TypeId; + let stage_label = stage_label.as_label(); assert!( - stage_label.type_id() != TypeId::of::(), + !stage_label.is::(), "add systems to a startup stage using App::add_startup_system_to_stage" ); self.schedule.add_system_to_stage(stage_label, system); @@ -414,9 +414,9 @@ impl App { stage_label: impl StageLabel, system_set: SystemSet, ) -> &mut Self { - use std::any::TypeId; + let stage_label = stage_label.as_label(); assert!( - stage_label.type_id() != TypeId::of::(), + !stage_label.is::(), "add system sets to a startup stage using App::add_startup_system_set_to_stage" ); self.schedule @@ -952,7 +952,7 @@ impl App { pub fn sub_app_mut(&mut self, label: impl AppLabel) -> &mut App { match self.get_sub_app_mut(label) { Ok(app) => app, - Err(label) => panic!("Sub-App with label '{:?}' does not exist", label.as_str()), + Err(label) => panic!("Sub-App with label '{:?}' does not exist", label.as_label()), } } @@ -974,7 +974,7 @@ impl App { pub fn sub_app(&self, label: impl AppLabel) -> &App { match self.get_sub_app(label) { Ok(app) => app, - Err(label) => panic!("Sub-App with label '{:?}' does not exist", label.as_str()), + Err(label) => panic!("Sub-App with label '{:?}' does not exist", label.as_label()), } } diff --git a/crates/bevy_derive/src/lib.rs b/crates/bevy_derive/src/lib.rs index e2088cfe04ec1..102f4cdb17aae 100644 --- a/crates/bevy_derive/src/lib.rs +++ b/crates/bevy_derive/src/lib.rs @@ -82,12 +82,20 @@ pub fn derive_enum_variant_meta(input: TokenStream) -> TokenStream { /// Generates an impl of the `AppLabel` trait. /// -/// This works only for unit structs, or enums with only unit variants. -/// You may force a struct or variant to behave as if it were fieldless with `#[app_label(ignore_fields)]`. +/// For unit structs and enums with only unit variants, a cheap implementation can easily be created. +/// +/// More complex types must be boxed and interned +/// - opt in to this by annotating the entire item with `#[app_label(intern)]`. +/// +/// Alternatively, you may force a struct or variant to behave as if +/// it were fieldless with `#[app_label(ignore_fields)]`. +/// This is especially useful for [`PhantomData`](core::marker::PhantomData) fields. #[proc_macro_derive(AppLabel, attributes(app_label))] pub fn derive_app_label(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as syn::DeriveInput); let mut trait_path = BevyManifest::default().get_path("bevy_app"); trait_path.segments.push(format_ident!("AppLabel").into()); - derive_label(input, &trait_path, "app_label") + let mut id_path = BevyManifest::default().get_path("bevy_app"); + id_path.segments.push(format_ident!("AppLabelId").into()); + derive_label(input, &trait_path, &id_path, "app_label") } diff --git a/crates/bevy_ecs/examples/derive_label.rs b/crates/bevy_ecs/examples/derive_label.rs index 573b42dc8153d..33ebf0e4b9fac 100644 --- a/crates/bevy_ecs/examples/derive_label.rs +++ b/crates/bevy_ecs/examples/derive_label.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use std::{fmt::Debug, hash::Hash, marker::PhantomData}; use bevy_ecs::prelude::*; @@ -18,6 +18,57 @@ fn main() { GenericLabel::::One.as_label(), GenericLabel::::One.as_label(), ); + + assert_eq!(format!("{:?}", UnitLabel.as_label()), "UnitLabel"); + assert_eq!(format!("{:?}", WeirdLabel(1).as_label()), "WeirdLabel"); + assert_eq!(format!("{:?}", WeirdLabel(2).as_label()), "WeirdLabel"); + assert_eq!( + format!("{:?}", GenericLabel::::One.as_label()), + "GenericLabel::One::" + ); + + // Working with labels that need to be heap allocated. + let label = ComplexLabel { + people: vec!["John", "William", "Sharon"], + }; + // Convert to to a LabelId. Its type gets erased. + let id = label.as_label(); + assert_eq!( + format!("{id:?}"), + r#"ComplexLabel { people: ["John", "William", "Sharon"] }"# + ); + // Try to downcast it back to its concrete type. + if let Some(complex_label) = id.downcast::() { + assert_eq!(complex_label.people, vec!["John", "William", "Sharon"]); + } else { + // The downcast will never fail in this example, since the label is always + // created from a value of type `ComplexLabel`. + unreachable!(); + } + + // Generic heap-allocated labels; + let id = ComplexerLabel(1_i128).as_label(); + assert_eq!(format!("{id:?}"), "ComplexerLabel(1)"); + assert!(id.downcast::>().is_none()); + if let Some(label) = id.downcast::>() { + assert_eq!(label.0, 1); + } else { + unreachable!(); + } + + // Different types with the same type constructor. + let id2 = ComplexerLabel(1_u32).as_label(); + // The debug representations are the same... + assert_eq!(format!("{id:?}"), format!("{id2:?}")); + // ...but they do not compare equal... + assert_ne!(id, id2); + // ...nor can you downcast between monomorphizations. + assert!(id2.downcast::>().is_none()); + if let Some(label) = id2.downcast::>() { + assert_eq!(label.0, 1); + } else { + unreachable!(); + } } #[derive(SystemLabel)] @@ -60,3 +111,13 @@ pub struct BadLabel2 { #[system_label(ignore_fields)] x: (), }*/ + +#[derive(Debug, Clone, PartialEq, Eq, Hash, SystemLabel)] +#[system_label(intern)] +pub struct ComplexLabel { + people: Vec<&'static str>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, SystemLabel)] +#[system_label(intern)] +pub struct ComplexerLabel(T); diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 98fbe27200b2e..536fee9191c8b 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -436,8 +436,14 @@ pub fn derive_world_query(input: TokenStream) -> TokenStream { /// Generates an impl of the `SystemLabel` trait. /// -/// This works only for unit structs, or enums with only unit variants. -/// You may force a struct or variant to behave as if it were fieldless with `#[system_label(ignore_fields)]`. +/// For unit structs and enums with only unit variants, a cheap implementation can easily be created. +/// +/// More complex types must be boxed and interned +/// - opt in to this by annotating the entire item with `#[system_label(intern)]`. +/// +/// Alternatively, you may force a struct or variant to behave as if +/// it were fieldless with `#[system_label(ignore_fields)]`. +/// This is especially useful for [`PhantomData`](core::marker::PhantomData) fields. #[proc_macro_derive(SystemLabel, attributes(system_label))] pub fn derive_system_label(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -446,26 +452,44 @@ pub fn derive_system_label(input: TokenStream) -> TokenStream { trait_path .segments .push(format_ident!("SystemLabel").into()); - derive_label(input, &trait_path, "system_label") + let mut id_path = bevy_ecs_path(); + id_path.segments.push(format_ident!("schedule").into()); + id_path.segments.push(format_ident!("SystemLabelId").into()); + derive_label(input, &trait_path, &id_path, "system_label") } /// Generates an impl of the `StageLabel` trait. /// -/// This works only for unit structs, or enums with only unit variants. -/// You may force a struct or variant to behave as if it were fieldless with `#[stage_label(ignore_fields)]`. +/// For unit structs and enums with only unit variants, a cheap implementation can easily be created. +/// +/// More complex types must be boxed and interned +/// - opt in to this by annotating the entire item with `#[stage_label(intern)]`. +/// +/// Alternatively, you may force a struct or variant to behave as if +/// it were fieldless with `#[stage_label(ignore_fields)]`. +/// This is especially useful for [`PhantomData`](core::marker::PhantomData) fields. #[proc_macro_derive(StageLabel, attributes(stage_label))] pub fn derive_stage_label(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let mut trait_path = bevy_ecs_path(); trait_path.segments.push(format_ident!("schedule").into()); trait_path.segments.push(format_ident!("StageLabel").into()); - derive_label(input, &trait_path, "stage_label") + let mut id_path = bevy_ecs_path(); + id_path.segments.push(format_ident!("schedule").into()); + id_path.segments.push(format_ident!("StageLabelId").into()); + derive_label(input, &trait_path, &id_path, "stage_label") } /// Generates an impl of the `AmbiguitySetLabel` trait. /// -/// This works only for unit structs, or enums with only unit variants. -/// You may force a struct or variant to behave as if it were fieldless with `#[ambiguity_set_label(ignore_fields)]`. +/// For unit structs and enums with only unit variants, a cheap implementation can easily be created. +/// +/// More complex types must be boxed and interned +/// - opt in to this by annotating the entire item with `#[ambiguity_set_label(intern)]`. +/// +/// Alternatively, you may force a struct or variant to behave as if +/// it were fieldless with `#[ambiguity_set_label(ignore_fields)]`. +/// This is especially useful for [`PhantomData`](core::marker::PhantomData) fields. #[proc_macro_derive(AmbiguitySetLabel, attributes(ambiguity_set_label))] pub fn derive_ambiguity_set_label(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -474,13 +498,24 @@ pub fn derive_ambiguity_set_label(input: TokenStream) -> TokenStream { trait_path .segments .push(format_ident!("AmbiguitySetLabel").into()); - derive_label(input, &trait_path, "ambiguity_set_label") + let mut id_path = bevy_ecs_path(); + id_path.segments.push(format_ident!("schedule").into()); + id_path + .segments + .push(format_ident!("AmbiguitySetLabelId").into()); + derive_label(input, &trait_path, &id_path, "ambiguity_set_label") } /// Generates an impl of the `RunCriteriaLabel` trait. /// -/// This works only for unit structs, or enums with only unit variants. -/// You may force a struct or variant to behave as if it were fieldless with `#[run_criteria_label(ignore_fields)]`. +/// For unit structs and enums with only unit variants, a cheap implementation can easily be created. +/// +/// More complex types must be boxed and interned +/// - opt in to this by annotating the entire item with `#[run_criteria_label(intern)]`. +/// +/// Alternatively, you may force a struct or variant to behave as if +/// it were fieldless with `#[run_criteria_label(ignore_fields)]`. +/// This is especially useful for [`PhantomData`](core::marker::PhantomData) fields. #[proc_macro_derive(RunCriteriaLabel, attributes(run_criteria_label))] pub fn derive_run_criteria_label(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -489,7 +524,12 @@ pub fn derive_run_criteria_label(input: TokenStream) -> TokenStream { trait_path .segments .push(format_ident!("RunCriteriaLabel").into()); - derive_label(input, &trait_path, "run_criteria_label") + let mut id_path = bevy_ecs_path(); + id_path.segments.push(format_ident!("schedule").into()); + id_path + .segments + .push(format_ident!("RunCriteriaLabelId").into()); + derive_label(input, &trait_path, &id_path, "run_criteria_label") } pub(crate) fn bevy_ecs_path() -> syn::Path { diff --git a/crates/bevy_ecs/src/schedule/label.rs b/crates/bevy_ecs/src/schedule/label.rs index c9bccf8303084..cec282dfb3147 100644 --- a/crates/bevy_ecs/src/schedule/label.rs +++ b/crates/bevy_ecs/src/schedule/label.rs @@ -1,6 +1,7 @@ -pub use bevy_ecs_macros::{AmbiguitySetLabel, RunCriteriaLabel, StageLabel, SystemLabel}; use bevy_utils::define_label; +pub use bevy_ecs_macros::{AmbiguitySetLabel, RunCriteriaLabel, StageLabel, SystemLabel}; + define_label!( /// A strongly-typed class of labels used to identify [`Stage`](crate::schedule::Stage)s. StageLabel, diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 2e1bd167caedf..1c30aae58c5a1 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -211,9 +211,10 @@ impl Schedule { ) } + let stage_label = stage_label.as_label(); let stage = self - .get_stage_mut::(&stage_label) - .unwrap_or_else(move || stage_not_found(&stage_label.as_label())); + .get_stage_mut::(stage_label) + .unwrap_or_else(move || stage_not_found(&stage_label)); stage.add_system(system); self } @@ -281,11 +282,9 @@ impl Schedule { label: impl StageLabel, func: F, ) -> &mut Self { - let stage = self.get_stage_mut::(&label).unwrap_or_else(move || { - panic!( - "stage '{:?}' does not exist or is the wrong type", - label.as_label() - ) + let label = label.as_label(); + let stage = self.get_stage_mut::(label).unwrap_or_else(move || { + panic!("stage '{label:?}' does not exist or is the wrong type") }); func(stage); self @@ -304,9 +303,9 @@ impl Schedule { /// # let mut schedule = Schedule::default(); /// # schedule.add_stage("my_stage", SystemStage::parallel()); /// # - /// let stage = schedule.get_stage::(&"my_stage").unwrap(); + /// let stage = schedule.get_stage::("my_stage").unwrap(); /// ``` - pub fn get_stage(&self, label: &dyn StageLabel) -> Option<&T> { + pub fn get_stage(&self, label: impl StageLabel) -> Option<&T> { self.stages .get(&label.as_label()) .and_then(|stage| stage.downcast_ref::()) @@ -325,9 +324,9 @@ impl Schedule { /// # let mut schedule = Schedule::default(); /// # schedule.add_stage("my_stage", SystemStage::parallel()); /// # - /// let stage = schedule.get_stage_mut::(&"my_stage").unwrap(); + /// let stage = schedule.get_stage_mut::("my_stage").unwrap(); /// ``` - pub fn get_stage_mut(&mut self, label: &dyn StageLabel) -> Option<&mut T> { + pub fn get_stage_mut(&mut self, label: impl StageLabel) -> Option<&mut T> { self.stages .get_mut(&label.as_label()) .and_then(|stage| stage.downcast_mut::()) diff --git a/crates/bevy_ecs/src/schedule/stage.rs b/crates/bevy_ecs/src/schedule/stage.rs index 72e7e39933830..faa43385a4153 100644 --- a/crates/bevy_ecs/src/schedule/stage.rs +++ b/crates/bevy_ecs/src/schedule/stage.rs @@ -1624,12 +1624,12 @@ mod tests { *systems[index_a] .labels() .iter() - .find(|a| a.type_id() == std::any::TypeId::of::<&str>()) + .find(|a| a.is::<&str>()) .unwrap(), *systems[index_b] .labels() .iter() - .find(|a| a.type_id() == std::any::TypeId::of::<&str>()) + .find(|a| a.is::<&str>()) .unwrap(), ) }) diff --git a/crates/bevy_ecs/src/schedule/state.rs b/crates/bevy_ecs/src/schedule/state.rs index c08dfaa886e52..80a70b0a1504a 100644 --- a/crates/bevy_ecs/src/schedule/state.rs +++ b/crates/bevy_ecs/src/schedule/state.rs @@ -1,4 +1,5 @@ use crate::{ + self as bevy_ecs, schedule::{ RunCriteriaDescriptor, RunCriteriaDescriptorCoercion, RunCriteriaLabel, ShouldRun, SystemSet, @@ -6,12 +7,10 @@ use crate::{ system::{In, IntoChainSystem, Local, Res, ResMut, Resource}, }; use std::{ - any::TypeId, fmt::{self, Debug}, hash::Hash, + marker::PhantomData, }; -// Required for derive macros -use crate as bevy_ecs; pub trait StateData: Send + Sync + Clone + Eq + Debug + Hash + 'static {} impl StateData for T where T: Send + Sync + Clone + Eq + Debug + Hash + 'static {} @@ -54,21 +53,12 @@ enum ScheduledOperation { Push(T), } -#[derive(Debug, PartialEq, Eq, Clone, Hash)] -struct DriverLabel(TypeId, &'static str); -impl RunCriteriaLabel for DriverLabel { - fn type_id(&self) -> core::any::TypeId { - self.0 - } - fn as_str(&self) -> &'static str { - self.1 - } -} +#[derive(RunCriteriaLabel)] +#[run_criteria_label(ignore_fields)] +struct DriverLabel(PhantomData T>); -impl DriverLabel { - fn of() -> Self { - Self(TypeId::of::(), std::any::type_name::()) - } +fn driver_label() -> DriverLabel { + DriverLabel(PhantomData) } impl State @@ -80,7 +70,7 @@ where state.stack.last().unwrap() == &pred && state.transition.is_none() }) .chain(should_run_adapter::) - .after(DriverLabel::of::()) + .after(driver_label::()) } pub fn on_inactive_update(pred: T) -> RunCriteriaDescriptor { @@ -96,7 +86,7 @@ where None => *is_inactive, }) .chain(should_run_adapter::) - .after(DriverLabel::of::()) + .after(driver_label::()) } pub fn on_in_stack_update(pred: T) -> RunCriteriaDescriptor { @@ -119,7 +109,7 @@ where None => *is_in_stack, }) .chain(should_run_adapter::) - .after(DriverLabel::of::()) + .after(driver_label::()) } pub fn on_enter(pred: T) -> RunCriteriaDescriptor { @@ -134,7 +124,7 @@ where }) }) .chain(should_run_adapter::) - .after(DriverLabel::of::()) + .after(driver_label::()) } pub fn on_exit(pred: T) -> RunCriteriaDescriptor { @@ -149,7 +139,7 @@ where }) }) .chain(should_run_adapter::) - .after(DriverLabel::of::()) + .after(driver_label::()) } pub fn on_pause(pred: T) -> RunCriteriaDescriptor { @@ -163,7 +153,7 @@ where }) }) .chain(should_run_adapter::) - .after(DriverLabel::of::()) + .after(driver_label::()) } pub fn on_resume(pred: T) -> RunCriteriaDescriptor { @@ -177,7 +167,7 @@ where }) }) .chain(should_run_adapter::) - .after(DriverLabel::of::()) + .after(driver_label::()) } pub fn on_update_set(s: T) -> SystemSet { @@ -209,7 +199,7 @@ where /// Important note: this set must be inserted **before** all other state-dependant sets to work /// properly! pub fn get_driver() -> SystemSet { - SystemSet::default().with_run_criteria(state_cleaner::.label(DriverLabel::of::())) + SystemSet::default().with_run_criteria(state_cleaner::.label(driver_label::())) } pub fn new(initial: T) -> Self { diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 8ca47e0f4c311..7cef707924bd5 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -12,7 +12,11 @@ use crate::{ world::{World, WorldId}, }; use bevy_ecs_macros::all_tuples; -use std::{borrow::Cow, fmt::Debug, marker::PhantomData}; +use std::{ + borrow::Cow, + fmt::{self, Debug}, + marker::PhantomData, +}; /// The metadata of a [`System`]. #[derive(Clone)] @@ -455,13 +459,16 @@ pub struct SystemTypeIdLabel(PhantomData T>); impl SystemLabel for SystemTypeIdLabel { #[inline] - fn as_str(&self) -> &'static str { - std::any::type_name::() + fn data(&self) -> u64 { + 0 + } + fn fmt(_: u64, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", std::any::type_name::()) } } impl Debug for SystemTypeIdLabel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("SystemTypeIdLabel") .field(&std::any::type_name::()) .finish() diff --git a/crates/bevy_macro_utils/Cargo.toml b/crates/bevy_macro_utils/Cargo.toml index 4eb7aaec77381..6871f4f303866 100644 --- a/crates/bevy_macro_utils/Cargo.toml +++ b/crates/bevy_macro_utils/Cargo.toml @@ -12,3 +12,4 @@ keywords = ["bevy"] toml = "0.5.8" syn = "1.0" quote = "1.0" +proc-macro2 = "1" diff --git a/crates/bevy_macro_utils/src/lib.rs b/crates/bevy_macro_utils/src/lib.rs index 0af86d052a946..b29d061b0331d 100644 --- a/crates/bevy_macro_utils/src/lib.rs +++ b/crates/bevy_macro_utils/src/lib.rs @@ -9,7 +9,8 @@ pub use shape::*; pub use symbol::*; use proc_macro::TokenStream; -use quote::{quote, quote_spanned}; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{format_ident, quote}; use std::{env, path::PathBuf}; use syn::spanned::Spanned; use toml::{map::Map, Value}; @@ -105,6 +106,60 @@ impl BevyManifest { } } +/// A set of attributes defined on an item, variant, or field, +/// in the form e.g. `#[system_label(..)]`. +#[derive(Default)] +struct LabelAttrs { + intern: Option, + ignore_fields: Option, +} + +impl LabelAttrs { + /// Parses a list of attributes. + /// + /// Ignores any that aren't of the form `#[my_label(..)]`. + /// Returns `Ok` if the iterator is empty. + pub fn new<'a>( + iter: impl IntoIterator, + attr_name: &str, + ) -> syn::Result { + let mut this = Self::default(); + for attr in iter { + // If it's not of the form `#[my_label(..)]`, skip it. + if attr.path.get_ident().as_ref().unwrap() != &attr_name { + continue; + } + + // Parse the argument/s to the attribute. + attr.parse_args_with(|input: syn::parse::ParseStream| { + loop { + syn::custom_keyword!(intern); + syn::custom_keyword!(ignore_fields); + + let next = input.lookahead1(); + if next.peek(intern) { + let kw: intern = input.parse()?; + this.intern = Some(kw.span); + } else if next.peek(ignore_fields) { + let kw: ignore_fields = input.parse()?; + this.ignore_fields = Some(kw.span); + } else { + return Err(next.error()); + } + + if input.is_empty() { + break; + } + let _comma: syn::Token![,] = input.parse()?; + } + Ok(()) + })?; + } + + Ok(this) + } +} + /// Derive a label trait /// /// # Args @@ -114,25 +169,25 @@ impl BevyManifest { pub fn derive_label( input: syn::DeriveInput, trait_path: &syn::Path, + id_path: &syn::Path, attr_name: &str, ) -> TokenStream { - // return true if the variant specified is an `ignore_fields` attribute - fn is_ignore(attr: &syn::Attribute, attr_name: &str) -> bool { - if attr.path.get_ident().as_ref().unwrap() != &attr_name { - return false; - } + let item_attrs = match LabelAttrs::new(&input.attrs, attr_name) { + Ok(a) => a, + Err(e) => return e.into_compile_error().into(), + }; - syn::custom_keyword!(ignore_fields); - attr.parse_args_with(|input: syn::parse::ParseStream| { - let ignore = input.parse::>()?.is_some(); - Ok(ignore) - }) - .unwrap() + // We use entirely different derives for interned and named labels. + if item_attrs.intern.is_some() { + derive_interned_label(input, trait_path, id_path, attr_name) + } else { + derive_named_label(input, &item_attrs, trait_path, attr_name) } + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} - let ident = input.ident.clone(); - - let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); +fn with_static_bound(where_clause: Option<&syn::WhereClause>) -> syn::WhereClause { let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause { where_token: Default::default(), predicates: Default::default(), @@ -140,79 +195,216 @@ pub fn derive_label( where_clause .predicates .push(syn::parse2(quote! { Self: 'static }).unwrap()); + where_clause +} - let as_str = match input.data { +fn derive_named_label( + input: syn::DeriveInput, + item_attrs: &LabelAttrs, + trait_path: &syn::Path, + attr_name: &str, +) -> syn::Result { + let ident = input.ident.clone(); + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let where_clause = with_static_bound(where_clause); + + let (data, mut fmt) = match input.data { syn::Data::Struct(d) => { + let all_field_attrs = + LabelAttrs::new(d.fields.iter().flat_map(|f| &f.attrs), attr_name)?; // see if the user tried to ignore fields incorrectly - if let Some(attr) = d - .fields - .iter() - .flat_map(|f| &f.attrs) - .find(|a| is_ignore(a, attr_name)) - { - let err_msg = format!("`#[{attr_name}(ignore_fields)]` cannot be applied to fields individually: add it to the struct declaration"); - return quote_spanned! { - attr.span() => compile_error!(#err_msg); - } - .into(); + if let Some(attr) = all_field_attrs.ignore_fields { + let err_msg = format!( + r#"`#[{attr_name}(ignore_fields)]` cannot be applied to fields individually: + try adding it to the struct declaration"# + ); + return Err(syn::Error::new(attr, err_msg)); + } + if let Some(attr) = all_field_attrs.intern { + let err_msg = format!( + r#"`#[{attr_name}(intern)]` cannot be applied to fields individually: + try adding it to the struct declaration"# + ); + return Err(syn::Error::new(attr, err_msg)); } // Structs must either be fieldless, or explicitly ignore the fields. - let ignore_fields = input.attrs.iter().any(|a| is_ignore(a, attr_name)); - if matches!(d.fields, syn::Fields::Unit) || ignore_fields { + let ignore_fields = item_attrs.ignore_fields.is_some(); + if d.fields.is_empty() || ignore_fields { let lit = ident.to_string(); - quote! { #lit } + let data = quote! { 0 }; + let as_str = quote! { write!(f, #lit) }; + (data, as_str) } else { - let err_msg = format!("Labels cannot contain data, unless explicitly ignored with `#[{attr_name}(ignore_fields)]`"); - return quote_spanned! { - d.fields.span() => compile_error!(#err_msg); - } - .into(); + let err_msg = format!( + r#"Simple labels cannot contain data, unless the whole type is boxed + by marking the type with `#[{attr_name}(intern)]`. + Alternatively, you can make this label behave as if it were fieldless with `#[{attr_name}(ignore_fields)]`."# + ); + return Err(syn::Error::new(d.fields.span(), err_msg)); } } syn::Data::Enum(d) => { // check if the user put #[label(ignore_fields)] in the wrong place - if let Some(attr) = input.attrs.iter().find(|a| is_ignore(a, attr_name)) { + if let Some(attr) = item_attrs.ignore_fields { let err_msg = format!("`#[{attr_name}(ignore_fields)]` can only be applied to enum variants or struct declarations"); - return quote_spanned! { - attr.span() => compile_error!(#err_msg); - } - .into(); + return Err(syn::Error::new(attr, err_msg)); } - let arms = d.variants.iter().map(|v| { + + let mut data_arms = Vec::with_capacity(d.variants.len()); + let mut fmt_arms = Vec::with_capacity(d.variants.len()); + + for (i, v) in d.variants.iter().enumerate() { + let v_attrs = LabelAttrs::new(&v.attrs, attr_name)?; + // Check if they used the intern attribute wrong. + if let Some(attr) = v_attrs.intern { + let err_msg = format!("`#[{attr_name}(intern)]` cannot be applied to individual variants; try applying it to the whole type"); + return Err(syn::Error::new(attr, err_msg)); + } // Variants must either be fieldless, or explicitly ignore the fields. - let ignore_fields = v.attrs.iter().any(|a| is_ignore(a, attr_name)); - if matches!(v.fields, syn::Fields::Unit) | ignore_fields { + let ignore_fields = v_attrs.ignore_fields.is_some(); + if v.fields.is_empty() || ignore_fields { let mut path = syn::Path::from(ident.clone()); path.segments.push(v.ident.clone().into()); + + let i = i as u64; + data_arms.push(quote! { #path { .. } => #i }); + let lit = format!("{ident}::{}", v.ident.clone()); - quote! { #path { .. } => #lit } + fmt_arms.push(quote! { #i => { write!(f, #lit) } }); } else { - let err_msg = format!("Label variants cannot contain data, unless explicitly ignored with `#[{attr_name}(ignore_fields)]`"); - quote_spanned! { - v.fields.span() => _ => { compile_error!(#err_msg); } - } + let err_msg = format!( + r#"Simple labels only allow unit variants -- more complex types must be boxed + by marking the whole type with `#[{attr_name}(intern)]`. + Alternatively, you can make the variant act fieldless using `#[{attr_name}(ignore_fields)]`."# + ); + return Err(syn::Error::new(v.fields.span(), err_msg)); } - }); - quote! { + } + + let data = quote! { match self { - #(#arms),* + #(#data_arms),* } - } + }; + let fmt = quote! { + match data { + #(#fmt_arms),* + _ => ::std::unreachable!(), + } + }; + (data, fmt) } syn::Data::Union(_) => { - return quote_spanned! { - input.span() => compile_error!("Unions cannot be used as labels."); + let err_msg = format!( + "Unions cannot be used as labels, unless marked with `#[{attr_name}(intern)]`." + ); + return Err(syn::Error::new(input.span(), err_msg)); + } + }; + + // Formatting for generics + let mut ty_args = input.generics.params.iter().filter_map(|p| match p { + syn::GenericParam::Type(ty) => Some({ + let ty = &ty.ident; + quote! { ::std::any::type_name::<#ty>() } + }), + _ => None, + }); + if let Some(first_arg) = ty_args.next() { + // Note: We're doing this manually instead of using magic `syn` methods, + // because those methods insert ugly whitespace everywhere. + // Those are for codegen, not user-facing formatting. + fmt = quote! { + ( #fmt )?; + write!(f, "::<")?; + write!(f, "{}", #first_arg)?; + #( write!(f, ", {}", #ty_args)?; )* + write!(f, ">") + } + } + + Ok(quote! { + impl #impl_generics #trait_path for #ident #ty_generics #where_clause { + #[inline] + fn data(&self) -> u64 { + #data } - .into(); + fn fmt(data: u64, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + #fmt + } + } + }) +} + +fn derive_interned_label( + input: syn::DeriveInput, + trait_path: &syn::Path, + id_path: &syn::Path, + _attr_name: &str, +) -> syn::Result { + let manifest = BevyManifest::default(); + + let ident = input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let mut where_clause = with_static_bound(where_clause); + where_clause.predicates.push( + syn::parse2(quote! { + Self: ::std::clone::Clone + ::std::cmp::Eq + ::std::hash::Hash + ::std::fmt::Debug + + ::std::marker::Send + ::std::marker::Sync + 'static + }) + .unwrap(), + ); + + let is_generic = !input.generics.params.is_empty(); + + let interner_type_path = { + let mut path = manifest.get_path("bevy_utils"); + // If the type is generic, we have to store all monomorphizations + // in the same global due to Rust restrictions. + if is_generic { + path.segments.push(format_ident!("AnyInterner").into()); + } else { + path.segments.push(format_ident!("Interner").into()); } + path + }; + let interner_type_expr = if is_generic { + quote! { #interner_type_path } + } else { + quote! { #interner_type_path <#ident> } + }; + let guard_type_path = { + let mut path = manifest.get_path("bevy_utils"); + path.segments.push(format_ident!("InternGuard").into()); + path + }; + let interner_ident = format_ident!("{}_INTERN", ident.to_string().to_uppercase()); + let downcast_trait_path = { + let mut path = manifest.get_path("bevy_utils"); + path.segments.push(format_ident!("label").into()); + path.segments.push(format_ident!("LabelDowncast").into()); + path }; - (quote! { + Ok(quote! { + static #interner_ident : #interner_type_expr = #interner_type_path::new(); + impl #impl_generics #trait_path for #ident #ty_generics #where_clause { - fn as_str(&self) -> &'static str { - #as_str + #[inline] + fn data(&self) -> u64 { + #interner_ident .intern(self) as u64 + } + fn fmt(idx: u64, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + let val: #guard_type_path = #interner_ident .get(idx as usize).ok_or(::std::fmt::Error)?; + ::std::fmt::Debug::fmt(&*val, f) + } + } + + impl #impl_generics #downcast_trait_path <#id_path> for #ident #ty_generics #where_clause { + type Output = #guard_type_path <'static, Self>; + fn downcast_from(idx: u64) -> Option { + #interner_ident .get(idx as usize) } } }) - .into() } diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 5efbb5ba9c03a..fbe5f14923970 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -249,7 +249,7 @@ impl Plugin for RenderPlugin { // prepare let prepare = render_app .schedule - .get_stage_mut::(&RenderStage::Prepare) + .get_stage_mut::(RenderStage::Prepare) .unwrap(); prepare.run(&mut render_app.world); } @@ -262,7 +262,7 @@ impl Plugin for RenderPlugin { // queue let queue = render_app .schedule - .get_stage_mut::(&RenderStage::Queue) + .get_stage_mut::(RenderStage::Queue) .unwrap(); queue.run(&mut render_app.world); } @@ -275,7 +275,7 @@ impl Plugin for RenderPlugin { // phase sort let phase_sort = render_app .schedule - .get_stage_mut::(&RenderStage::PhaseSort) + .get_stage_mut::(RenderStage::PhaseSort) .unwrap(); phase_sort.run(&mut render_app.world); } @@ -288,7 +288,7 @@ impl Plugin for RenderPlugin { // render let render = render_app .schedule - .get_stage_mut::(&RenderStage::Render) + .get_stage_mut::(RenderStage::Render) .unwrap(); render.run(&mut render_app.world); } @@ -301,7 +301,7 @@ impl Plugin for RenderPlugin { // cleanup let cleanup = render_app .schedule - .get_stage_mut::(&RenderStage::Cleanup) + .get_stage_mut::(RenderStage::Cleanup) .unwrap(); cleanup.run(&mut render_app.world); } @@ -335,7 +335,7 @@ struct ScratchMainWorld(World); fn extract(app_world: &mut World, render_app: &mut App) { let extract = render_app .schedule - .get_stage_mut::(&RenderStage::Extract) + .get_stage_mut::(RenderStage::Extract) .unwrap(); // temporarily add the app world to the render world as a resource diff --git a/crates/bevy_utils/Cargo.toml b/crates/bevy_utils/Cargo.toml index 49c67fc021f5c..5c6d27535c41c 100644 --- a/crates/bevy_utils/Cargo.toml +++ b/crates/bevy_utils/Cargo.toml @@ -14,6 +14,8 @@ tracing = { version = "0.1", default-features = false, features = ["std"] } instant = { version = "0.1", features = ["wasm-bindgen"] } uuid = { version = "1.1", features = ["v4", "serde"] } hashbrown = { version = "0.12", features = ["serde"] } +indexmap = "1.9" +parking_lot = "0.12.1" [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = {version = "0.2.0", features = ["js"]} diff --git a/crates/bevy_utils/src/intern.rs b/crates/bevy_utils/src/intern.rs new file mode 100644 index 0000000000000..60225c8083dda --- /dev/null +++ b/crates/bevy_utils/src/intern.rs @@ -0,0 +1,135 @@ +use std::{any::TypeId, hash::Hash}; + +use parking_lot::{RwLock, RwLockReadGuard}; + +use crate::{FixedState, StableHashMap}; + +type IndexSet = indexmap::IndexSet; + +/// A data structure used to intern a set of values of a specific type. +/// To store multiple distinct types, or generic types, try [`Labels`]. +pub struct Interner( + // The `IndexSet` is a hash set that preserves ordering as long as + // you don't remove items (which we don't). + // This allows us to have O(~1) hashing and map each entry to a stable index. + RwLock>, +); + +/// The type returned from [`Labels::get`](Labels#method.get). +/// +/// Will hold a lock on the label interner until this value gets dropped. +pub type InternGuard<'a, L> = parking_lot::MappedRwLockReadGuard<'a, L>; + +impl Interner { + pub const fn new() -> Self { + Self(RwLock::new(IndexSet::with_hasher(FixedState))) + } + + /// Interns a value, if it was not already interned in this set. + /// + /// Returns an integer used to refer to the value later on. + pub fn intern(&self, val: &T) -> usize { + use parking_lot::RwLockUpgradableReadGuard as Guard; + + // Acquire an upgradeable read lock, since we might not have to do any writing. + let set = self.0.upgradable_read(); + + // If the value is already interned, return its index. + if let Some(idx) = set.get_index_of(val) { + return idx; + } + + // Upgrade to a mutable lock. + let mut set = Guard::upgrade(set); + let (idx, _) = set.insert_full(val.clone()); + idx + } + + /// Gets a reference to the label with specified index. + pub fn get(&self, idx: usize) -> Option> { + RwLockReadGuard::try_map(self.0.read(), |set| set.get_index(idx)).ok() + } +} + +struct TypeMap(StableHashMap>); + +impl TypeMap { + pub const fn new() -> Self { + Self(StableHashMap::with_hasher(FixedState)) + } + + pub fn insert(&mut self, val: T) -> Option { + self.0.insert(TypeId::of::(), Box::new(val)) + } + pub fn get(&self) -> Option<&T> { + let val = self.0.get(&TypeId::of::())?.as_ref(); + // SAFETY: `val` was keyed with the TypeId of `T`, so we can cast it to `T`. + Some(unsafe { &*(val as *const _ as *const T) }) + } + pub fn get_mut(&mut self) -> Option<&mut T> { + let val = self.0.get_mut(&TypeId::of::())?.as_mut(); + // SAFETY: `val` was keyed with the TypeId of `T`, so we can cast it to `T`. + Some(unsafe { &mut *(val as *mut _ as *mut T) }) + } +} + +/// Data structure used to intern a set of values of any given type. +/// +/// If you just need to store a single concrete type, [`Interner`] is more efficient. +pub struct AnyInterner( + // This type-map stores instances of `IndexSet`, for any `T`. + RwLock, +); + +impl AnyInterner { + pub const fn new() -> Self { + Self(RwLock::new(TypeMap::new())) + } + + /// Interns a value, if it was not already interned in this set. + /// + /// Returns an integer used to refer to the value later on. + pub fn intern(&self, val: &L) -> usize + where + L: Clone + Hash + Eq + Send + Sync + 'static, + { + use parking_lot::RwLockUpgradableReadGuard as Guard; + + // Acquire an upgradeable read lock, since we might not have to do any writing. + let type_map = self.0.upgradable_read(); + + if let Some(set) = type_map.get::>() { + // If the value is already interned, return its index. + if let Some(idx) = set.get_index_of(val) { + return idx; + } + + // Get mutable access to the interner. + let mut type_map = Guard::upgrade(type_map); + let set = type_map.get_mut::>().unwrap(); + + // Insert a clone of the value and return its index. + let (idx, _) = set.insert_full(val.clone()); + idx + } else { + let mut type_map = Guard::upgrade(type_map); + + // Initialize the `L` interner for the first time, including `val` in it. + let mut set = IndexSet::default(); + let (idx, _) = set.insert_full(val.clone()); + let old = type_map.insert(set); + // We already checked that there is no set for type `L`, + // so let's avoid generating useless drop code for the "previous" entry. + std::mem::forget(old); + idx + } + } + + /// Gets a reference to the label with specified index. + pub fn get(&self, key: usize) -> Option> { + RwLockReadGuard::try_map(self.0.read(), |type_map| { + type_map.get::>()?.get_index(key) + }) + .ok() + } +} diff --git a/crates/bevy_utils/src/label.rs b/crates/bevy_utils/src/label.rs index 835656569c1fe..a2463f71fd512 100644 --- a/crates/bevy_utils/src/label.rs +++ b/crates/bevy_utils/src/label.rs @@ -3,8 +3,11 @@ use std::{ any::Any, hash::{Hash, Hasher}, + ops::Deref, }; +use crate::Interner; + pub trait DynEq: Any { fn as_any(&self) -> &dyn Any; @@ -47,6 +50,24 @@ where } } +/// Trait for implementors of `*Label` types that support downcasting. +pub trait LabelDowncast { + /// The type returned from [`downcast_from`](#method.downcast_from). + type Output: Deref; + /// Attempts to downcast a label to type `Self`. Returns a reference-like type. + fn downcast_from(data: u64) -> Option; +} + +#[doc(hidden)] +pub struct VTable { + // FIXME: When const TypeId stabilizes, inline the type instead of using a fn pointer for indirection. + pub ty: fn() -> ::std::any::TypeId, + pub fmt: fn(u64, &mut ::std::fmt::Formatter) -> ::std::fmt::Result, +} + +#[doc(hidden)] +pub static STR_INTERN: Interner<&str> = Interner::new(); + /// Macro to define a new label trait /// /// # Example @@ -70,49 +91,113 @@ macro_rules! define_label { $id_name:ident $(,)? ) => { $(#[$id_attr])* - #[derive(Clone, Copy, PartialEq, Eq, Hash)] - pub struct $id_name(::core::any::TypeId, &'static str); + #[derive(Clone, Copy)] + pub struct $id_name { + data: u64, + vtable: &'static $crate::label::VTable, + } - impl ::core::fmt::Debug for $id_name { - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - write!(f, "{}", self.1) + impl ::std::fmt::Debug for $id_name { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + let data = self.data(); + (self.vtable.fmt)(data, f) } } $(#[$label_attr])* pub trait $label_name: 'static { /// Converts this type into an opaque, strongly-typed label. + #[inline] fn as_label(&self) -> $id_name { - let id = self.type_id(); - let label = self.as_str(); - $id_name(id, label) - } - /// Returns the [`TypeId`] used to differentiate labels. - fn type_id(&self) -> ::core::any::TypeId { - ::core::any::TypeId::of::() + // This is just machinery that lets us store the TypeId and formatter fn in the same static reference. + struct VTables(::std::marker::PhantomData); + impl VTables { + const VTABLE: $crate::label::VTable = $crate::label::VTable { + ty: || ::std::any::TypeId::of::(), + fmt: ::fmt, + }; + } + + let data = self.data(); + $id_name { data, vtable: &VTables::::VTABLE } } - /// Returns the representation of this label as a string literal. + /// Returns a number used to distinguish different labels of the same type. + fn data(&self) -> u64; + /// Writes debug info for a label of the current type. + /// * `data`: the result of calling [`data()`](#method.data) on an instance of this type. /// - /// In cases where you absolutely need a label to be determined at runtime, - /// you can use [`Box::leak`] to get a `'static` reference. - fn as_str(&self) -> &'static str; + /// You should not call this method directly, as it may panic for some types; + /// use [`as_label`](#method.as_label) instead. + fn fmt(data: u64, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result; } impl $label_name for $id_name { + #[inline] fn as_label(&self) -> Self { *self } - fn type_id(&self) -> ::core::any::TypeId { - self.0 + #[inline] + fn data(&self) -> u64 { + self.data + } + #[track_caller] + fn fmt(data: u64, f: &mut ::std::fmt::Formatter) -> std::fmt::Result { + ::std::unimplemented!("do not call `Label::fmt` directly -- use the result of `as_label()` for formatting instead") + } + } + + impl PartialEq for $id_name { + #[inline] + fn eq(&self, rhs: &Self) -> bool { + self.data() == rhs.data() && self.type_id() == rhs.type_id() + } + } + impl Eq for $id_name {} + + + impl std::hash::Hash for $id_name { + fn hash(&self, state: &mut H) { + self.type_id().hash(state); + self.data().hash(state); + } + } + + impl $id_name { + /// Returns the [`TypeId`] of the label from which this ID was constructed. + /// + /// [`TypeId`]: ::std::any::TypeId + #[inline] + pub fn type_id(self) -> ::std::any::TypeId { + (self.vtable.ty)() } - fn as_str(&self) -> &'static str { - self.1 + /// Returns true if this label was constructed from an instance of type `L`. + pub fn is(self) -> bool { + self.type_id() == ::std::any::TypeId::of::() + } + /// Attempts to downcast this label to type `L`. + /// + /// This method is not available for all types of labels. + pub fn downcast(self) -> Option> + where + L: $label_name + $crate::label::LabelDowncast<$id_name> + { + if self.is::() { + L::downcast_from(self.data()) + } else { + None + } } } impl $label_name for &'static str { - fn as_str(&self) -> Self { - self + fn data(&self) -> u64 { + $crate::label::STR_INTERN.intern(self) as u64 + } + fn fmt(idx: u64, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let s = $crate::label::STR_INTERN + .get(idx as usize) + .ok_or(::std::fmt::Error)?; + write!(f, "{s}") } } }; diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index 2b09fdbf0f3b2..1c5b2d2e6b021 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -9,12 +9,14 @@ pub use short_names::get_short_name; mod default; mod float_ord; +mod intern; pub use ahash::AHasher; pub use default::default; pub use float_ord::*; pub use hashbrown; pub use instant::{Duration, Instant}; +pub use intern::*; pub use tracing; pub use uuid::Uuid;