mirror of
https://github.com/bevyengine/bevy
synced 2024-12-25 12:33:07 +00:00
d21c7a1911
# Objective
Currently function reflection requires users to manually monomorphize
their generic functions. For example:
```rust
fn add<T: Add<Output=T>>(a: T, b: T) -> T {
a + b
}
// We have to specify the type of `T`:
let reflect_add = add::<i32>.into_function();
```
This PR doesn't aim to solve that problem—this is just a limitation in
Rust. However, it also means that reflected functions can only ever work
for a single monomorphization. If we wanted to support other types for
`T`, we'd have to create a separate function for each one:
```rust
let reflect_add_i32 = add::<i32>.into_function();
let reflect_add_u32 = add::<u32>.into_function();
let reflect_add_f32 = add::<f32>.into_function();
// ...
```
So in addition to requiring manual monomorphization, we also lose the
benefit of having a single function handle multiple argument types.
If a user wanted to create a small modding script that utilized function
reflection, they'd have to either:
- Store all sets of supported monomorphizations and require users to
call the correct one
- Write out some logic to find the correct function based on the given
arguments
While the first option would work, it wouldn't be very ergonomic. The
second option is better, but it adds additional complexity to the user's
logic—complexity that `bevy_reflect` could instead take on.
## Solution
Introduce [function
overloading](https://en.wikipedia.org/wiki/Function_overloading).
A `DynamicFunction` can now be overloaded with other `DynamicFunction`s.
We can rewrite the above code like so:
```rust
let reflect_add = add::<i32>
.into_function()
.with_overload(add::<u32>)
.with_overload(add::<f32>);
```
When invoked, the `DynamicFunction` will attempt to find a matching
overload for the given set of arguments.
And while I went into this PR only looking to improve generic function
reflection, I accidentally added support for variadic functions as well
(hence why I use the broader term "overload" over "generic").
```rust
// Supports 1 to 4 arguments
let multiply_all = (|a: i32| a)
.into_function()
.with_overload(|a: i32, b: i32| a * b)
.with_overload(|a: i32, b: i32, c: i32| a * b * c)
.with_overload(|a: i32, b: i32, c: i32, d: i32| a * b * c * d);
```
This is simply an added bonus to this particular implementation. ~~Full
variadic support (i.e. allowing for an indefinite number of arguments)
will be added in a later PR.~~ I actually decided to limit the maximum
number of arguments to 63 to supplement faster lookups, a reduced memory
footprint, and faster cloning.
### Alternatives & Rationale
I explored a few options for handling generic functions. This PR is the
one I feel the most confident in, but I feel I should mention the others
and why I ultimately didn't move forward with them.
#### Adding `GenericDynamicFunction`
**TL;DR:** Adding a distinct `GenericDynamicFunction` type unnecessarily
splits and complicates the API.
<details>
<summary>Details</summary>
My initial explorations involved a dedicated `GenericDynamicFunction` to
contain and handle the mappings.
This was initially started back when `DynamicFunction` was distinct from
`DynamicClosure`. My goal was to not prevent us from being able to
somehow make `DynamicFunction` implement `Copy`. But once we reverted
back to a single `DynamicFunction`, that became a non-issue.
But that aside, the real problem was that it created a split in the API.
If I'm using a third-party library that uses function reflection, I have
to know whether to request a `DynamicFunction` or a
`GenericDynamicFunction`. I might not even know ahead of time which one
I want. It might need to be determined at runtime.
And if I'm creating a library, I might want a type to contain both
`DynamicFunction` and `GenericDynamicFunction`. This might not be
possible if, for example, I need to store the function in a `HashMap`.
The other concern is with `IntoFunction`. Right now `DynamicFunction`
trivially implements `IntoFunction` since it can just return itself. But
what should `GenericDynamicFunction` do? It could return itself wrapped
into a `DynamicFunction`, but then the API for `DynamicFunction` would
have to account for this. So then what was the point of having a
separate `GenericDynamicFunction` anyways?
And even apart from `IntoFunction`, there's nothing stopping someone
from manually creating a generic `DynamicFunction` through lying about
its `FunctionInfo` and wrapping a `GenericDynamicFunction`.
That being said, this is probably the "best" alternative if we added a
`Function` trait and stored functions as `Box<dyn Function>`.
However, I'm not convinced we gain much from this. Sure, we could keep
the API for `DynamicFunction` the same, but consumers of `Function` will
need to account for `GenericDynamicFunction` regardless (e.g. handling
multiple `FunctionInfo`, a ranged argument count, etc.). And for all
cases, except where using `DynamicFunction` directly, you end up
treating them all like `GenericDynamicFunction`.
Right now, if we did go with `GenericDynamicFunction`, the only major
benefit we'd gain would be saving 24 bytes. If memory ever does become
an issue here, we could swap over. But I think for the time being it's
better for us to pursue a clearer mental model and end-user ergonomics
through unification.
</details>
##### Using the `FunctionRegistry`
**TL;DR:** Having overloads only exist in the `FunctionRegistry`
unnecessarily splits and complicates the API.
<details>
<summary>Details</summary>
Another idea was to store the overloads in the `FunctionRegistry`. Users
would then just call functions directly through the registry (i.e.
`registry.call("my_func", my_args)`).
I didn't go with this option because of how it specifically relies on
the functions being registered. You'd not only always need access to the
registry, but you'd need to ensure that the functions you want to call
are even registered.
It also means you can't just store a generic `DynamicFunction` on a
type. Instead, you'll need to store the function's name and use that to
look up the function in the registry—even if it's only ever used by that
type.
Doing so also removes all the benefits of `DynamicFunction`, such as the
ability to pass it to functions accepting `IntoFunction`, modify it if
needed, and so on.
Like `GenericDynamicFunction` this introduces a split in the ecosystem:
you either store `DynamicFunction`, store a string to look up the
function, or force `DynamicFunction` to wrap your generic function
anyways. Or worse yet: have `DynamicFunction` wrap the lookup function
using `FunctionRegistryArc`.
</details>
#### Generic `ArgInfo`
**TL;DR:** Allowing `ArgInfo` and `ReturnInfo` to store the generic
information introduces a footgun when interpreting `FunctionInfo`.
<details>
<summary>Details</summary>
Regardless of how we represent a generic function, one thing is clear:
we need to be able to represent the information for such a function.
This PR does so by introducing a `FunctionInfoType` enum to wrap one or
more `FunctionInfo` values.
Originally, I didn't do this. I had `ArgInfo` and `ReturnInfo` allow for
generic types. This allowed us to have a single `FunctionInfo` to
represent our function, but then I realized that it actually lies about
our function.
If we have two `ArgInfo` that both allow for either `i32` or `u32`, what
does this tell us about our function? It turns out: nothing! We can't
know whether our function takes `(i32, i32)`, `(u32, u32)`, `(i32,
u32)`, or `(u32, i32)`.
It therefore makes more sense to just represent a function with multiple
`FunctionInfo` since that's really what it's made up of.
</details>
#### Flatten `FunctionInfo`
**TL;DR:** Flattening removes additional per-overload information some
users may desire and prevents us from adding more information in the
future.
<details>
<summary>Details</summary>
Why don't we just flatten multiple `FunctionInfo` into just one that can
contain multiple signatures?
This is something we could do, but I decided against it for a few
reasons:
- The only thing we'd be able to get rid of for each signature would be
the `name`. While not enough to not do it, it doesn't really suggest we
*have* to either.
- Some consumers may want access to the names of the functions that make
up the overloaded function. For example, to track a bug where an
undesirable function is being added as an overload. Or to more easily
locate the original function of an overload.
- We may eventually allow for more information to be stored on
`FunctionInfo`. For example, we may allow for documentation to be stored
like we do for `TypeInfo`. Consumers of this documentation may want
access to the documentation of each overload as they may provide
documentation specific to that overload.
</details>
## Testing
This PR adds lots of tests and benchmarks, and also adds to the example.
To run the tests:
```
cargo test --package bevy_reflect --all-features
```
To run the benchmarks:
```
cargo bench --bench reflect_function --all-features
```
To run the example:
```
cargo run --package bevy --example function_reflection --all-features
```
### Benchmarks
One of my goals with this PR was to leave the typical case of
non-overloaded functions largely unaffected by the changes introduced in
this PR. ~~And while the static size of `DynamicFunction` has increased
by 17% (from 136 to 160 bytes), the performance has generally stayed the
same~~ The static size of `DynamicFunction` has decreased from 136 to
112 bytes, while calling performance has generally stayed the same:
| | `main` | 7d293ab | 252f3897d
|
|-------------------------------------|--------|---------|-----------|
| `into/function` | 37 ns | 46 ns | 142 ns |
| `with_overload/01_simple_overload` | - | 149 ns | 268 ns |
| `with_overload/01_complex_overload` | - | 332 ns | 431 ns |
| `with_overload/10_simple_overload` | - | 1266 ns | 2618 ns |
| `with_overload/10_complex_overload` | - | 2544 ns | 4170 ns |
| `call/function` | 57 ns | 58 ns | 61 ns |
| `call/01_simple_overload` | - | 255 ns | 242 ns |
| `call/01_complex_overload` | - | 595 ns | 431 ns |
| `call/10_simple_overload` | - | 740 ns | 699 ns |
| `call/10_complex_overload` | - | 1824 ns | 1618 ns |
For the overloaded function tests, the leading number indicates how many
overloads there are: `01` indicates 1 overload, `10` indicates 10
overloads. The `complex` cases have 10 unique generic types and 10
arguments, compared to the `simple` 1 generic type and 2 arguments.
I aimed to prioritize the performance of calling the functions over
creating them, hence creation speed tends to be a bit slower.
There may be other optimizations we can look into but that's probably
best saved for a future PR.
The important bit is that the standard ~~`into/function`~~ and
`call/function` benchmarks show minimal regressions. Since the latest
changes, `into/function` does have some regressions, but again the
priority was `call/function`. We can probably optimize `into/function`
if needed in the future.
---
## Showcase
Function reflection now supports [function
overloading](https://en.wikipedia.org/wiki/Function_overloading)! This
can be used to simulate generic functions:
```rust
fn add<T: Add<Output=T>>(a: T, b: T) -> T {
a + b
}
let reflect_add = add::<i32>
.into_function()
.with_overload(add::<u32>)
.with_overload(add::<f32>);
let args = ArgList::default().push_owned(25_i32).push_owned(75_i32);
let result = func.call(args).unwrap().unwrap_owned();
assert_eq!(result.try_take::<i32>().unwrap(), 100);
let args = ArgList::default().push_owned(25.0_f32).push_owned(75.0_f32);
let result = func.call(args).unwrap().unwrap_owned();
assert_eq!(result.try_take::<f32>().unwrap(), 100.0);
```
You can also simulate variadic functions:
```rust
#[derive(Reflect, PartialEq, Debug)]
struct Player {
name: Option<String>,
health: u32,
}
// Creates a `Player` with one of the following:
// - No name and 100 health
// - A name and 100 health
// - No name and custom health
// - A name and custom health
let create_player = (|| Player {
name: None,
health: 100,
})
.into_function()
.with_overload(|name: String| Player {
name: Some(name),
health: 100,
})
.with_overload(|health: u32| Player {
name: None,
health
})
.with_overload(|name: String, health: u32| Player {
name: Some(name),
health,
});
let args = ArgList::default()
.push_owned(String::from("Urist"))
.push_owned(55_u32);
let player = create_player
.call(args)
.unwrap()
.unwrap_owned()
.try_take::<Player>()
.unwrap();
assert_eq!(
player,
Player {
name: Some(String::from("Urist")),
health: 55
}
);
```
220 lines
10 KiB
Rust
220 lines
10 KiB
Rust
//! This example demonstrates how functions can be called dynamically using reflection.
|
|
//!
|
|
//! Function reflection is useful for calling regular Rust functions in a dynamic context,
|
|
//! where the types of arguments, return values, and even the function itself aren't known at compile time.
|
|
//!
|
|
//! This can be used for things like adding scripting support to your application,
|
|
//! processing deserialized reflection data, or even just storing type-erased versions of your functions.
|
|
|
|
use bevy::reflect::{
|
|
func::{
|
|
ArgList, DynamicFunction, DynamicFunctionMut, FunctionResult, IntoFunction,
|
|
IntoFunctionMut, Return, SignatureInfo,
|
|
},
|
|
PartialReflect, Reflect,
|
|
};
|
|
|
|
// Note that the `dbg!` invocations are used purely for demonstration purposes
|
|
// and are not strictly necessary for the example to work.
|
|
fn main() {
|
|
// There are times when it may be helpful to store a function away for later.
|
|
// In Rust, we can do this by storing either a function pointer or a function trait object.
|
|
// For example, say we wanted to store the following function:
|
|
fn add(left: i32, right: i32) -> i32 {
|
|
left + right
|
|
}
|
|
|
|
// We could store it as either of the following:
|
|
let fn_pointer: fn(i32, i32) -> i32 = add;
|
|
let fn_trait_object: Box<dyn Fn(i32, i32) -> i32> = Box::new(add);
|
|
|
|
// And we can call them like so:
|
|
let result = fn_pointer(2, 2);
|
|
assert_eq!(result, 4);
|
|
let result = fn_trait_object(2, 2);
|
|
assert_eq!(result, 4);
|
|
|
|
// However, you'll notice that we have to know the types of the arguments and return value at compile time.
|
|
// This means there's not really a way to store or call these functions dynamically at runtime.
|
|
// Luckily, Bevy's reflection crate comes with a set of tools for doing just that!
|
|
// We do this by first converting our function into the reflection-based `DynamicFunction` type
|
|
// using the `IntoFunction` trait.
|
|
let function: DynamicFunction<'static> = dbg!(add.into_function());
|
|
|
|
// This time, you'll notice that `DynamicFunction` doesn't take any information about the function's arguments or return value.
|
|
// This is because `DynamicFunction` checks the types of the arguments and return value at runtime.
|
|
// Now we can generate a list of arguments:
|
|
let args: ArgList = dbg!(ArgList::new().push_owned(2_i32).push_owned(2_i32));
|
|
|
|
// And finally, we can call the function.
|
|
// This returns a `Result` indicating whether the function was called successfully.
|
|
// For now, we'll just unwrap it to get our `Return` value,
|
|
// which is an enum containing the function's return value.
|
|
let return_value: Return = dbg!(function.call(args).unwrap());
|
|
|
|
// The `Return` value can be pattern matched or unwrapped to get the underlying reflection data.
|
|
// For the sake of brevity, we'll just unwrap it here and downcast it to the expected type of `i32`.
|
|
let value: Box<dyn PartialReflect> = return_value.unwrap_owned();
|
|
assert_eq!(value.try_take::<i32>().unwrap(), 4);
|
|
|
|
// The same can also be done for closures that capture references to their environment.
|
|
// Closures that capture their environment immutably can be converted into a `DynamicFunction`
|
|
// using the `IntoFunction` trait.
|
|
let minimum = 5;
|
|
let clamp = |value: i32| value.max(minimum);
|
|
|
|
let function: DynamicFunction = dbg!(clamp.into_function());
|
|
let args = dbg!(ArgList::new().push_owned(2_i32));
|
|
let return_value = dbg!(function.call(args).unwrap());
|
|
let value: Box<dyn PartialReflect> = return_value.unwrap_owned();
|
|
assert_eq!(value.try_take::<i32>().unwrap(), 5);
|
|
|
|
// We can also handle closures that capture their environment mutably
|
|
// using the `IntoFunctionMut` trait.
|
|
let mut count = 0;
|
|
let increment = |amount: i32| count += amount;
|
|
|
|
let closure: DynamicFunctionMut = dbg!(increment.into_function_mut());
|
|
let args = dbg!(ArgList::new().push_owned(5_i32));
|
|
|
|
// Because `DynamicFunctionMut` mutably borrows `total`,
|
|
// it will need to be dropped before `total` can be accessed again.
|
|
// This can be done manually with `drop(closure)` or by using the `DynamicFunctionMut::call_once` method.
|
|
dbg!(closure.call_once(args).unwrap());
|
|
assert_eq!(count, 5);
|
|
|
|
// Generic functions can also be converted into a `DynamicFunction`,
|
|
// however, they will need to be manually monomorphized first.
|
|
fn stringify<T: ToString>(value: T) -> String {
|
|
value.to_string()
|
|
}
|
|
|
|
// We have to manually specify the concrete generic type we want to use.
|
|
let function = stringify::<i32>.into_function();
|
|
|
|
let args = ArgList::new().push_owned(123_i32);
|
|
let return_value = function.call(args).unwrap();
|
|
let value: Box<dyn PartialReflect> = return_value.unwrap_owned();
|
|
assert_eq!(value.try_take::<String>().unwrap(), "123");
|
|
|
|
// To make things a little easier, we can also "overload" functions.
|
|
// This makes it so that a single `DynamicFunction` can represent multiple functions,
|
|
// and the correct one is chosen based on the types of the arguments.
|
|
// Each function overload must have a unique argument signature.
|
|
let function = stringify::<i32>
|
|
.into_function()
|
|
.with_overload(stringify::<f32>);
|
|
|
|
// Now our `function` accepts both `i32` and `f32` arguments.
|
|
let args = ArgList::new().push_owned(1.23_f32);
|
|
let return_value = function.call(args).unwrap();
|
|
let value: Box<dyn PartialReflect> = return_value.unwrap_owned();
|
|
assert_eq!(value.try_take::<String>().unwrap(), "1.23");
|
|
|
|
// Function overloading even allows us to have a variable number of arguments.
|
|
let function = (|| 0)
|
|
.into_function()
|
|
.with_overload(|a: i32| a)
|
|
.with_overload(|a: i32, b: i32| a + b)
|
|
.with_overload(|a: i32, b: i32, c: i32| a + b + c);
|
|
|
|
let args = ArgList::new()
|
|
.push_owned(1_i32)
|
|
.push_owned(2_i32)
|
|
.push_owned(3_i32);
|
|
let return_value = function.call(args).unwrap();
|
|
let value: Box<dyn PartialReflect> = return_value.unwrap_owned();
|
|
assert_eq!(value.try_take::<i32>().unwrap(), 6);
|
|
|
|
// As stated earlier, `IntoFunction` works for many kinds of simple functions.
|
|
// Functions with non-reflectable arguments or return values may not be able to be converted.
|
|
// Generic functions are also not supported (unless manually monomorphized like `foo::<i32>.into_function()`).
|
|
// Additionally, the lifetime of the return value is tied to the lifetime of the first argument.
|
|
// However, this means that many methods (i.e. functions with a `self` parameter) are also supported:
|
|
#[derive(Reflect, Default)]
|
|
struct Data {
|
|
value: String,
|
|
}
|
|
|
|
impl Data {
|
|
fn set_value(&mut self, value: String) {
|
|
self.value = value;
|
|
}
|
|
|
|
// Note that only `&'static str` implements `Reflect`.
|
|
// To get around this limitation we can use `&String` instead.
|
|
fn get_value(&self) -> &String {
|
|
&self.value
|
|
}
|
|
}
|
|
|
|
let mut data = Data::default();
|
|
|
|
let set_value = dbg!(Data::set_value.into_function());
|
|
let args = dbg!(ArgList::new().push_mut(&mut data)).push_owned(String::from("Hello, world!"));
|
|
dbg!(set_value.call(args).unwrap());
|
|
assert_eq!(data.value, "Hello, world!");
|
|
|
|
let get_value = dbg!(Data::get_value.into_function());
|
|
let args = dbg!(ArgList::new().push_ref(&data));
|
|
let return_value = dbg!(get_value.call(args).unwrap());
|
|
let value: &dyn PartialReflect = return_value.unwrap_ref();
|
|
assert_eq!(value.try_downcast_ref::<String>().unwrap(), "Hello, world!");
|
|
|
|
// For more complex use cases, you can always create a custom `DynamicFunction` manually.
|
|
// This is useful for functions that can't be converted via the `IntoFunction` trait.
|
|
// For example, this function doesn't implement `IntoFunction` due to the fact that
|
|
// the lifetime of the return value is not tied to the lifetime of the first argument.
|
|
fn get_or_insert(value: i32, container: &mut Option<i32>) -> &i32 {
|
|
if container.is_none() {
|
|
*container = Some(value);
|
|
}
|
|
|
|
container.as_ref().unwrap()
|
|
}
|
|
|
|
let get_or_insert_function = dbg!(DynamicFunction::new(
|
|
|mut args: ArgList| -> FunctionResult {
|
|
// The `ArgList` contains the arguments in the order they were pushed.
|
|
// The `DynamicFunction` will validate that the list contains
|
|
// exactly the number of arguments we expect.
|
|
// We can retrieve them out in order (note that this modifies the `ArgList`):
|
|
let value = args.take::<i32>()?;
|
|
let container = args.take::<&mut Option<i32>>()?;
|
|
|
|
// We could have also done the following to make use of type inference:
|
|
// let value = args.take_owned()?;
|
|
// let container = args.take_mut()?;
|
|
|
|
Ok(Return::Ref(get_or_insert(value, container)))
|
|
},
|
|
// Functions can be either anonymous or named.
|
|
// It's good practice, though, to try and name your functions whenever possible.
|
|
// This makes it easier to debug and is also required for function registration.
|
|
// We can either give it a custom name or use the function's type name as
|
|
// derived from `std::any::type_name_of_val`.
|
|
SignatureInfo::named(std::any::type_name_of_val(&get_or_insert))
|
|
// We can always change the name if needed.
|
|
// It's a good idea to also ensure that the name is unique,
|
|
// such as by using its type name or by prefixing it with your crate name.
|
|
.with_name("my_crate::get_or_insert")
|
|
// Since our function takes arguments, we should provide that argument information.
|
|
// This is used to validate arguments when calling the function.
|
|
// And it aids consumers of the function with their own validation and debugging.
|
|
// Arguments should be provided in the order they are defined in the function.
|
|
.with_arg::<i32>("value")
|
|
.with_arg::<&mut Option<i32>>("container")
|
|
// We can provide return information as well.
|
|
.with_return::<&i32>(),
|
|
));
|
|
|
|
let mut container: Option<i32> = None;
|
|
|
|
let args = dbg!(ArgList::new().push_owned(5_i32).push_mut(&mut container));
|
|
let value = dbg!(get_or_insert_function.call(args).unwrap()).unwrap_ref();
|
|
assert_eq!(value.try_downcast_ref::<i32>(), Some(&5));
|
|
|
|
let args = dbg!(ArgList::new().push_owned(500_i32).push_mut(&mut container));
|
|
let value = dbg!(get_or_insert_function.call(args).unwrap()).unwrap_ref();
|
|
assert_eq!(value.try_downcast_ref::<i32>(), Some(&5));
|
|
}
|