Introductions
Note: this book contains runnable examples at the end of some chapters. You can click the play button to see their output!
What is this book about?
Several rust libraries use a pattern something like this (in this example, bevy):
fn main() {
App::new()
.add_system(foo)
.add_system(bar)
.run();
}
fn foo(query: Query<Foo, With<Bar>>) {
// some code
}
fn bar(query: Query<Bar, Without<Foo>>) {
// some other code
}
Most users can intuitively grasp that this causes the app to automatically call the systems foo
and bar
once per frame, but very few are able to easily figure out how this is possible. This book aims to explain
how this works, starting from scratch.
What is Dependency Injection?
Dependency injection is a needlessly complicated way to phrase "asking for things instead of providing them".
An easy example of dependency injection would be Iterator::map()
; you provide a function/closure which asks for
an item to map and maps it, and the iterator itself "injects" that "dependency".
In this case we're mimicking what
Bevy Engine does. An App
is created, provided with System
's, and those System
's are called automatically. System
's have various parameters which are automatically known and provided by the App
. The parameters are the dependencies, and the app is "injecting" them.
Dependency injection is a useful pattern, and most people have probably used it at least somewhere even if they don't know it by name.
Is this book exclusively about rust and bevy?
About rust: Yes, but the techniques within can be applied to other languages if those languages have the features to support it.
About bevy: Sort of. This book is heavily inspired by it, but these techniques can certainly be applied to other rust projects, and have already shown up before in libraries like axum
. And overall the technique shown will be simpler than what bevy actually does.
How much rust do I need to know to understand this?
I'll try to aim to make this as easily understandable as possible, but understanding of traits, dyn Trait
s, tuples, a little bit of lifetimes, and other basic rust knowledge will likely be required.
Setting up the scheduler
In order to illustrate what's going on, we'll want a data structure that can store resources to be queried, our systems, and run them. We'll keep it extremely simple:
#![allow(unused)] fn main() { use std::collections::HashMap; use std::any::{Any, TypeId}; type StoredSystem = (); struct Scheduler { systems: Vec<StoredSystem>, resources: HashMap<TypeId, Box<dyn Any>>, } }
The scheduler stores StoredSystem
's (don't worry, we haven't defined those yet) and uses a basic TypeMap
which can store one item of every type (provided that the item lives for 'static
a.k.a. is not a borrow and does not include a borrow).
What about that StoredSystem
? We'll get back to those later, for now just supply it with a dummy definition:
#![allow(unused)] fn main() { struct StoredSystem; }
Defining a system
We need to define what a System
is in our context.
From a design perspective, we already know we can't store borrowing types; so those aren't allowed to be parameters to systems. We can also just say we'll panic!
if a system asks for a resource we don't actually have one of. Finally we don't have anything to do with return values, so we'll prohibit them. That makes the definition of a system pretty straightforward: any function that takes 'static
parameters and returns ()
. Let's translate that to rust:
trait System<Input> {}
impl<F: FnMut()> System<()> for F {}
impl<F: FnMut(T1), T1: 'static> System<(T1,)> for F {}
impl<F: FnMut(T1, T2), T1: 'static, T2: 'static> System<(T1, T2)> for F {}
// repeat the pattern up until the maximum parameter count you want to support.
(We have to include the inputs as a type parameter on System
for complicated type system reasons that we'll get back to later...)
Ok, cool, but this is useless on its own. How can we have one function signature that can call any of these systems? We need to expose some way to flatten our input, give every system one parameter that can satisfy all of their requirements. How can we do that...?
How about this?
trait System<Input> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>);
}
Then this run function just needs to pull the resources out and we can wrap the actual call behind it!
Some boilerplate later:
impl<F: FnMut()> System<()> for F {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
(self)()
}
}
impl<F: FnMut(T1), T1: 'static> System<(T1,)> for F {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
let _0 = *resources.remove(&TypeId::of::<T1>()).unwrap().downcast::<T1>().unwrap();
(self)(_0)
}
}
impl<F: FnMut(T1, T2), T1: 'static, T2: 'static> System<(T1, T2)> for F {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
let _0 = *resources.remove(&TypeId::of::<T1>()).unwrap().downcast::<T1>().unwrap();
let _1 = *resources.remove(&TypeId::of::<T2>()).unwrap().downcast::<T2>().unwrap();
(self)(_0, _1)
}
}
Spicy sidenote here: this does permanently remove the resources from the resource store on call. We'll get back to that later, just use the scheduler no more than once for now, or refill the resources after each run.
So we've implemented a trait so that we can call some functions without actually knowing their params. Mostly. The trait is still parameterized with that associated type, so we can't just Box<dyn System>
. Let's make a type erased wrapper:
trait ErasedSystem {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>);
}
impl<S: System<I>, I> ErasedSystem for S {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
<Self as System<I>>::run(self, resources);
}
}
Oops, that complicated type system stuff is back:
error[E0207]: the type parameter
I
is not constrained by the impl trait, self type, or predicates
I'll save you the trouble of trying to figure out what this really means: Any given type can implement multiple traits. FnMut(T)
and FnMut(T, U)
are different traits. Therefore a type can have multiple function implementations, and we're not explicitly selecting one. Now, we don't have any fancy future type system stuff like specialization (which might not help this situation I'm not sure), but we do have structs. While F
can implement multiple FnMut
traits, if we wrap F
in a struct then that struct can "select" a specific implementation; the implementation is whichever matches the struct's generic parameters, which only one implementation can do. We'll call the struct FunctionSystem
:
struct FunctionSystem<Input, F> {
f: F,
// we need a marker because otherwise we're not using `Input`.
// fn() -> Input is chosen because just using Input would not be `Send` + `Sync`,
// but the fnptr is always `Send` + `Sync`.
//
// Also, this way Input is covariant, but that's not super relevant since we can only deal with
// static parameters here anyway so there's no subtyping. More info here:
// https://doc.rust-lang.org/nomicon/subtyping.html
marker: PhantomData<fn() -> Input>,
}
Now let's remove System
's generic parameters and move System
from being on the function itself to FunctionSystem
:
trait System {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>);
}
impl<F: FnMut()> System for FunctionSystem<(), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
(self.f)()
}
}
impl<F: FnMut(T1), T1: 'static> System for FunctionSystem<(T1,), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
let _0 = *resources.remove(&TypeId::of::<T1>()).unwrap().downcast::<T1>().unwrap();
(self.f)(_0)
}
}
impl<F: FnMut(T1, T2), T1: 'static, T2: 'static> System for FunctionSystem<(T1, T2), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
let _0 = *resources.remove(&TypeId::of::<T1>()).unwrap().downcast::<T1>().unwrap();
let _1 = *resources.remove(&TypeId::of::<T2>()).unwrap().downcast::<T2>().unwrap();
(self.f)(_0, _1)
}
}
Now that System
takes no associated types or generic parameters, we can box it easily:
type StoredSystem = Box<dyn System>;
We'll also want to be able to convert FnMut(...)
to a system easily instead of manually wrapping:
trait IntoSystem<Input> {
type System: System;
fn into_system(self) -> Self::System;
}
impl<F: FnMut()> IntoSystem<()> for F {
type System = FunctionSystem<(), Self>;
fn into_system(self) -> Self::System {
FunctionSystem {
f: self,
marker: Default::default(),
}
}
}
impl<F: FnMut(T1,), T1: 'static> IntoSystem<(T1,)> for F {
type System = FunctionSystem<(T1,), Self>;
fn into_system(self) -> Self::System {
FunctionSystem {
f: self,
marker: Default::default(),
}
}
}
impl<F: FnMut(T1, T2), T1: 'static, T2: 'static> IntoSystem<(T1, T2)> for F {
type System = FunctionSystem<(T1, T2), Self>;
fn into_system(self) -> Self::System {
FunctionSystem {
f: self,
marker: Default::default(),
}
}
}
// etc.
Some helpers on Scheduler
:
use std::any::{Any, TypeId};
use std::collections::HashMap;
trait System {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>);
}
type StoredSystem = Box<dyn System>;
struct Scheduler {
systems: Vec<StoredSystem>,
resources: HashMap<TypeId, Box<dyn Any>>,
}
trait IntoSystem<Input> {
type System: System;
fn into_system(self) -> Self::System;
}
impl Scheduler {
pub fn run(&mut self) {
for system in self.systems.iter_mut() {
system.run(&mut self.resources);
}
}
pub fn add_system<I, S: System + 'static>(&mut self, system: impl IntoSystem<I, System = S>) {
self.systems.push(Box::new(system.into_system()));
}
pub fn add_resource<R: 'static>(&mut self, res: R) {
self.resources.insert(TypeId::of::<R>(), Box::new(res));
}
}
All together now!
struct FunctionSystem<Input, F> {
f: F,
marker: PhantomData<fn() -> Input>,
}
trait System {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>);
}
impl<F: FnMut()> System for FunctionSystem<(), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
(self.f)()
}
}
impl<F: FnMut(T1), T1: 'static> System for FunctionSystem<(T1,), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
let _0 = *resources.remove(&TypeId::of::<T1>()).unwrap().downcast::<T1>().unwrap();
(self.f)(_0)
}
}
impl<F: FnMut(T1, T2), T1: 'static, T2: 'static> System for FunctionSystem<(T1, T2), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
let _0 = *resources.remove(&TypeId::of::<T1>()).unwrap().downcast::<T1>().unwrap();
let _1 = *resources.remove(&TypeId::of::<T2>()).unwrap().downcast::<T2>().unwrap();
(self.f)(_0, _1)
}
}
trait IntoSystem<Input> {
type System: System;
fn into_system(self) -> Self::System;
}
impl<F: FnMut()> IntoSystem<()> for F {
type System = FunctionSystem<(), Self>;
fn into_system(self) -> Self::System {
FunctionSystem {
f: self,
marker: Default::default(),
}
}
}
impl<F: FnMut(T1,), T1: 'static> IntoSystem<(T1,)> for F {
type System = FunctionSystem<(T1,), Self>;
fn into_system(self) -> Self::System {
FunctionSystem {
f: self,
marker: Default::default(),
}
}
}
impl<F: FnMut(T1, T2), T1: 'static, T2: 'static> IntoSystem<(T1, T2)> for F {
type System = FunctionSystem<(T1, T2), Self>;
fn into_system(self) -> Self::System {
FunctionSystem {
f: self,
marker: Default::default(),
}
}
}
type StoredSystem = Box<dyn System>;
struct Scheduler {
systems: Vec<StoredSystem>,
resources: HashMap<TypeId, Box<dyn Any>>,
}
impl Scheduler {
pub fn run(&mut self) {
for system in self.systems.iter_mut() {
system.run(&mut self.resources);
}
}
pub fn add_system<I, S: System + 'static>(&mut self, system: impl IntoSystem<I, System = S>) {
self.systems.push(Box::new(system.into_system()));
}
pub fn add_resource<R: 'static>(&mut self, res: R) {
self.resources.insert(TypeId::of::<R>(), Box::new(res));
}
}
Now we can write some code to actually use it!
fn main() { let mut scheduler = Scheduler { systems: vec![], resources: HashMap::default(), }; scheduler.add_system(foo); scheduler.add_resource(12i32); scheduler.run(); } fn foo(int: i32) { println!("int! {int}"); } use std::collections::HashMap; use std::marker::PhantomData; use std::any::{Any, TypeId}; struct FunctionSystem<Input, F> { f: F, marker: PhantomData<fn() -> Input>, } trait System { fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>); } impl<F: FnMut()> System for FunctionSystem<(), F> { fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) { (self.f)() } } impl<F: FnMut(T1), T1: 'static> System for FunctionSystem<(T1,), F> { fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) { let _0 = *resources.remove(&TypeId::of::<T1>()).unwrap().downcast::<T1>().unwrap(); (self.f)(_0) } } impl<F: FnMut(T1, T2), T1: 'static, T2: 'static> System for FunctionSystem<(T1, T2), F> { fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) { let _0 = *resources.remove(&TypeId::of::<T1>()).unwrap().downcast::<T1>().unwrap(); let _1 = *resources.remove(&TypeId::of::<T2>()).unwrap().downcast::<T2>().unwrap(); (self.f)(_0, _1) } } trait IntoSystem<Input> { type System: System; fn into_system(self) -> Self::System; } impl<F: FnMut()> IntoSystem<()> for F { type System = FunctionSystem<(), Self>; fn into_system(self) -> Self::System { FunctionSystem { f: self, marker: Default::default(), } } } impl<F: FnMut(T1,), T1: 'static> IntoSystem<(T1,)> for F { type System = FunctionSystem<(T1,), Self>; fn into_system(self) -> Self::System { FunctionSystem { f: self, marker: Default::default(), } } } impl<F: FnMut(T1, T2), T1: 'static, T2: 'static> IntoSystem<(T1, T2)> for F { type System = FunctionSystem<(T1, T2), Self>; fn into_system(self) -> Self::System { FunctionSystem { f: self, marker: Default::default(), } } } type StoredSystem = Box<dyn System>; struct Scheduler { systems: Vec<StoredSystem>, resources: HashMap<TypeId, Box<dyn Any>>, } impl Scheduler { pub fn run(&mut self) { for system in self.systems.iter_mut() { system.run(&mut self.resources); } } pub fn add_system<I, S: System + 'static>(&mut self, system: impl IntoSystem<I, System = S>) { self.systems.push(Box::new(system.into_system())); } pub fn add_resource<R: 'static>(&mut self, res: R) { self.resources.insert(TypeId::of::<R>(), Box::new(res)); } }
It prints int! 12
like we want! And the user would never actually see their function get called. Mission success?
Yes, but there's obviously some rough edges. It permanently removes resources from the store each run, we have a max limit on parameters, etc, etc. We can do better, and I'll come back to this later to add some more.
Macros
This section will serve as an iterative introduction to rust declarative macros. Rust has 2 kinds of macro, declarative and procedural. Declarative macros use a strange syntax involving pattern matching, whereas a procedural macro is (mostly) normal rust code that manipulates an input syntax tree.
I will not be covering procedural macros here, as they're generally for much more "polished" approaches
like bevy_reflect
, thiserror
, and other derive or attribute macros. Procedural macros are rarely
used in function style like my_macro!()
, whereas declarative macros can only be put in function position.
First, a use case: Why do we want to use a declarative macro? In this case, let's look at the System
trait
implementations:
impl<F: FnMut()> System for FunctionSystem<(), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
(self.f)()
}
}
impl<F: FnMut(T1), T1: 'static> System for FunctionSystem<(T1,), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
let _0 = *resources.remove(&TypeId::of::<T1>()).unwrap().downcast::<T1>().unwrap();
(self.f)(_0)
}
}
impl<F: FnMut(T1, T2), T1: 'static, T2: 'static> System for FunctionSystem<(T1, T2), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
let _0 = *resources.remove(&TypeId::of::<T1>()).unwrap().downcast::<T1>().unwrap();
let _1 = *resources.remove(&TypeId::of::<T2>()).unwrap().downcast::<T2>().unwrap();
(self.f)(_0, _1)
}
}
This is a mouthful of boilerplate, and for only 3 impls. We'd likely prefer to have 16 or 17. We can dramatically shrink this code and the amount of effort required to add impls with a decl macro.
First, let's take a reasonably representative implementation from our target output:
impl<F: FnMut(T1, T2), T1: 'static, T2: 'static> System for FunctionSystem<(T1, T2), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
let _0 = *resources.remove(&TypeId::of::<T1>()).unwrap().downcast::<T1>().unwrap();
let _1 = *resources.remove(&TypeId::of::<T2>()).unwrap().downcast::<T2>().unwrap();
(self.f)(_0, _1)
}
}
(it's ideal to choose one that "scales", i.e. shows off the pattern well; usually picking the implementation corresponding to "2" works best. In this case, the "2" is "2 parameters".)
First, let's just wrap it in macro declaration syntax:
macro_rules! impl_system {
() => {
impl<F: FnMut(T1, T2), T1: 'static, T2: 'static> System for FunctionSystem<(T1, T2), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
let _0 = *resources.remove(&TypeId::of::<T1>()).unwrap().downcast::<T1>().unwrap();
let _1 = *resources.remove(&TypeId::of::<T2>()).unwrap().downcast::<T2>().unwrap();
(self.f)(_0, _1)
}
}
};
}
This produces a macro that takes no parameters and will just spit out this impl verbatim.
Now we need to identify what changes are happening between implementations. We need to change:
- Instances of T1, T2, etc.
- The
_0
,_1
, etc. chain - The function call parameters
Crucially, these are all repetitions derived from the list of type arguments (T1, T2, ...).
That means our parameters to the macro will be T1, T2, ...
, and we can extract the rest from there.
macro_rules! impl_system {
($($params:ident),*) => {
// ...
};
}
$
is a major symbol in decl macros, indicating some decl-macro specific syntax. $[name]:[type]
syntax
declares a syntax variable; in this case we bind some ident (any string that would be valid for e.g. a function,
variable, or type name, among others) to the name param
. We can access this variable inside the
macro body with $param
.
Then, we wrapped that variable in $()*
to signify that we want to match
0 or more of it. This means we need to begin a repetition in the macro body to actually access
the variable, since we match it 0 or more times.
Finally, we slip a ,
into $(),*
to signify that when we match 0 or more times, there must be a
comma between each element but not one at the end. If we wanted to optionally consume a trailing comma,
we could add $(,)?
after the parameter to mean "0 or 1 commas" like so: $($params:ident),* $(,)?
.
Alternatively, if we wanted to always match a trailing comma, we could instead do this:
$($params:ident ,)*
which would match "an ident followed by a comma, 0 or more times".
So, we now have some parameters available; probably a sequence of T1, T2, ..., TN
. The first thing
we can do is replace our hardcoded type lists:
macro_rules! impl_system {
($($params:ident),*) => {
impl<F: FnMut($($params),*), T1: 'static, T2: 'static> System for FunctionSystem<($($params ,)*), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
let _0 = *resources.remove(&TypeId::of::<T1>()).unwrap().downcast::<T1>().unwrap();
let _1 = *resources.remove(&TypeId::of::<T2>()).unwrap().downcast::<T2>().unwrap();
(self.f)(_0, _1)
}
}
};
}
Note that the FnMut
receives a plain list of type arguments, but the FunctionSystem
receives a tuple;
thus we use $($params),*
for the FnMut but $($params ,)*
for the tuple, to ensure the T1
case
actually creates a valid tuple and not just a parenthesized T1
. Technically, we could use $($params ,)*
for both since rust is good about respecting trailing commas, generally.
As you can see, the syntax to extract the syntax variables is very similar to the syntax to match
them in the first place. The next thing we want to do is replace those TN: 'static
bits; they're
slightly more complex.
macro_rules! impl_system {
($($params:ident),*) => {
impl<F: FnMut($($params),*), $($params : 'static),*> System for FunctionSystem<($($params ,)*), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
let _0 = *resources.remove(&TypeId::of::<T1>()).unwrap().downcast::<T1>().unwrap();
let _1 = *resources.remove(&TypeId::of::<T2>()).unwrap().downcast::<T2>().unwrap();
(self.f)(_0, _1)
}
}
};
}
We just insert new verbatim syntax into the repetition just like when we want to match a comma after every ident. Next, we need to deal with those variables, but this presents a problem; how do we come up with a unique variable name for each one?
That's the neat part, you don't
We'll just name the variable the exact same thing as its type. The compiler'll figure it out, it's fine.
macro_rules! impl_system {
($($params:ident),*) => {
impl<F: FnMut($($params),*), $($params : 'static),*> System for FunctionSystem<($($params ,)*), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
$(
let $params = *resources.remove(&TypeId::of::<$params>()).unwrap().downcast::<$params>().unwrap();
)*
(self.f)(_0, _1)
}
}
};
}
And look, now that the variables are named the same thing as their types, we can just replace the parameters to the function call like we did in the first step.
Also, let's add some lint suppressors, because the generated code will raise some pointless warnings.
macro_rules! impl_system {
($($params:ident),*) => {
#[allow(unused_variables)]
#[allow(non_snake_case)]
impl<F: FnMut($($params),*), $($params : 'static),*> System for FunctionSystem<($($params ,)*), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
$(
let $params = *resources.remove(&TypeId::of::<$params>()).unwrap().downcast::<$params>().unwrap();
)*
(self.f)($($params),*)
}
}
};
}
Now just call the macro a few times:
impl_system!();
impl_system!(T1);
impl_system!(T1, T2);
impl_system!(T1, T2, T3);
impl_system!(T1, T2, T3, T4);
impl_system!(T1, T2, T3, T4, T5);
// and so on
And now we've massively cut down on code duplication, at the cost of code obfuscation to those not versed in the dark art of macros.
But this is still somewhat... duplicate-y. What if we wrote another macro to invoke this macro a number of times?
If we don't need to parameterize it, we can hardcode it to expand, say, 16 times:
macro_rules! call_16_times {
($target:ident) => {
$target!();
$target!(T1);
$target!(T1, T2);
// etc.
};
}
Or, we can even make use of pattern matching to make it inductive:
macro_rules! call_n_times {
($target:ident, 1) => {
$target!();
};
($target:ident, 2) => {
$target!(T1);
call_n_times!($target, 1);
};
($target:ident, 3) => {
$target!(T1, T2);
call_n_times!($target, 2);
};
// etc.
}
At this point, we're starting to spin our wheels in a turing tarpit, so if you're going any further than this consider switching to procedural macros. But to prove this all works:
use std::collections::HashMap; use std::marker::PhantomData; use std::any::{Any, TypeId}; struct FunctionSystem<Input, F> { f: F, marker: PhantomData<fn() -> Input>, } trait System { fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>); } trait IntoSystem<Input> { type System: System; fn into_system(self) -> Self::System; } impl<F: FnMut()> IntoSystem<()> for F { type System = FunctionSystem<(), Self>; fn into_system(self) -> Self::System { FunctionSystem { f: self, marker: Default::default(), } } } impl<F: FnMut(T1,), T1: 'static> IntoSystem<(T1,)> for F { type System = FunctionSystem<(T1,), Self>; fn into_system(self) -> Self::System { FunctionSystem { f: self, marker: Default::default(), } } } impl<F: FnMut(T1, T2), T1: 'static, T2: 'static> IntoSystem<(T1, T2)> for F { type System = FunctionSystem<(T1, T2), Self>; fn into_system(self) -> Self::System { FunctionSystem { f: self, marker: Default::default(), } } } type StoredSystem = Box<dyn System>; struct Scheduler { systems: Vec<StoredSystem>, resources: HashMap<TypeId, Box<dyn Any>>, } impl Scheduler { pub fn run(&mut self) { for system in self.systems.iter_mut() { system.run(&mut self.resources); } } pub fn add_system<I, S: System + 'static>(&mut self, system: impl IntoSystem<I, System = S>) { self.systems.push(Box::new(system.into_system())); } pub fn add_resource<R: 'static>(&mut self, res: R) { self.resources.insert(TypeId::of::<R>(), Box::new(res)); } } macro_rules! impl_system { ($($params:ident),*) => { #[allow(unused_variables)] #[allow(non_snake_case)] impl<F: FnMut($($params),*), $($params : 'static),*> System for FunctionSystem<($($params ,)*), F> { fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) { $( let $params = *resources.remove(&TypeId::of::<$params>()).unwrap().downcast::<$params>().unwrap(); )* (self.f)($($params),*) } } }; } macro_rules! call_n_times { ($target:ident, 1) => { $target!(); }; ($target:ident, 2) => { $target!(T1); call_n_times!($target, 1); }; ($target:ident, 3) => { $target!(T1, T2); call_n_times!($target, 2); }; // etc. } call_n_times!(impl_system, 3); fn main() { let mut scheduler = Scheduler { systems: vec![], resources: HashMap::default(), }; scheduler.add_system(foo); scheduler.add_resource(12i32); scheduler.add_resource(24f32); scheduler.run(); } fn foo(int: i32, float: f32) { println!("int! {int} float! {float}"); }
Passing references
Yes, but there's obviously some rough edges. It permanently removes resources from the store each run, we have a max limit on parameters, etc, etc. We can do better, and I'll come back to this later to add some more.
Having gotten the basic architecture working, it's time to make some refinements. In this chapter we'll be focusing on two issues: The maximum limit on system parameters, and the fact that it "self destructs" every run by consuming resources. The latter will enable the former, so we'll start with allowing borrows.
First let's switch from owned values to borrowed ones, and see what we can do from there:
impl<F: FnMut(&T1, &T2), T1: 'static, T2: 'static> System for FunctionSystem<(T1, T2), F> {
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
let _0 = resources.get(&TypeId::of::<T1>()).unwrap().downcast_ref::<T1>().unwrap();
let _1 = resources.get(&TypeId::of::<T2>()).unwrap().downcast_ref::<T2>().unwrap();
(self.f)(_0, _1)
}
}
impl<F: FnMut(&T1, &T2), T1: 'static, T2: 'static> IntoSystem<(T1, T2)> for F {
type System = FunctionSystem<(T1, T2), Self>;
fn into_system(self) -> Self::System {
FunctionSystem {
f: self,
marker: Default::default(),
}
}
}
This works, but there's a pretty obvious problem:
fn foo(int: i32) {
println!("int! {int}");
}
error[E0277]: the trait bound
fn(i32) {foo}: IntoSystem<_>
is not satisfied
That's not great. It'd be nice to be able to still consume resources if desired- or more likely, use mutable references. We could change it to mutable references, but then we can't use immutable references. And trying to manually implement all three would be a bit of a combinatorial explosion- every permutation of owned/&/&mut leads to something like 3^8 implementations for the 8 parameter version alone. Not exactly reasonable, even with macros.
Let's try something else; let's abstract over all possible system parameters.
trait SystemParam {
fn retrieve(resources: &mut HashMap<TypeId, Box<dyn Any>>) -> Self;
}
struct Res<'a, T: 'static> {
value: &'a T,
}
impl<'a, T: 'static> SystemParam for Res<'a, T> {
fn retrieve(resources: &mut HashMap<TypeId, Box<dyn Any>>) -> Self {
let value = resources.get(&TypeId::of::<T>()).unwrap().downcast_ref::<T>().unwrap();
Res { value }
}
}
struct ResMut<'a, T: 'static> {
value: &'a mut T,
}
impl<'a, T: 'static> SystemParam for ResMut<'a, T> {
fn retrieve(resources: &mut HashMap<TypeId, Box<dyn Any>>) -> Self {
let value = resources.get_mut(&TypeId::of::<T>()).unwrap().downcast_mut::<T>().unwrap();
ResMut { value }
}
}
struct ResOwned<T: 'static> {
value: T
}
impl<T: 'static> SystemParam for ResOwned<T> {
fn retrieve(resources: &mut HashMap<TypeId, Box<dyn Any>>) -> Self {
let value = *resources.remove(&TypeId::of::<T>()).unwrap().downcast::<T>().unwrap();
ResOwned { value }
}
}
SystemParam
provides the retrieve
function which is where our logic for gethering resources lives.
Res/ResMut/ResOwned map to &/&mut/owned respectively. They also closely resemble some of bevy's own SystemParam
s.
Great, now let's try to compile and-
error: lifetime may not live long enough
oh wow lifetime errors my favorite
This seems like an easy fix at first...
// The modification is the same for ResMut/Owned
impl<'a, T: 'static> SystemParam for Res<'a, T> {
fn retrieve(resources: &'a mut HashMap<TypeId, Box<dyn Any>>) -> Self {
let value = resources.get(&TypeId::of::<T>()).unwrap().downcast_ref::<T>().unwrap();
Res { value }
}
}
But this changes the function signature, so we need a lifetime in SystemParam
trait SystemParam<'a> {
fn retrieve(resources: &'a mut HashMap<TypeId, Box<dyn Any>>) -> Self;
}
This leads to yet another lifetime error in implementing systems, as they try to pass in a &'_ mut HashMap...
rather than &'a mut HashMap...
.
trait System<'a> {
fn run(&mut self, resources: &'a mut HashMap<TypeId, Box<dyn Any>>);
}
Which then impacts IntoSystem
...
trait IntoSystem<'a, Input> {
type System: System<'a>;
fn into_system(self) -> Self::System;
}
AND StoredSystem
...
type StoredSystem = Box<dyn for<'a> System<'a>>;
And finally add_system
pub fn add_system<I, S: for<'a> System<'a> + 'static>(&mut self, system: impl for<'a> IntoSystem<'a, I, System = S>) {
self.systems.push(Box::new(system.into_system()));
}
WHEW! Glad that's over.
Just kidding, none of it worked, throw it out.
error[E0499]: cannot borrow
*resources
as mutable more than once at a time
Because we're mutably borrowing resources
multiple times for variants with > 1 parameter!
How do we solve this, using all the clever tools rust provides to create a safe, expressive solution-
trait SystemParam<'a> {
fn retrieve(resources: &'a HashMap<TypeId, Box<dyn Any>>) -> Self;
}
impl<'a, T: 'static> SystemParam<'a> for Res<'a, T> {
fn retrieve(resources: &'a HashMap<TypeId, Box<dyn Any>>) -> Self {
let value = resources.get(&TypeId::of::<T>()).unwrap().downcast_ref::<T>().unwrap();
Res { value }
}
}
// struct ResMut<'a, T: 'static> {
// value: &'a mut T,
// }
// impl<'a, T: 'static> SystemParam for ResMut<'a, T> {
// fn retrieve(resources: &mut HashMap<TypeId, Box<dyn Any>>) -> Self {
// let value = resources.get_mut(&TypeId::of::<T>()).unwrap().downcast_mut::<T>().unwrap();
// ResMut { value }
// }
// }
// struct ResOwned<T: 'static> {
// value: T
// }
// impl<T: 'static> SystemParam for ResOwned<T> {
// fn retrieve(resources: &mut HashMap<TypeId, Box<dyn Any>>) -> Self {
// let value = *resources.remove(&TypeId::of::<T>()).unwrap().downcast::<T>().unwrap();
// ResOwned { value }
// }
// }
We'll burn that bridge when we get to it, I don't have the time for interior mutability or unsafe shenanigans right now. Because unfortunately that lifetime stuff is back.
error: implementation of
System
is not general enough
We can't actually pass any existing system to add_system
, because it requires that the system implement both System
and IntoSystem
for all lifetimes.
(That's what that for<'a>
bit means). It doesn't, it's only implemented for the lifetime of its parameter, so that won't work. And if that won't work, then we can't box it like this either, so it looks like we'll need to go back to the drawing board. Why not take a look at how bevy approaches this?
impl<Out, Func: Send + Sync + 'static, $($param: SystemParam),*> SystemParamFunction<(), Out, ($($param,)*), ()> for Func
where
for <'a> &'a mut Func:
FnMut($($param),*) -> Out +
FnMut($(SystemParamItem<$param>),*) -> Out, Out: 'static
How interesting... and what is SystemParamItem
?
/// Shorthand way of accessing the associated type [`SystemParam::Item`] for a given [`SystemParam`].
pub type SystemParamItem<'w, 's, P> = <P as SystemParam>::Item<'w, 's>;
Ah, "easy". So SystemParam
has a Generic Associated Type called Item
which is the same as the SystemParam
, but with a new lifetime. They can take the function with some irrelevant lifetime, and then give it a new lifetime of the passed in resources. And while the type alias makes it shorter, I'm going to go without it to illustrate what it really means. Very complicated and clever. Let's do it!
trait SystemParam {
type Item<'new>;
fn retrieve<'r>(resources: &'r HashMap<TypeId, Box<dyn Any>>) -> Self::Item<'r>;
}
struct Res<'a, T: 'static> {
value: &'a T,
}
impl<'res, T: 'static> SystemParam for Res<'res, T> {
type Item<'new> = Res<'new, T>;
fn retrieve<'r>(resources: &'r HashMap<TypeId, Box<dyn Any>>) -> Self::Item<'r> {
Res { value: resources.get(&TypeId::of::<T>()).unwrap().downcast_ref().unwrap() }
}
}
impl<F, T1: SystemParam, T2: SystemParam> System for FunctionSystem<(T1, T2), F>
where
// for any two arbitrary lifetimes, a mutable reference to F with lifetime 'a
// implements FnMut taking parameters of lifetime 'b
for<'a, 'b> &'a mut F:
FnMut(T1, T2) +
FnMut(<T1 as SystemParam>::Item<'b>, <T2 as SystemParam>::Item<'b>)
{
fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) {
// necessary to tell rust exactly which impl to call; it gets a bit confused otherwise
fn call_inner<T1, T2>(
mut f: impl FnMut(T1, T2),
_0: T1,
_1: T2
) {
f(_0, _1)
}
let _0 = T1::retrieve(resources);
let _1 = T2::retrieve(resources);
call_inner(&mut self.f, _0, _1)
}
}
impl<F: FnMut(T1, T2), T1: SystemParam, T2: SystemParam> IntoSystem<(T1, T2)> for F
where
for<'a, 'b> &'a mut F:
FnMut(T1, T2) +
FnMut(<T1 as SystemParam>::Item<'b>, <T2 as SystemParam>::Item<'b>)
{
type System = FunctionSystem<(T1, T2), Self>;
fn into_system(self) -> Self::System {
FunctionSystem {
f: self,
marker: Default::default(),
}
}
}
(implementations for other arities of system left as exercise for the reader)
And finally a new main:
use std::collections::HashMap; use std::marker::PhantomData; use std::any::{Any, TypeId}; struct FunctionSystem<Input, F> { f: F, marker: PhantomData<fn() -> Input>, } trait System { fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>); } trait IntoSystem<Input> { type System: System; fn into_system(self) -> Self::System; } type StoredSystem = Box<dyn System>; struct Scheduler { systems: Vec<StoredSystem>, resources: HashMap<TypeId, Box<dyn Any>>, } impl Scheduler { pub fn run(&mut self) { for system in self.systems.iter_mut() { system.run(&mut self.resources); } } pub fn add_system<I, S: System + 'static>(&mut self, system: impl IntoSystem<I, System = S>) { self.systems.push(Box::new(system.into_system())); } pub fn add_resource<R: 'static>(&mut self, res: R) { self.resources.insert(TypeId::of::<R>(), Box::new(res)); } } trait SystemParam { type Item<'new>; fn retrieve<'r>(resources: &'r HashMap<TypeId, Box<dyn Any>>) -> Self::Item<'r>; } struct Res<'a, T: 'static> { value: &'a T, } impl<'res, T: 'static> SystemParam for Res<'res, T> { type Item<'new> = Res<'new, T>; fn retrieve<'r>(resources: &'r HashMap<TypeId, Box<dyn Any>>) -> Self::Item<'r> { Res { value: resources.get(&TypeId::of::<T>()).unwrap().downcast_ref().unwrap() } } } impl<F, T1: SystemParam, T2: SystemParam> System for FunctionSystem<(T1, T2), F> where // for any two arbitrary lifetimes, a mutable reference to F with lifetime 'a // implements FnMut taking parameters of lifetime 'b for<'a, 'b> &'a mut F: FnMut(T1, T2) + FnMut(<T1 as SystemParam>::Item<'b>, <T2 as SystemParam>::Item<'b>) { fn run(&mut self, resources: &mut HashMap<TypeId, Box<dyn Any>>) { // necessary to tell rust exactly which impl to call; it gets a bit confused otherwise fn call_inner<T1, T2>( mut f: impl FnMut(T1, T2), _0: T1, _1: T2 ) { f(_0, _1) } let _0 = T1::retrieve(resources); let _1 = T2::retrieve(resources); call_inner(&mut self.f, _0, _1) } } impl<F: FnMut(T1, T2), T1: SystemParam, T2: SystemParam> IntoSystem<(T1, T2)> for F where for<'a, 'b> &'a mut F: FnMut(T1, T2) + FnMut(<T1 as SystemParam>::Item<'b>, <T2 as SystemParam>::Item<'b>) { type System = FunctionSystem<(T1, T2), Self>; fn into_system(self) -> Self::System { FunctionSystem { f: self, marker: Default::default(), } } } fn main() { let mut scheduler = Scheduler { systems: vec![], resources: HashMap::default(), }; scheduler.add_system(foo); scheduler.add_resource(12i32); scheduler.add_resource(24f32); scheduler.run(); } fn foo(int: Res<i32>, float: Res<f32>) { println!("int! {} float! {}", int.value, float.value); }
And this works! Perfectly! No weird errors, we can now actually implement pass by ref.
And this infrastructure lends itself perfectly to allowing unlimited parameters, which we'll do next.
More parameters
Now that we have SystemParam
in place, it'll be easy to expand this to work with unlimited parameters. We just need one crucial idea: what if a tuple of SystemParam
is, itself, a SystemParam
?
Let's implement:
impl<T1: SystemParam, T2: SystemParam> SystemParam for (T1, T2) {
type Item<'new> = (T1::Item<'new>, T2::Item<'new>);
fn retrieve<'r>(resources: &'r HashMap<TypeId, Box<dyn Any>>) -> Self::Item<'r> {
(
T1::retrieve(resources),
T2::retrieve(resources),
)
}
}
fn foo(int: (Res<i32>, Res<u32>)) {
println!("int! {} uint! {}", int.0.value, int.1.value);
}
It just works!
Now you may wonder:
But this is only two items? This doesn't actually give us "unlimited" parameters, just slightly more?
But this is actually sufficient to have unlimited parameters:
fn foo(int: (Res<One>, (Res<Two>, (Res<Three>, Res<Four>)))) {
// ...
}
And so on. As I hinted at in chapter 1, there's a syntax cost to this, but it's alleviated by implementing up to 16-tuples, so just implement for bigger tuples. Maybe investigate the macros section in chapter one for a convenient way to do this!
And that's it, actually. We can nest parameters indefinitely to pass infinite parameters. Next time we'll go back to that aliasing issue and figure out how to get disjoint mutable access to resources.
The easy way out
So, our goal is to get disjoint mutable access to resources in the world and provide them to systems that are run strictly serially (singlethreaded) (!!!). This isn't easy, because the borrow checker has a hard time when you try to get multiple mutable references inside a data structure (such as our hashmap) at the same time.
Luckily for us, rust provides some escape hatches. First I'll cover the safe, easier way, then the fun way.
The primary tool we're going to use for the "easy way" is a concept called
Interior Mutability:tm:
Very scary sounding, but actually very simple. Interior mutability simply means there exists a function
for this type roughly like fn(&self) -> &mut Self::Inner
. This, of course, seems to violate a
fundamental rule of the borrow checker, that you cannot mutate from an immutable reference.
But sometimes you need to do that, so rust provides the UnsafeCell
type. Then, safe types were built on top of it for our convenience. In this case, we're going
to use a neat type called RefCell
.
RefCell
is a type that allows safe interior mutability by checking at runtime if it's being accessed
correctly. Pretend it looks like this on the inside (this is pseudocode which will not compile for the sake of
being clearer to read, nor does it exactly resemble RefCell's actual implementation which is somewhat
more optimized):
enum Borrow {
None,
Immutable(NonZeroUsize),
Mutable(NonZeroUsize),
}
struct RefCell<T> {
cell: UnsafeCell<T>,
borrows: Borrow,
}
Then when you attempt to borrow it:
// immutable
match &mut self.borrows {
Borrow::None => {
self.borrows = Borrow::Immutable(1);
unsafe { &*self.cell.get() }
}
Borrow::Immutable(x) => {
*x += 1;
unsafe { &*self.cell.get() }
},
Borrow::Mutable(_) => panic!(),
}
// mutable
match &mut self.borrows {
Borrow::None => {
self.borrows = Borrow::Mutable(1);
unsafe { &mut *self.cell.get() }
}
Borrow::Immutable(_) => panic!(),
Borrow::Mutable(_) => panic!(),
}
It increments counters whenever you borrow, or panics if you attempt to make invalid borrows like an immutable reference when a mutable reference exists.
Then, instead of returning &T
or &mut T
, it returns the special types
Ref
and
RefMut
, which are Deref<Target = T>
.
When these are dropped, they decrement the borrow counter.
It's like a runtime borrow checker! This is super useful but very critically: not threadsafe,
as I alluded to at the top with heavy emphasis. A threadsafe alternative would be something like
Mutex
or
RwLock
, which have different semantics.
RwLock is a closer match but, to my knowledge, rarely actually useful compared to a Mutex for complicated
performance reasons.
But we're not threading, so let's just use RefCell
.
First, an observation:
#![allow(unused)] fn main() { let v = vec![1, 2]; let x = &mut v[0]; let y = &mut v[1]; println!("{x} {y}"); }
This doesn't compile, since we're violating the principle of mutability XOR aliasing. But this works:
#![allow(unused)] fn main() { use std::cell::RefCell; let v = vec![ RefCell::new(1), RefCell::new(2), ]; let mut x = v[0].borrow_mut(); let mut y = v[1].borrow_mut(); *x += 1; *y += 1; println!("{x} {y}"); }
This should make at least some intuitive sense now, but to be more clear:
.borrow_mut()
takes&self
, not&mut self
- Thus
x
andy
are immutably borrowing fromv
x
andy
are of typeRefMut
, which can provide a mutable reference to its inner type (but they must be markedmut
to get a mutable reference to them to do so)RefMut
implsDisplay for T: Display
andDeref<Target = T>
, so we can basically use them as if they're&mut T
Now we should understand the tool well enough to put it to use.
Implementation
First let's redefine a few things:
- Add
RefCell
intoSchedule
'sresources
(and also add default derive for convenience)
#[derive(Default)]
struct Scheduler {
systems: Vec<StoredSystem>,
resources: HashMap<TypeId, RefCell<Box<dyn Any>>>,
}
- Wrap resources in
RefCell
inadd_resource
impl Scheduler {
// ...
pub fn add_resource<R: 'static>(&mut self, res: R) {
self.resources
.insert(TypeId::of::<R>(), RefCell::new(Box::new(res)));
}
}
- Add
RefCell
to signature here
trait System {
fn run(&mut self, resources: &mut HashMap<TypeId, RefCell<Box<dyn Any>>>);
}
- And here
trait SystemParam {
type Item<'new>;
fn retrieve<'r>(resources: &'r HashMap<TypeId, RefCell<Box<dyn Any>>>) -> Self::Item<'r>;
}
- Res needs to store a
Ref<Box<dyn Any>>
now instead of&T
, or theRef
will be dropped early
struct Res<'a, T: 'static> {
value: Ref<'a, Box<dyn Any>>,
_marker: PhantomData<&'a T>,
}
- Add
Deref
impl toRes
for convenience
impl<T: 'static> Deref for Res<'_, T> {
type Target = T;
fn deref(&self) -> &T {
self.value.downcast_ref().unwrap()
}
}
- Add a
.borrow()
here to implementRes
trivially
impl<'res, T: 'static> SystemParam for Res<'res, T> {
type Item<'new> = Res<'new, T>;
fn retrieve<'r>(resources: &'r HashMap<TypeId, RefCell<Box<dyn Any>>>) -> Self::Item<'r> {
Res {
value: resources.get(&TypeId::of::<T>()).unwrap().borrow(),
_marker: PhantomData,
}
}
}
And this gives us a functioning Res
again! Now let's implement ResMut
:
- Define
ResMut
(plusDeref
impls):
struct ResMut<'a, T: 'static> {
value: RefMut<'a, Box<dyn Any>>,
_marker: PhantomData<&'a mut T>,
}
impl<T: 'static> Deref for ResMut<'_, T> {
type Target = T;
fn deref(&self) -> &T {
self.value.downcast_ref().unwrap()
}
}
impl<T: 'static> DerefMut for ResMut<'_, T> {
fn deref_mut(&mut self) -> &mut T {
self.value.downcast_mut().unwrap()
}
}
- Impl
SystemParam
for it:
impl<'res, T: 'static> SystemParam for ResMut<'res, T> {
type Item<'new> = ResMut<'new, T>;
fn retrieve<'r>(resources: &'r HashMap<TypeId, RefCell<Box<dyn Any>>>) -> Self::Item<'r> {
ResMut {
value: resources.get(&TypeId::of::<T>()).unwrap().borrow_mut(),
_marker: PhantomData,
}
}
}
And there we go! We can now access multiple resources mutably from systems.
However, we can't actually add owned resources still- one might notice that bevy does not have anything
like this anyway. If you wanted to accomplish this, one way would be to wrap the RefCell
in
resources
with Option
, and then you can use .take()
to remove a resource from resources
entirely to define SystemParam::retrieve
for the owned resource. However this would be niche and
error prone to use, so I'm not going to do it myself.
Final Product
use std::any::{Any, TypeId}; use std::cell::{Ref, RefCell, RefMut}; use std::collections::HashMap; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; struct FunctionSystem<Input, F> { f: F, marker: PhantomData<fn() -> Input>, } trait System { fn run(&mut self, resources: &mut HashMap<TypeId, RefCell<Box<dyn Any>>>); } trait IntoSystem<Input> { type System: System; fn into_system(self) -> Self::System; } type StoredSystem = Box<dyn System>; #[derive(Default)] struct Scheduler { systems: Vec<StoredSystem>, resources: HashMap<TypeId, RefCell<Box<dyn Any>>>, } impl Scheduler { pub fn run(&mut self) { for system in self.systems.iter_mut() { system.run(&mut self.resources); } } pub fn add_system<I, S: System + 'static>(&mut self, system: impl IntoSystem<I, System = S>) { self.systems.push(Box::new(system.into_system())); } pub fn add_resource<R: 'static>(&mut self, res: R) { self.resources .insert(TypeId::of::<R>(), RefCell::new(Box::new(res))); } } trait SystemParam { type Item<'new>; fn retrieve<'r>(resources: &'r HashMap<TypeId, RefCell<Box<dyn Any>>>) -> Self::Item<'r>; } struct Res<'a, T: 'static> { value: Ref<'a, Box<dyn Any>>, _marker: PhantomData<&'a T>, } impl<T: 'static> Deref for Res<'_, T> { type Target = T; fn deref(&self) -> &T { self.value.downcast_ref().unwrap() } } impl<'res, T: 'static> SystemParam for Res<'res, T> { type Item<'new> = Res<'new, T>; fn retrieve<'r>(resources: &'r HashMap<TypeId, RefCell<Box<dyn Any>>>) -> Self::Item<'r> { Res { value: resources.get(&TypeId::of::<T>()).unwrap().borrow(), _marker: PhantomData, } } } struct ResMut<'a, T: 'static> { value: RefMut<'a, Box<dyn Any>>, _marker: PhantomData<&'a mut T>, } impl<T: 'static> Deref for ResMut<'_, T> { type Target = T; fn deref(&self) -> &T { self.value.downcast_ref().unwrap() } } impl<T: 'static> DerefMut for ResMut<'_, T> { fn deref_mut(&mut self) -> &mut T { self.value.downcast_mut().unwrap() } } impl<'res, T: 'static> SystemParam for ResMut<'res, T> { type Item<'new> = ResMut<'new, T>; fn retrieve<'r>(resources: &'r HashMap<TypeId, RefCell<Box<dyn Any>>>) -> Self::Item<'r> { ResMut { value: resources.get(&TypeId::of::<T>()).unwrap().borrow_mut(), _marker: PhantomData, } } } impl<F, T1: SystemParam> System for FunctionSystem<(T1,), F> where for<'a, 'b> &'a mut F: FnMut(T1) + FnMut(<T1 as SystemParam>::Item<'b>) { fn run(&mut self, resources: &mut HashMap<TypeId, RefCell<Box<dyn Any>>>) { // necessary to tell rust exactly which impl to call; it gets a bit confused otherwise fn call_inner<T1>( mut f: impl FnMut(T1), _0: T1 ) { f(_0) } let _0 = T1::retrieve(resources); call_inner(&mut self.f, _0) } } impl<F, T1: SystemParam, T2: SystemParam> System for FunctionSystem<(T1, T2), F> where for<'a, 'b> &'a mut F: FnMut(T1, T2) + FnMut(<T1 as SystemParam>::Item<'b>, <T2 as SystemParam>::Item<'b>) { fn run(&mut self, resources: &mut HashMap<TypeId, RefCell<Box<dyn Any>>>) { // necessary to tell rust exactly which impl to call; it gets a bit confused otherwise fn call_inner<T1, T2>( mut f: impl FnMut(T1, T2), _0: T1, _1: T2 ) { f(_0, _1) } let _0 = T1::retrieve(resources); let _1 = T2::retrieve(resources); call_inner(&mut self.f, _0, _1) } } impl<F: FnMut(T1), T1: SystemParam> IntoSystem<(T1,)> for F where for<'a, 'b> &'a mut F: FnMut(T1) + FnMut(<T1 as SystemParam>::Item<'b>) { type System = FunctionSystem<(T1,), Self>; fn into_system(self) -> Self::System { FunctionSystem { f: self, marker: Default::default(), } } } impl<F: FnMut(T1, T2), T1: SystemParam, T2: SystemParam> IntoSystem<(T1, T2)> for F where for<'a, 'b> &'a mut F: FnMut(T1, T2) + FnMut(<T1 as SystemParam>::Item<'b>, <T2 as SystemParam>::Item<'b>) { type System = FunctionSystem<(T1, T2), Self>; fn into_system(self) -> Self::System { FunctionSystem { f: self, marker: Default::default(), } } } fn main() { let mut scheduler = Scheduler::default(); scheduler.add_system(foo); scheduler.add_system(bar); scheduler.add_resource(12i32); scheduler.add_resource("Hello, world!"); scheduler.run(); } fn foo(mut int: ResMut<i32>) { *int += 1; } fn bar(statement: Res<&'static str>, num: Res<i32>) { assert_eq!(*num, 13); println!("{} My lucky number is: {}", *statement, *num); }
Pretty cool! But this does have one sharp edge (if you run this, it will panic):
use std::any::{Any, TypeId}; use std::cell::{Ref, RefCell, RefMut}; use std::collections::HashMap; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; struct FunctionSystem<Input, F> { f: F, marker: PhantomData<fn() -> Input>, } trait System { fn run(&mut self, resources: &mut HashMap<TypeId, RefCell<Box<dyn Any>>>); } trait IntoSystem<Input> { type System: System; fn into_system(self) -> Self::System; } type StoredSystem = Box<dyn System>; #[derive(Default)] struct Scheduler { systems: Vec<StoredSystem>, resources: HashMap<TypeId, RefCell<Box<dyn Any>>>, } impl Scheduler { pub fn run(&mut self) { for system in self.systems.iter_mut() { system.run(&mut self.resources); } } pub fn add_system<I, S: System + 'static>(&mut self, system: impl IntoSystem<I, System = S>) { self.systems.push(Box::new(system.into_system())); } pub fn add_resource<R: 'static>(&mut self, res: R) { self.resources .insert(TypeId::of::<R>(), RefCell::new(Box::new(res))); } } trait SystemParam { type Item<'new>; fn retrieve<'r>(resources: &'r HashMap<TypeId, RefCell<Box<dyn Any>>>) -> Self::Item<'r>; } struct Res<'a, T: 'static> { value: Ref<'a, Box<dyn Any>>, _marker: PhantomData<&'a T>, } impl<T: 'static> Deref for Res<'_, T> { type Target = T; fn deref(&self) -> &T { self.value.downcast_ref().unwrap() } } impl<'res, T: 'static> SystemParam for Res<'res, T> { type Item<'new> = Res<'new, T>; fn retrieve<'r>(resources: &'r HashMap<TypeId, RefCell<Box<dyn Any>>>) -> Self::Item<'r> { Res { value: resources.get(&TypeId::of::<T>()).unwrap().borrow(), _marker: PhantomData, } } } struct ResMut<'a, T: 'static> { value: RefMut<'a, Box<dyn Any>>, _marker: PhantomData<&'a mut T>, } impl<T: 'static> Deref for ResMut<'_, T> { type Target = T; fn deref(&self) -> &T { self.value.downcast_ref().unwrap() } } impl<T: 'static> DerefMut for ResMut<'_, T> { fn deref_mut(&mut self) -> &mut T { self.value.downcast_mut().unwrap() } } impl<'res, T: 'static> SystemParam for ResMut<'res, T> { type Item<'new> = ResMut<'new, T>; fn retrieve<'r>(resources: &'r HashMap<TypeId, RefCell<Box<dyn Any>>>) -> Self::Item<'r> { ResMut { value: resources.get(&TypeId::of::<T>()).unwrap().borrow_mut(), _marker: PhantomData, } } } impl<F, T1: SystemParam> System for FunctionSystem<(T1,), F> where for<'a, 'b> &'a mut F: FnMut(T1) + FnMut(<T1 as SystemParam>::Item<'b>) { fn run(&mut self, resources: &mut HashMap<TypeId, RefCell<Box<dyn Any>>>) { // necessary to tell rust exactly which impl to call; it gets a bit confused otherwise fn call_inner<T1>( mut f: impl FnMut(T1), _0: T1 ) { f(_0) } let _0 = T1::retrieve(resources); call_inner(&mut self.f, _0) } } impl<F, T1: SystemParam, T2: SystemParam> System for FunctionSystem<(T1, T2), F> where for<'a, 'b> &'a mut F: FnMut(T1, T2) + FnMut(<T1 as SystemParam>::Item<'b>, <T2 as SystemParam>::Item<'b>) { fn run(&mut self, resources: &mut HashMap<TypeId, RefCell<Box<dyn Any>>>) { // necessary to tell rust exactly which impl to call; it gets a bit confused otherwise fn call_inner<T1, T2>( mut f: impl FnMut(T1, T2), _0: T1, _1: T2 ) { f(_0, _1) } let _0 = T1::retrieve(resources); let _1 = T2::retrieve(resources); call_inner(&mut self.f, _0, _1) } } impl<F: FnMut(T1), T1: SystemParam> IntoSystem<(T1,)> for F where for<'a, 'b> &'a mut F: FnMut(T1) + FnMut(<T1 as SystemParam>::Item<'b>) { type System = FunctionSystem<(T1,), Self>; fn into_system(self) -> Self::System { FunctionSystem { f: self, marker: Default::default(), } } } impl<F: FnMut(T1, T2), T1: SystemParam, T2: SystemParam> IntoSystem<(T1, T2)> for F where for<'a, 'b> &'a mut F: FnMut(T1, T2) + FnMut(<T1 as SystemParam>::Item<'b>, <T2 as SystemParam>::Item<'b>) { type System = FunctionSystem<(T1, T2), Self>; fn into_system(self) -> Self::System { FunctionSystem { f: self, marker: Default::default(), } } } fn main() { let mut scheduler = Scheduler::default(); scheduler.add_system(spooky); scheduler.add_resource(13i32); scheduler.run(); } fn spooky(_foo: ResMut<i32>, _bar: ResMut<i32>) { println!("Haha lmao"); }
We of course still can't borrow the same resource mutably multiple times at once, and RefCell
will prevent this by panicking if we ever try to construct an ill-formed system like this. Bevy will
do something similar, but with a better error message; We will show how in the next section.