From 4be8e497ca2130561faa0a8e70644e8e36b7adbf Mon Sep 17 00:00:00 2001 From: Chris Russell <8494645+chescock@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:24:52 -0400 Subject: [PATCH] SystemParamBuilder - Allow deriving a SystemParamBuilder struct when deriving SystemParam. (#14818) # Objective Allow `SystemParamBuilder` implementations for custom system parameters created using `#[derive(SystemParam)]`. ## Solution Extend the derive macro to accept a `#[system_param(builder)]` attribute. When present, emit a builder type with a field corresponding to each field of the param. ## Example ```rust #[derive(SystemParam)] #[system_param(builder)] struct CustomParam<'w, 's> { query: Query<'w, 's, ()>, local: Local<'s, usize>, } let system = (CustomParamBuilder { local: LocalBuilder(100), query: QueryParamBuilder::new(|builder| { builder.with::(); }), },) .build_state(&mut world) .build_system(|param: CustomParam| *param.local + param.query.iter().count()); ``` --- crates/bevy_ecs/macros/src/lib.rs | 54 ++++++++++++++++++++++ crates/bevy_ecs/src/system/builder.rs | 27 +++++++++++ crates/bevy_ecs/src/system/system_param.rs | 49 ++++++++++++++++++++ 3 files changed, 130 insertions(+) diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index ed5cc22c24..f33659ca14 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -423,6 +423,56 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { let state_struct_visibility = &ast.vis; let state_struct_name = ensure_no_collision(format_ident!("FetchState"), token_stream); + let mut builder_name = None; + for meta in ast + .attrs + .iter() + .filter(|a| a.path().is_ident("system_param")) + { + if let Err(e) = meta.parse_nested_meta(|nested| { + if nested.path.is_ident("builder") { + builder_name = Some(format_ident!("{struct_name}Builder")); + Ok(()) + } else { + Err(nested.error("Unsupported attribute")) + } + }) { + return e.into_compile_error().into(); + } + } + + let builder = builder_name.map(|builder_name| { + let builder_type_parameters: Vec<_> = (0..fields.len()).map(|i| format_ident!("B{i}")).collect(); + let builder_doc_comment = format!("A [`SystemParamBuilder`] for a [`{struct_name}`]."); + let builder_struct = quote! { + #[doc = #builder_doc_comment] + struct #builder_name<#(#builder_type_parameters,)*> { + #(#fields: #builder_type_parameters,)* + } + }; + let lifetimes: Vec<_> = generics.lifetimes().collect(); + let generic_struct = quote!{ #struct_name <#(#lifetimes,)* #punctuated_generic_idents> }; + let builder_impl = quote!{ + // SAFETY: This delegates to the `SystemParamBuilder` for tuples. + unsafe impl< + #(#lifetimes,)* + #(#builder_type_parameters: #path::system::SystemParamBuilder<#field_types>,)* + #punctuated_generics + > #path::system::SystemParamBuilder<#generic_struct> for #builder_name<#(#builder_type_parameters,)*> + #where_clause + { + fn build(self, world: &mut #path::world::World, meta: &mut #path::system::SystemMeta) -> <#generic_struct as #path::system::SystemParam>::State { + let #builder_name { #(#fields: #field_locals,)* } = self; + #state_struct_name { + state: #path::system::SystemParamBuilder::build((#(#tuple_patterns,)*), world, meta) + } + } + } + }; + (builder_struct, builder_impl) + }); + let (builder_struct, builder_impl) = builder.unzip(); + TokenStream::from(quote! { // We define the FetchState struct in an anonymous scope to avoid polluting the user namespace. // The struct can still be accessed via SystemParam::State, e.g. EventReaderState can be accessed via @@ -479,7 +529,11 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { // Safety: Each field is `ReadOnlySystemParam`, so this can only read from the `World` unsafe impl<'w, 's, #punctuated_generics> #path::system::ReadOnlySystemParam for #struct_name #ty_generics #read_only_where_clause {} + + #builder_impl }; + + #builder_struct }) } diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index cd532776e5..ea5b070010 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -556,4 +556,31 @@ mod tests { let result = world.run_system_once(system); assert_eq!(result, 4); } + + #[derive(SystemParam)] + #[system_param(builder)] + struct CustomParam<'w, 's> { + query: Query<'w, 's, ()>, + local: Local<'s, usize>, + } + + #[test] + fn custom_param_builder() { + let mut world = World::new(); + + world.spawn(A); + world.spawn_empty(); + + let system = (CustomParamBuilder { + local: LocalBuilder(100), + query: QueryParamBuilder::new(|builder| { + builder.with::(); + }), + },) + .build_state(&mut world) + .build_system(|param: CustomParam| *param.local + param.query.iter().count()); + + let result = world.run_system_once(system); + assert_eq!(result, 101); + } } diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 10e7fd4ec3..6b604c54c2 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -121,6 +121,55 @@ use std::{ /// This will most commonly occur when working with `SystemParam`s generically, as the requirement /// has not been proven to the compiler. /// +/// ## Builders +/// +/// If you want to use a [`SystemParamBuilder`](crate::system::SystemParamBuilder) with a derived [`SystemParam`] implementation, +/// add a `#[system_param(builder)]` attribute to the struct. +/// This will generate a builder struct whose name is the param struct suffixed with `Builder`. +/// The builder will not be `pub`, so you may want to expose a method that returns an `impl SystemParamBuilder`. +/// +/// ``` +/// mod custom_param { +/// # use bevy_ecs::{ +/// # prelude::*, +/// # system::{LocalBuilder, QueryParamBuilder, SystemParam}, +/// # }; +/// # +/// #[derive(SystemParam)] +/// #[system_param(builder)] +/// pub struct CustomParam<'w, 's> { +/// query: Query<'w, 's, ()>, +/// local: Local<'s, usize>, +/// } +/// +/// impl<'w, 's> CustomParam<'w, 's> { +/// pub fn builder( +/// local: usize, +/// query: impl FnOnce(&mut QueryBuilder<()>), +/// ) -> impl SystemParamBuilder { +/// CustomParamBuilder { +/// local: LocalBuilder(local), +/// query: QueryParamBuilder::new(query), +/// } +/// } +/// } +/// } +/// +/// use custom_param::CustomParam; +/// +/// # use bevy_ecs::prelude::*; +/// # #[derive(Component)] +/// # struct A; +/// # +/// # let mut world = World::new(); +/// # +/// let system = (CustomParam::builder(100, |builder| { +/// builder.with::(); +/// }),) +/// .build_state(&mut world) +/// .build_system(|param: CustomParam| {}); +/// ``` +/// /// # Safety /// /// The implementor must ensure the following is true.