bevy/examples/reflection/function_reflection.rs
Gino Valente df61117850
bevy_reflect: Function registry (#14098)
# Objective

#13152 added support for reflecting functions. Now, we need a way to
register those functions such that they may be accessed anywhere within
the ECS.

## Solution

Added a `FunctionRegistry` type similar to `TypeRegistry`.

This allows a function to be registered and retrieved by name.

```rust
fn foo() -> i32 {
    123
}

let mut registry = FunctionRegistry::default();
registry.register("my_function", foo);

let function = registry.get_mut("my_function").unwrap();
let value = function.call(ArgList::new()).unwrap().unwrap_owned();
assert_eq!(value.downcast_ref::<i32>(), Some(&123));
```

Additionally, I added an `AppFunctionRegistry` resource which wraps a
`FunctionRegistryArc`. Functions can be registered into this resource
using `App::register_function` or by getting a mutable reference to the
resource itself.

### Limitations

#### `Send + Sync`

In order to get this registry to work across threads, it needs to be
`Send + Sync`. This means that `DynamicFunction` needs to be `Send +
Sync`, which means that its internal function also needs to be `Send +
Sync`.

In most cases, this won't be an issue because standard Rust functions
(the type most likely to be registered) are always `Send + Sync`.
Additionally, closures tend to be `Send + Sync` as well, granted they
don't capture any `!Send` or `!Sync` variables.

This PR adds this `Send + Sync` requirement, but as mentioned above, it
hopefully shouldn't be too big of an issue.

#### Closures

Unfortunately, closures can't be registered yet. This will likely be
explored and added in a followup PR.

### Future Work

Besides addressing the limitations listed above, another thing we could
look into is improving the lookup of registered functions. One aspect is
in the performance of hashing strings. The other is in the developer
experience of having to call `std::any::type_name_of_val` to get the
name of their function (assuming they didn't give it a custom name).

## Testing

You can run the tests locally with:

```
cargo test --package bevy_reflect
```

---

## Changelog

- Added `FunctionRegistry`
- Added `AppFunctionRegistry` (a `Resource` available from `bevy_ecs`)
- Added `FunctionRegistryArc`
- Added `FunctionRegistrationError`
- Added `reflect_functions` feature to `bevy_ecs` and `bevy_app`
- `FunctionInfo` is no longer `Default`
- `DynamicFunction` now requires its wrapped function be `Send + Sync`

## Internal Migration Guide

> [!important]
> Function reflection was introduced as part of the 0.15 dev cycle. This
migration guide was written for developers relying on `main` during this
cycle, and is not a breaking change coming from 0.14.

`DynamicFunction` (both those created manually and those created with
`IntoFunction`), now require `Send + Sync`. All standard Rust functions
should meet that requirement. Closures, on the other hand, may not if
they capture any `!Send` or `!Sync` variables from its environment.
2024-08-06 01:09:48 +00:00

177 lines
8.4 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, DynamicClosure, DynamicClosureMut, DynamicFunction, FunctionError, FunctionInfo,
IntoClosure, IntoClosureMut, IntoFunction, Return,
};
use bevy::reflect::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 = 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 Reflect> = return_value.unwrap_owned();
assert_eq!(value.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 `DynamicClosure`
// using the `IntoClosure` trait.
let minimum = 5;
let clamp = |value: i32| value.max(minimum);
let function: DynamicClosure = dbg!(clamp.into_closure());
let args = dbg!(ArgList::new().push_owned(2_i32));
let return_value = dbg!(function.call(args).unwrap());
let value: Box<dyn Reflect> = return_value.unwrap_owned();
assert_eq!(value.take::<i32>().unwrap(), 5);
// We can also handle closures that capture their environment mutably
// using the `IntoClosureMut` trait.
let mut count = 0;
let increment = |amount: i32| count += amount;
let closure: DynamicClosureMut = dbg!(increment.into_closure_mut());
let args = dbg!(ArgList::new().push_owned(5_i32));
// Because `DynamicClosureMut` 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 `DynamicClosureMut::call_once` method.
dbg!(closure.call_once(args).unwrap());
assert_eq!(count, 5);
// As stated before, this 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 Reflect = return_value.unwrap_ref();
assert_eq!(value.downcast_ref::<String>().unwrap(), "Hello, world!");
// Lastly, 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| {
// We can optionally add a check to ensure we were given the correct number of arguments.
if args.len() != 2 {
return Err(FunctionError::ArgCountMismatch {
expected: 2,
received: args.len(),
});
}
// The `ArgList` contains the arguments in the order they were pushed.
// 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)))
},
// All functions require a name.
// We can either give it a custom name or use the function's name as
// derived from `std::any::type_name_of_val`.
FunctionInfo::new(std::any::type_name_of_val(&get_or_insert))
// We can always change the name if needed.
.with_name("get_or_insert")
// Since our function takes arguments, we should provide that argument information.
// This helps ensure that consumers of the function can validate the arguments they
// pass into the function and helps for 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.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.downcast_ref::<i32>(), Some(&5));
}