
# Objective ### TL;DR #14098 added the `FunctionRegistry` but had some last minute complications due to anonymous functions. It ended up going with a "required name" approach to ensure anonymous functions would always have a name. However, this approach isn't ideal for named functions since, by definition, they will always have a name. Therefore, this PR aims to modify function reflection such that we can make function registration easier for named functions, while still allowing anonymous functions to be registered as well. ### Context Function registration (#14098) ran into a little problem: anonymous functions. Anonymous functions, including function pointers, have very non-unique type names. For example, the anonymous function `|a: i32, b: i32| a + b` has the type name of `fn(i32, i32) -> i32`. This obviously means we'd conflict with another function like `|a: i32, b: i32| a - b`. The solution that #14098 landed on was to always require a name during function registration. The downside with this is that named functions (e.g. `fn add(a: i32, b: i32) -> i32 { a + b }`) had to redundantly provide a name. Additionally, manually constructed `DynamicFunction`s also ran into this ergonomics issue. I don't entirely know how the function registry will be used, but I have a strong suspicion that most of its registrations will either be named functions or manually constructed `DynamicFunction`s, with anonymous functions only being used here and there for quick prototyping or adding small functionality. Why then should the API prioritize the anonymous function use case by always requiring a name during registration? #### Telling Functions Apart Rust doesn't provide a lot of out-of-the-box tools for reflecting functions. One of the biggest hurdles in attempting to solve the problem outlined above would be to somehow tell the different kinds of functions apart. Let's briefly recap on the categories of functions in Rust: | Category | Example | | ------------------ | ----------------------------------------- | | Named function | `fn add(a: i32, b: i32) -> i32 { a + b }` | | Closure | `\|a: i32\| a + captured_variable` | | Anonymous function | `\|a: i32, b: i32\| a + b` | | Function pointer | `fn(i32, i32) -> i32` | My first thought was to try and differentiate these categories based on their size. However, we can see that this doesn't quite work: | Category | `size_of` | | ------------------ | --------- | | Named function | 0 | | Closure | 0+ | | Anonymous function | 0 | | Function pointer | 8 | Not only does this not tell anonymous functions from named ones, but it struggles with pretty much all of them. My second then was to differentiate based on type name: | Category | `type_name` | | ------------------ | ----------------------- | | Named function | `foo::bar::baz` | | Closure | `foo::bar::{{closure}}` | | Anonymous function | `fn() -> String` | | Function pointer | `fn() -> String` | This is much better. While it can't distinguish between function pointers and anonymous functions, this doesn't matter too much since we only care about whether we can _name_ the function. So why didn't we implement this in #14098? #### Relying on `type_name` While this solution was known about while working on #14098, it was left out from that PR due to it being potentially controversial. The [docs](https://doc.rust-lang.org/stable/std/any/fn.type_name.html) for `std::any::type_name` state: > The returned string must not be considered to be a unique identifier of a type as multiple types may map to the same type name. Similarly, there is no guarantee that all parts of a type will appear in the returned string: for example, lifetime specifiers are currently not included. In addition, the output may change between versions of the compiler. So that's it then? We can't use `type_name`? Well, this statement isn't so much a rule as it is a guideline. And Bevy is no stranger to bending the rules to make things work or to improve ergonomics. Remember that before `TypePath`, Bevy's scene system was entirely dependent on `type_name`. Not to mention that `type_name` is being used as a key into both the `TypeRegistry` and the `FunctionRegistry`. Bevy's practices aside, can we reliably use `type_name` for this? My answer would be "yes". Anonymous functions are anonymous. They have no name. There's nothing Rust could do to give them a name apart from generating a random string of characters. But remember that this is a diagnostic tool, it doesn't make sense to obfuscate the type by randomizing the output. So changing it to be anything other than what it is now is very unlikely. The only changes that I could potentially see happening are: 1. Closures replace `{{closure}}` with the name of their variable 2. Lifetimes are included in the output I don't think the first is likely to happen, but if it does then it actually works out in our favor: closures are now named! The second point is probably the likeliest. However, adding lifetimes doesn't mean we can't still rely on `type_name` to determine whether or not a function is named. So we should be okay in this case as well. ## Solution Parse the `type_name` of the function in the `TypedFunction` impl to determine if the function is named or anonymous. This once again makes `FunctionInfo::name` optional. For manual constructions of `DynamicFunction`, `FunctionInfo::named` or ``FunctionInfo::anonymous` can be used. The `FunctionRegistry` API has also been reworked to account for this change. `FunctionRegistry::register` no longer takes a name and instead takes it from the supplied function, returning a `FunctionRegistrationError::MissingName` error if the name is `None`. This also doubles as a replacement for the old `FunctionRegistry::register_dynamic` method, which has been removed. To handle anonymous functions, a `FunctionRegistry::register_with_name` method has been added. This works in the same way `FunctionRegistry::register` used to work before this PR. The overwriting methods have been updated in a similar manner, with modifications to `FunctionRegistry::overwrite_registration`, the removal of `FunctionRegistry::overwrite_registration_dynamic`, and the addition of `FunctionRegistry::overwrite_registration_with_name`. This PR also updates the methods on `App` in a similar way: `App::register_function` no longer requires a name argument and `App::register_function_with_name` has been added to handle anonymous functions (and eventually closures). ## Testing You can run the tests locally by running: ``` cargo test --package bevy_reflect --features functions ``` --- ## 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. > [!note] > This list is not exhaustive. It only contains some of the most important changes. `FunctionRegistry::register` no longer requires a name string for named functions. Anonymous functions, however, need to be registered using `FunctionRegistry::register_with_name`. ```rust // BEFORE registry .register(std::any::type_name_of_val(&foo), foo)? .register("bar", || println!("Hello world!")); // AFTER registry .register(foo)? .register_with_name("bar", || println!("Hello world!")); ``` `FunctionInfo::name` is now optional. Anonymous functions and closures will now have their name set to `None` by default. Additionally, `FunctionInfo::new` has been renamed to `FunctionInfo::named`.
181 lines
8.7 KiB
Rust
181 lines
8.7 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)))
|
|
},
|
|
// 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`.
|
|
FunctionInfo::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 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));
|
|
}
|