
# Objective As mentioned in [this](https://github.com/bevyengine/bevy/pull/13152#issuecomment-2198387297) comment, creating a function registry (see #14098) is a bit difficult due to the requirements of `DynamicFunction`. Internally, a `DynamicFunction` contains a `Box<dyn FnMut>` (the function that reifies reflected arguments and calls the actual function), which requires `&mut self` in order to be called. This means that users would require a mutable reference to the function registry for it to be useful— which isn't great. And they can't clone the `DynamicFunction` either because cloning an `FnMut` isn't really feasible (wrapping it in an `Arc` would allow it to be cloned but we wouldn't be able to call the clone since we need a mutable reference to the `FnMut`, which we can't get with multiple `Arc`s still alive, requiring us to also slap in a `Mutex`, which adds additional overhead). And we don't want to just replace the `dyn FnMut` with `dyn Fn` as that would prevent reflecting closures that mutate their environment. Instead, we need to introduce a new type to split the requirements of `DynamicFunction`. ## Solution Introduce new types for representing closures. Specifically, this PR introduces `DynamicClosure` and `DynamicClosureMut`. Similar to how `IntoFunction` exists for `DynamicFunction`, two new traits were introduced: `IntoClosure` and `IntoClosureMut`. Now `DynamicFunction` stores a `dyn Fn` with a `'static` lifetime. `DynamicClosure` also uses a `dyn Fn` but has a lifetime, `'env`, tied to its environment. `DynamicClosureMut` is most like the old `DynamicFunction`, keeping the `dyn FnMut` and also typing its lifetime, `'env`, to the environment Here are some comparison tables: | | `DynamicFunction` | `DynamicClosure` | `DynamicClosureMut` | | - | ----------------- | ---------------- | ------------------- | | Callable with `&self` | ✅ | ✅ | ❌ | | Callable with `&mut self` | ✅ | ✅ | ✅ | | Allows for non-`'static` lifetimes | ❌ | ✅ | ✅ | | | `IntoFunction` | `IntoClosure` | `IntoClosureMut` | | - | -------------- | ------------- | ---------------- | | Convert `fn` functions | ✅ | ✅ | ✅ | | Convert `fn` methods | ✅ | ✅ | ✅ | | Convert anonymous functions | ✅ | ✅ | ✅ | | Convert closures that capture immutable references | ❌ | ✅ | ✅ | | Convert closures that capture mutable references | ❌ | ❌ | ✅ | | Convert closures that capture owned values | ❌[^1] | ✅ | ✅ | [^1]: Due to limitations in Rust, `IntoFunction` can't be implemented for just functions (unless we forced users to manually coerce them to function pointers first). So closures that meet the trait requirements _can technically_ be converted into a `DynamicFunction` as well. To both future-proof and reduce confusion, though, we'll just pretend like this isn't a thing. ```rust let mut list: Vec<i32> = vec![1, 2, 3]; // `replace` is a closure that captures a mutable reference to `list` let mut replace = |index: usize, value: i32| -> i32 { let old_value = list[index]; list[index] = value; old_value }; // Convert the closure into a dynamic closure using `IntoClosureMut::into_closure_mut` let mut func: DynamicClosureMut = replace.into_closure_mut(); // Dynamically call the closure: let args = ArgList::default().push_owned(1_usize).push_owned(-2_i32); let value = func.call_once(args).unwrap().unwrap_owned(); // Check the result: assert_eq!(value.take::<i32>().unwrap(), 2); assert_eq!(list, vec![1, -2, 3]); ``` ### `ReflectFn`/`ReflectFnMut` To make extending the function reflection system easier (the blanket impls for `IntoFunction`, `IntoClosure`, and `IntoClosureMut` are all incredibly short), this PR generalizes callables with two new traits: `ReflectFn` and `ReflectFnMut`. These traits mimic `Fn` and `FnMut` but allow for being called via reflection. In fact, their blanket implementations are identical save for `ReflectFn` being implemented over `Fn` types and `ReflectFnMut` being implemented over `FnMut` types. And just as `Fn` is a subtrait of `FnMut`, `ReflectFn` is a subtrait of `ReflectFnMut`. So anywhere that expects a `ReflectFnMut` can also be given a `ReflectFn`. To reiterate, these traits aren't 100% necessary. They were added in purely for extensibility. If we decide to split things up differently or add new traits/types in the future, then those changes should be much simpler to implement. ### `TypedFunction` Because of the split into `ReflectFn` and `ReflectFnMut`, we needed a new way to access the function type information. This PR moves that concept over into `TypedFunction`. Much like `Typed`, this provides a way to access a function's `FunctionInfo`. By splitting this trait out, it helps to ensure the other traits are focused on a single responsibility. ### Internal Macros The original function PR (#13152) implemented `IntoFunction` using a macro which was passed into an `all_tuples!` macro invocation. Because we needed the same functionality for these new traits, this PR has copy+pasted that code for `ReflectFn`, `ReflectFnMut`, and `TypedFunction`— albeit with some differences between them. Originally, I was going to try and macro-ify the impls and where clauses such that we wouldn't have to straight up duplicate a lot of this logic. However, aside from being more complex in general, autocomplete just does not play nice with such heavily nested macros (tried in both RustRover and VSCode). And both of those problems told me that it just wasn't worth it: we need to ensure the crate is easily maintainable, even at the cost of duplicating code. So instead, I made sure to simplify the macro code by removing all fully-qualified syntax and cutting the where clauses down to the bare essentials, which helps to clean up a lot of the visual noise. I also tried my best to document the macro logic in certain areas (I may even add a bit more) to help with maintainability for future devs. ### Documentation Documentation for this module was a bit difficult for me. So many of these traits and types are very interconnected. And each trait/type has subtle differences that make documenting it in a single place, like at the module level, difficult to do cleanly. Describing the valid signatures is also challenging to do well. Hopefully what I have here is okay. I think I did an okay job, but let me know if there any thoughts on ways to improve it. We can also move such a task to a followup PR for more focused discussion. ## Testing You can test locally by running: ``` cargo test --package bevy_reflect ``` --- ## Changelog - Added `DynamicClosure` struct - Added `DynamicClosureMut` struct - Added `IntoClosure` trait - Added `IntoClosureMut` trait - Added `ReflectFn` trait - Added `ReflectFnMut` trait - Added `TypedFunction` trait - `IntoFunction` now only works for standard Rust functions - `IntoFunction` no longer takes a lifetime parameter - `DynamicFunction::call` now only requires `&self` - Removed `DynamicFunction::call_once` - Changed the `IntoReturn::into_return` signature to include a where clause ## 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. ### `IntoClosure` `IntoFunction` now only works for standard Rust functions. Calling `IntoFunction::into_function` on a closure that captures references to its environment (either mutable or immutable), will no longer compile. Instead, you will need to use either `IntoClosure::into_closure` to create a `DynamicClosure` or `IntoClosureMut::into_closure_mut` to create a `DynamicClosureMut`, depending on your needs: ```rust let punct = String::from("!"); let print = |value: String| { println!("{value}{punct}"); }; // BEFORE let func: DynamicFunction = print.into_function(); // AFTER let func: DynamicClosure = print.into_closure(); ``` ### `IntoFunction` lifetime Additionally, `IntoFunction` no longer takes a lifetime parameter as it always expects a `'static` lifetime. Usages will need to remove any lifetime parameters: ```rust // BEFORE fn execute<'env, F: IntoFunction<'env, Marker>, Marker>(f: F) {/* ... */} // AFTER fn execute<F: IntoFunction<Marker>, Marker>(f: F) {/* ... */} ``` ### `IntoReturn` `IntoReturn::into_return` now has a where clause. Any manual implementors will need to add this where clause to their implementation.
171 lines
8.0 KiB
Rust
171 lines
8.0 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::args::ArgInfo;
|
|
use bevy::reflect::func::{
|
|
ArgList, DynamicClosure, DynamicClosureMut, DynamicFunction, FunctionInfo, IntoClosure,
|
|
IntoClosureMut, IntoFunction, Return, ReturnInfo,
|
|
};
|
|
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, info| {
|
|
let container_info = &info.args()[1];
|
|
let value_info = &info.args()[0];
|
|
|
|
// The `ArgList` contains the arguments in the order they were pushed.
|
|
// Therefore, we need to pop them in reverse order.
|
|
let container = args
|
|
.pop()
|
|
.unwrap()
|
|
.take_mut::<Option<i32>>(container_info)
|
|
.unwrap();
|
|
let value = args.pop().unwrap().take_owned::<i32>(value_info).unwrap();
|
|
|
|
Ok(Return::Ref(get_or_insert(value, container)))
|
|
},
|
|
FunctionInfo::new()
|
|
// We can optionally provide a name for the function
|
|
.with_name("get_or_insert")
|
|
// Since our function takes arguments, we MUST provide that argument information.
|
|
// The arguments should be provided in the order they are defined in the function.
|
|
// This is used to validate any arguments given at runtime.
|
|
.with_args(vec![
|
|
ArgInfo::new::<i32>(0).with_name("value"),
|
|
ArgInfo::new::<&mut Option<i32>>(1).with_name("container"),
|
|
])
|
|
// We can optionally provide return information as well.
|
|
.with_return_info(ReturnInfo::new::<&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));
|
|
}
|