bevy/examples/reflection/function_reflection.rs
Gino Valente af865e76a3
bevy_reflect: Improve DynamicFunction ergonomics (#14201)
# Objective

Many functions can be converted to `DynamicFunction` using
`IntoFunction`. Unfortunately, we are limited by Rust itself and the
implementations are far from exhaustive. For example, we can't convert
functions with more than 16 arguments. Additionally, we can't handle
returns with lifetimes not tied to the lifetime of the first argument.

In such cases, users will have to create their `DynamicFunction`
manually.

Let's take the following function:

```rust
fn get(index: usize, list: &Vec<String>) -> &String {
    &list[index]
}
```

This function cannot be converted to a `DynamicFunction` via
`IntoFunction` due to the lifetime of the return value being tied to the
second argument. Therefore, we need to construct the `DynamicFunction`
manually:

```rust
DynamicFunction::new(
    |mut args, info| {
        let list = args
            .pop()
            .unwrap()
            .take_ref::<Vec<String>>(&info.args()[1])?;
        let index = args.pop().unwrap().take_owned::<usize>(&info.args()[0])?;
        Ok(Return::Ref(get(index, list)))
    },
    FunctionInfo::new()
        .with_name("get")
        .with_args(vec![
            ArgInfo:🆕:<usize>(0).with_name("index"),
            ArgInfo:🆕:<&Vec<String>>(1).with_name("list"),
        ])
        .with_return_info(ReturnInfo:🆕:<&String>()),
);
```

While still a small and straightforward snippet, there's a decent amount
going on here. There's a lot of room for improvements when it comes to
ergonomics and readability.

The goal of this PR is to address those issues.

## Solution

Improve the ergonomics and readability of manually created
`DynamicFunction`s.

Some of the major changes:
1. Removed the need for `&ArgInfo` when reifying arguments (i.e. the
`&info.args()[1]` calls)
2. Added additional `pop` methods on `ArgList` to handle both popping
and casting
3. Added `take` methods on `ArgList` for taking the arguments out in
order
4. Removed the need for `&FunctionInfo` in the internal closure (Change
1 made it no longer necessary)
5. Added methods to automatically handle generating `ArgInfo` and
`ReturnInfo`

With all these changes in place, we get something a lot nicer to both
write and look at:

```rust
DynamicFunction::new(
    |mut args| {
        let index = args.take::<usize>()?;
        let list = args.take::<&Vec<String>>()?;
        Ok(Return::Ref(get(index, list)))
    },
    FunctionInfo::new()
        .with_name("get")
        .with_arg::<usize>("index")
        .with_arg::<&Vec<String>>("list")
        .with_return::<&String>(),
);
```

Alternatively, to rely on type inference for taking arguments, you could
do:

```rust
DynamicFunction::new(
    |mut args| {
        let index = args.take_owned()?;
        let list = args.take_ref()?;
        Ok(Return::Ref(get(index, list)))
    },
    FunctionInfo::new()
        .with_name("get")
        .with_arg::<usize>("index")
        .with_arg::<&Vec<String>>("list")
        .with_return::<&String>(),
);
```

## Testing

You can test locally by running:

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

---

## Changelog

- Removed `&ArgInfo` argument from `FromArg::from_arg` trait method
- Removed `&ArgInfo` argument from `Arg::take_***` methods
- Added `ArgValue`
- `Arg` is now a struct containing an `ArgValue` and an argument `index`
- `Arg::take_***` methods now require `T` is also `TypePath`
- Added `Arg::new`, `Arg::index`, `Arg::value`, `Arg::take_value`, and
`Arg::take` methods
- Replaced `ArgId` in `ArgError` with just the argument `index`
- Added `ArgError::EmptyArgList`
- Renamed `ArgList::push` to `ArgList::push_arg`
- Added `ArgList::pop_arg`, `ArgList::pop_owned`, `ArgList::pop_ref`,
and `ArgList::pop_mut`
- Added `ArgList::take_arg`, `ArgList::take_owned`, `ArgList::take_ref`,
`ArgList::take_mut`, and `ArgList::take`
- `ArgList::pop` is now generic
- Renamed `FunctionError::InvalidArgCount` to
`FunctionError::ArgCountMismatch`
- The closure given to `DynamicFunction::new` no longer has a
`&FunctionInfo` argument
- Added `FunctionInfo::with_arg`
- Added `FunctionInfo::with_return`

## 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.

* The `FromArg::from_arg` trait method and the `Arg::take_***` methods
no longer take a `&ArgInfo` argument.
* What used to be `Arg` is now `ArgValue`. `Arg` is now a struct which
contains an `ArgValue`.
* `Arg::take_***` methods now require `T` is also `TypePath`
* Instances of `id: ArgId` in `ArgError` have been replaced with `index:
usize`
* `ArgList::push` is now `ArgList::push_arg`. It also takes the new
`ArgValue` type.
* `ArgList::pop` has become `ArgList::pop_arg` and now returns
`ArgValue`. `Arg::pop` now takes a generic type and downcasts to that
type. It's recommended to use `ArgList::take` and friends instead since
they allow removing the arguments from the list in the order they were
pushed (rather than reverse order).
* `FunctionError::InvalidArgCount` is now
`FunctionError::ArgCountMismatch`
* The closure given to `DynamicFunction::new` no longer has a
`&FunctionInfo` argument. This argument can be removed.
2024-07-16 13:01:52 +00:00

173 lines
8.2 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)))
},
FunctionInfo::new()
// We can optionally provide a name for the function.
.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));
}