SystemParamBuilder - Support dynamic system parameters (#14817)

# Objective

Support building systems with parameters whose types can be determined
at runtime.

## Solution

Create a `DynSystemParam` type that can be built using a
`SystemParamBuilder` of any type and then downcast to the appropriate
type dynamically.

## Example

```rust
let system = (
    DynParamBuilder::new(LocalBuilder(3_usize)),
    DynParamBuilder:🆕:<Query<()>>(QueryParamBuilder::new(|builder| {
        builder.with::<A>();
    })),
    DynParamBuilder:🆕:<&Entities>(ParamBuilder),
)
    .build_state(&mut world)
    .build_system(
        |mut p0: DynSystemParam, mut p1: DynSystemParam, mut p2: DynSystemParam| {
            let local = p0.downcast_mut::<Local<usize>>().unwrap();
            let query_count = p1.downcast_mut::<Query<()>>().unwrap();
            let entities = p2.downcast_mut::<&Entities>().unwrap();
        },
    );
```

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Periwink <charlesbour@gmail.com>
This commit is contained in:
Chris Russell 2024-08-25 10:23:44 -04:00 committed by GitHub
parent 94d40d206e
commit 335f2903d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 307 additions and 1 deletions

View file

@ -4,7 +4,7 @@ use crate::{
prelude::QueryBuilder,
query::{QueryData, QueryFilter, QueryState},
system::{
system_param::{Local, ParamSet, SystemParam},
system_param::{DynSystemParam, DynSystemParamState, Local, ParamSet, SystemParam},
Query, SystemMeta,
},
world::{FromWorld, World},
@ -251,6 +251,34 @@ macro_rules! impl_param_set_builder_tuple {
all_tuples!(impl_param_set_builder_tuple, 1, 8, P, B, meta);
/// A [`SystemParamBuilder`] for a [`DynSystemParam`].
pub struct DynParamBuilder<'a>(
Box<dyn FnOnce(&mut World, &mut SystemMeta) -> DynSystemParamState + 'a>,
);
impl<'a> DynParamBuilder<'a> {
/// Creates a new [`DynParamBuilder`] by wrapping a [`SystemParamBuilder`] of any type.
/// The built [`DynSystemParam`] can be downcast to `T`.
pub fn new<T: SystemParam + 'static>(builder: impl SystemParamBuilder<T> + 'a) -> Self {
Self(Box::new(|world, meta| {
DynSystemParamState::new::<T>(builder.build(world, meta))
}))
}
}
// SAFETY: `DynSystemParam::get_param` will call `get_param` on the boxed `DynSystemParamState`,
// and the boxed builder was a valid implementation of `SystemParamBuilder` for that type.
// The resulting `DynSystemParam` can only perform access by downcasting to that param type.
unsafe impl<'a, 'w, 's> SystemParamBuilder<DynSystemParam<'w, 's>> for DynParamBuilder<'a> {
fn build(
self,
world: &mut World,
meta: &mut SystemMeta,
) -> <DynSystemParam<'w, 's> as SystemParam>::State {
(self.0)(world, meta)
}
}
/// A [`SystemParamBuilder`] for a [`Local`].
/// The provided value will be used as the initial value of the `Local`.
pub struct LocalBuilder<T>(pub T);
@ -271,6 +299,7 @@ unsafe impl<'s, T: FromWorld + Send + 'static> SystemParamBuilder<Local<'s, T>>
#[cfg(test)]
mod tests {
use crate as bevy_ecs;
use crate::entity::Entities;
use crate::prelude::{Component, Query};
use crate::system::{Local, RunSystemOnce};
@ -382,4 +411,33 @@ mod tests {
let result = world.run_system_once(system);
assert_eq!(result, 5);
}
#[test]
fn dyn_builder() {
let mut world = World::new();
world.spawn(A);
world.spawn_empty();
let system = (
DynParamBuilder::new(LocalBuilder(3_usize)),
DynParamBuilder::new::<Query<()>>(QueryParamBuilder::new(|builder| {
builder.with::<A>();
})),
DynParamBuilder::new::<&Entities>(ParamBuilder),
)
.build_state(&mut world)
.build_system(
|mut p0: DynSystemParam, mut p1: DynSystemParam, mut p2: DynSystemParam| {
let local = *p0.downcast_mut::<Local<usize>>().unwrap();
let query_count = p1.downcast_mut::<Query<()>>().unwrap().iter().count();
let _entities = p2.downcast_mut::<&Entities>().unwrap();
assert!(p0.downcast_mut::<Query<()>>().is_none());
local + query_count
},
);
let result = world.run_system_once(system);
assert_eq!(result, 4);
}
}

View file

@ -22,6 +22,7 @@ use bevy_utils::{all_tuples, synccell::SyncCell};
#[cfg(feature = "track_change_detection")]
use std::panic::Location;
use std::{
any::Any,
fmt::Debug,
marker::PhantomData,
ops::{Deref, DerefMut},
@ -1674,6 +1675,253 @@ unsafe impl<T: ?Sized> SystemParam for PhantomData<T> {
// SAFETY: No world access.
unsafe impl<T: ?Sized> ReadOnlySystemParam for PhantomData<T> {}
/// A [`SystemParam`] with a type that can be configured at runtime.
/// To be useful, this must be configured using a [`DynParamBuilder`](crate::system::DynParamBuilder) to build the system using a [`SystemParamBuilder`](crate::prelude::SystemParamBuilder).
///
/// # Examples
///
/// ```
/// # use bevy_ecs::{prelude::*, system::*};
/// #
/// # #[derive(Default, Resource)]
/// # struct A;
/// #
/// # #[derive(Default, Resource)]
/// # struct B;
/// #
/// let mut world = World::new();
/// world.init_resource::<A>();
/// world.init_resource::<B>();
///
/// // If the inner parameter doesn't require any special building, use `ParamBuilder`.
/// // Either specify the type parameter on `DynParamBuilder::new()` ...
/// let system = (DynParamBuilder::new::<Res<A>>(ParamBuilder),)
/// .build_state(&mut world)
/// .build_system(expects_res_a);
/// world.run_system_once(system);
///
/// // ... or use a factory method on `ParamBuilder` that returns a specific type.
/// let system = (DynParamBuilder::new(ParamBuilder::resource::<A>()),)
/// .build_state(&mut world)
/// .build_system(expects_res_a);
/// world.run_system_once(system);
///
/// fn expects_res_a(mut param: DynSystemParam) {
/// // Use the `downcast` methods to retrieve the inner parameter.
/// // They will return `None` if the type does not match.
/// assert!(param.is::<Res<A>>());
/// assert!(!param.is::<Res<B>>());
/// assert!(param.downcast_mut::<Res<B>>().is_none());
/// let foo: Res<A> = param.downcast::<Res<A>>().unwrap();
/// }
///
/// let system = (
/// // If the inner parameter also requires building,
/// // pass the appropriate `SystemParamBuilder`.
/// DynParamBuilder::new(LocalBuilder(10usize)),
/// // `DynSystemParam` is just an ordinary `SystemParam`,
/// // and can be combined with other parameters as usual!
/// ParamBuilder::query(),
/// )
/// .build_state(&mut world)
/// .build_system(|param: DynSystemParam, query: Query<()>| {
/// let local: Local<usize> = param.downcast::<Local<usize>>().unwrap();
/// assert_eq!(*local, 10);
/// });
/// world.run_system_once(system);
/// ```
pub struct DynSystemParam<'w, 's> {
/// A `ParamState<T>` wrapping the state for the underlying system param.
state: &'s mut dyn Any,
world: UnsafeWorldCell<'w>,
system_meta: SystemMeta,
change_tick: Tick,
}
impl<'w, 's> DynSystemParam<'w, 's> {
/// # SAFETY
/// - `state` must be a `ParamState<T>` for some inner `T: SystemParam`.
/// - The passed [`UnsafeWorldCell`] must have access to any world data registered
/// in [`init_state`](SystemParam::init_state) for the inner system param.
/// - `world` must be the same `World` that was used to initialize
/// [`state`](SystemParam::init_state) for the inner system param.
unsafe fn new(
state: &'s mut dyn Any,
world: UnsafeWorldCell<'w>,
system_meta: SystemMeta,
change_tick: Tick,
) -> Self {
Self {
state,
world,
system_meta,
change_tick,
}
}
/// Returns `true` if the inner system param is the same as `T`.
pub fn is<T: SystemParam + 'static>(&self) -> bool {
self.state.is::<ParamState<T>>()
}
/// Returns the inner system param if it is the correct type.
/// This consumes the dyn param, so the returned param can have its original world and state lifetimes.
pub fn downcast<T: SystemParam + 'static>(self) -> Option<T::Item<'w, 's>> {
// SAFETY:
// - `DynSystemParam::new()` ensures `state` is a `ParamState<T>`, that the world matches,
// and that it has access required by the inner system param.
// - This consumes the `DynSystemParam`, so it is the only use of `world` with this access and it is available for `'w`.
unsafe { downcast::<T>(self.state, &self.system_meta, self.world, self.change_tick) }
}
/// Returns the inner system parameter if it is the correct type.
/// This borrows the dyn param, so the returned param is only valid for the duration of that borrow.
pub fn downcast_mut<T: SystemParam + 'static>(&mut self) -> Option<T::Item<'_, '_>> {
// SAFETY:
// - `DynSystemParam::new()` ensures `state` is a `ParamState<T>`, that the world matches,
// and that it has access required by the inner system param.
// - This exclusively borrows the `DynSystemParam` for `'_`, so it is the only use of `world` with this access for `'_`.
unsafe { downcast::<T>(self.state, &self.system_meta, self.world, self.change_tick) }
}
/// Returns the inner system parameter if it is the correct type.
/// This borrows the dyn param, so the returned param is only valid for the duration of that borrow,
/// but since it only performs read access it can keep the original world lifetime.
/// This can be useful with methods like [`Query::iter_inner()`] or [`Res::into_inner()`]
/// to obtain references with the original world lifetime.
pub fn downcast_mut_inner<T: ReadOnlySystemParam + 'static>(
&mut self,
) -> Option<T::Item<'w, '_>> {
// SAFETY:
// - `DynSystemParam::new()` ensures `state` is a `ParamState<T>`, that the world matches,
// and that it has access required by the inner system param.
// - The inner system param only performs read access, so it's safe to copy that access for the full `'w` lifetime.
unsafe { downcast::<T>(self.state, &self.system_meta, self.world, self.change_tick) }
}
}
/// # SAFETY
/// - `state` must be a `ParamState<T>` for some inner `T: SystemParam`.
/// - The passed [`UnsafeWorldCell`] must have access to any world data registered
/// in [`init_state`](SystemParam::init_state) for the inner system param.
/// - `world` must be the same `World` that was used to initialize
/// [`state`](SystemParam::init_state) for the inner system param.
unsafe fn downcast<'w, 's, T: SystemParam + 'static>(
state: &'s mut dyn Any,
system_meta: &SystemMeta,
world: UnsafeWorldCell<'w>,
change_tick: Tick,
) -> Option<T::Item<'w, 's>> {
state.downcast_mut::<ParamState<T>>().map(|state| {
// SAFETY:
// - The caller ensures the world has access for the underlying system param,
// and since the downcast succeeded, the underlying system param is T.
// - The caller ensures the `world` matches.
unsafe { T::get_param(&mut state.0, system_meta, world, change_tick) }
})
}
/// The [`SystemParam::State`] for a [`DynSystemParam`].
pub struct DynSystemParamState(Box<dyn DynParamState>);
impl DynSystemParamState {
pub(crate) fn new<T: SystemParam + 'static>(state: T::State) -> Self {
Self(Box::new(ParamState::<T>(state)))
}
}
/// Allows a [`SystemParam::State`] to be used as a trait object for implementing [`DynSystemParam`].
trait DynParamState: Sync + Send {
/// Casts the underlying `ParamState<T>` to an `Any` so it can be downcast.
fn as_any_mut(&mut self) -> &mut dyn Any;
/// For the specified [`Archetype`], registers the components accessed by this [`SystemParam`] (if applicable).a
///
/// # Safety
/// `archetype` must be from the [`World`] used to initialize `state` in `init_state`.
unsafe fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta);
/// Applies any deferred mutations stored in this [`SystemParam`]'s state.
/// This is used to apply [`Commands`] during [`apply_deferred`](crate::prelude::apply_deferred).
///
/// [`Commands`]: crate::prelude::Commands
fn apply(&mut self, system_meta: &SystemMeta, world: &mut World);
/// Queues any deferred mutations to be applied at the next [`apply_deferred`](crate::prelude::apply_deferred).
fn queue(&mut self, system_meta: &SystemMeta, world: DeferredWorld);
}
/// A wrapper around a [`SystemParam::State`] that can be used as a trait object in a [`DynSystemParam`].
struct ParamState<T: SystemParam>(T::State);
impl<T: SystemParam + 'static> DynParamState for ParamState<T> {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
unsafe fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta) {
// SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`.
unsafe { T::new_archetype(&mut self.0, archetype, system_meta) };
}
fn apply(&mut self, system_meta: &SystemMeta, world: &mut World) {
T::apply(&mut self.0, system_meta, world);
}
fn queue(&mut self, system_meta: &SystemMeta, world: DeferredWorld) {
T::queue(&mut self.0, system_meta, world);
}
}
// SAFETY: `init_state` creates a state of (), which performs no access. The interesting safety checks are on the `SystemParamBuilder`.
unsafe impl SystemParam for DynSystemParam<'_, '_> {
type State = DynSystemParamState;
type Item<'world, 'state> = DynSystemParam<'world, 'state>;
fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {
DynSystemParamState::new::<()>(())
}
unsafe fn get_param<'world, 'state>(
state: &'state mut Self::State,
system_meta: &SystemMeta,
world: UnsafeWorldCell<'world>,
change_tick: Tick,
) -> Self::Item<'world, 'state> {
// SAFETY:
// - `state.0` is a boxed `ParamState<T>`, and its implementation of `as_any_mut` returns `self`.
// - The state was obtained from `SystemParamBuilder::build()`, which registers all [`World`] accesses used
// by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta).
// - The caller ensures that the provided world is the same and has the required access.
unsafe {
DynSystemParam::new(
state.0.as_any_mut(),
world,
system_meta.clone(),
change_tick,
)
}
}
unsafe fn new_archetype(
state: &mut Self::State,
archetype: &Archetype,
system_meta: &mut SystemMeta,
) {
// SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`.
unsafe { state.0.new_archetype(archetype, system_meta) };
}
fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) {
state.0.apply(system_meta, world);
}
fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) {
state.0.queue(system_meta, world);
}
}
#[cfg(test)]
mod tests {
use super::*;