bevy/crates/bevy_reflect/src/func/function.rs
Gino Valente 276dd04001
bevy_reflect: Function reflection (#13152)
# Objective

We're able to reflect types sooooooo... why not functions?

The goal of this PR is to make functions callable within a dynamic
context, where type information is not readily available at compile
time.

For example, if we have a function:

```rust
fn add(left: i32, right: i32) -> i32 {
  left + right
}
```

And two `Reflect` values we've already validated are `i32` types:

```rust
let left: Box<dyn Reflect> = Box::new(2_i32);
let right: Box<dyn Reflect> = Box::new(2_i32);
```

We should be able to call `add` with these values:

```rust
// ?????
let result: Box<dyn Reflect> = add.call_dynamic(left, right);
```

And ideally this wouldn't just work for functions, but methods and
closures too!

Right now, users have two options:

1. Manually parse the reflected data and call the function themselves
2. Rely on registered type data to handle the conversions for them

For a small function like `add`, this isn't too bad. But what about for
more complex functions? What about for many functions?

At worst, this process is error-prone. At best, it's simply tedious.

And this is assuming we know the function at compile time. What if we
want to accept a function dynamically and call it with our own
arguments?

It would be much nicer if `bevy_reflect` could alleviate some of the
problems here.

## Solution

Added function reflection!

This adds a `DynamicFunction` type to wrap a function dynamically. This
can be called with an `ArgList`, which is a dynamic list of
`Reflect`-containing `Arg` arguments. It returns a `FunctionResult`
which indicates whether or not the function call succeeded, returning a
`Reflect`-containing `Return` type if it did succeed.

Many functions can be converted into this `DynamicFunction` type thanks
to the `IntoFunction` trait.

Taking our previous `add` example, this might look something like
(explicit types added for readability):

```rust
fn add(left: i32, right: i32) -> i32 {
  left + right
}

let mut function: DynamicFunction = add.into_function();
let args: ArgList = ArgList::new().push_owned(2_i32).push_owned(2_i32);
let result: Return = function.call(args).unwrap();
let value: Box<dyn Reflect> = result.unwrap_owned();
assert_eq!(value.take::<i32>().unwrap(), 4);
```

And it also works on closures:

```rust
let add = |left: i32, right: i32| left + right;

let mut function: DynamicFunction = add.into_function();
let args: ArgList = ArgList::new().push_owned(2_i32).push_owned(2_i32);
let result: Return = function.call(args).unwrap();
let value: Box<dyn Reflect> = result.unwrap_owned();
assert_eq!(value.take::<i32>().unwrap(), 4);
```

As well as methods:

```rust
#[derive(Reflect)]
struct Foo(i32);

impl Foo {
  fn add(&mut self, value: i32) {
    self.0 += value;
  }
}

let mut foo = Foo(2);

let mut function: DynamicFunction = Foo::add.into_function();
let args: ArgList = ArgList::new().push_mut(&mut foo).push_owned(2_i32);
function.call(args).unwrap();
assert_eq!(foo.0, 4);
```

### Limitations

While this does cover many functions, it is far from a perfect system
and has quite a few limitations. Here are a few of the limitations when
using `IntoFunction`:

1. The lifetime of the return value is only tied to the lifetime of the
first argument (useful for methods). This means you can't have a
function like `(a: i32, b: &i32) -> &i32` without creating the
`DynamicFunction` manually.
2. Only 15 arguments are currently supported. If the first argument is a
(mutable) reference, this number increases to 16.
3. Manual implementations of `Reflect` will need to implement the new
`FromArg`, `GetOwnership`, and `IntoReturn` traits in order to be used
as arguments/return types.

And some limitations of `DynamicFunction` itself:

1. All arguments share the same lifetime, or rather, they will shrink to
the shortest lifetime.
2. Closures that capture their environment may need to have their
`DynamicFunction` dropped before accessing those variables again (there
is a `DynamicFunction::call_once` to make this a bit easier)
3. All arguments and return types must implement `Reflect`. While not a
big surprise coming from `bevy_reflect`, this implementation could
actually still work by swapping `Reflect` out with `Any`. Of course,
that makes working with the arguments and return values a bit harder.
4. Generic functions are not supported (unless they have been manually
monomorphized)

And general, reflection gotchas:

1. `&str` does not implement `Reflect`. Rather, `&'static str`
implements `Reflect` (the same is true for `&Path` and similar types).
This means that `&'static str` is considered an "owned" value for the
sake of generating arguments. Additionally, arguments and return types
containing `&str` will assume it's `&'static str`, which is almost never
the desired behavior. In these cases, the only solution (I believe) is
to use `&String` instead.

### Followup Work

This PR is the first of two PRs I intend to work on. The second PR will
aim to integrate this new function reflection system into the existing
reflection traits and `TypeInfo`. The goal would be to register and call
a reflected type's methods dynamically.

I chose not to do that in this PR since the diff is already quite large.
I also want the discussion for both PRs to be focused on their own
implementation.

Another followup I'd like to do is investigate allowing common container
types as a return type, such as `Option<&[mut] T>` and `Result<&[mut] T,
E>`. This would allow even more functions to opt into this system. I
chose to not include it in this one, though, for the same reasoning as
previously mentioned.

### Alternatives

One alternative I had considered was adding a macro to convert any
function into a reflection-based counterpart. The idea would be that a
struct that wraps the function would be created and users could specify
which arguments and return values should be `Reflect`. It could then be
called via a new `Function` trait.

I think that could still work, but it will be a fair bit more involved,
requiring some slightly more complex parsing. And it of course is a bit
more work for the user, since they need to create the type via macro
invocation.

It also makes registering these functions onto a type a bit more
complicated (depending on how it's implemented).

For now, I think this is a fairly simple, yet powerful solution that
provides the least amount of friction for users.

---

## Showcase

Bevy now adds support for storing and calling functions dynamically
using reflection!

```rust
// 1. Take a standard Rust function
fn add(left: i32, right: i32) -> i32 {
  left + right
}

// 2. Convert it into a type-erased `DynamicFunction` using the `IntoFunction` trait
let mut function: DynamicFunction = add.into_function();
// 3. Define your arguments from reflected values
let args: ArgList = ArgList::new().push_owned(2_i32).push_owned(2_i32);
// 4. Call the function with your arguments
let result: Return = function.call(args).unwrap();
// 5. Extract the return value
let value: Box<dyn Reflect> = result.unwrap_owned();
assert_eq!(value.take::<i32>().unwrap(), 4);
```

## Changelog

#### TL;DR

- Added support for function reflection
- Added a new `Function Reflection` example:
ba727898f2/examples/reflection/function_reflection.rs (L1-L157)

#### Details

Added the following items:

- `ArgError` enum
- `ArgId` enum
- `ArgInfo` struct
- `ArgList` struct
- `Arg` enum
- `DynamicFunction` struct
- `FromArg` trait (derived with `derive(Reflect)`)
- `FunctionError` enum
- `FunctionInfo` struct
- `FunctionResult` alias
- `GetOwnership` trait (derived with `derive(Reflect)`)
- `IntoFunction` trait (with blanket implementation)
- `IntoReturn` trait (derived with `derive(Reflect)`)
- `Ownership` enum
- `ReturnInfo` struct
- `Return` enum

---------

Co-authored-by: Periwink <charlesbour@gmail.com>
2024-07-01 13:49:08 +00:00

221 lines
7.6 KiB
Rust

use crate::func::args::{ArgInfo, ArgList};
use crate::func::error::FunctionError;
use crate::func::info::FunctionInfo;
use crate::func::return_type::Return;
use crate::func::{IntoFunction, ReturnInfo};
use alloc::borrow::Cow;
use core::fmt::{Debug, Formatter};
use std::ops::DerefMut;
/// The result of calling a dynamic [`DynamicFunction`].
///
/// Returns `Ok(value)` if the function was called successfully,
/// where `value` is the [`Return`] value of the function.
pub type FunctionResult<'a> = Result<Return<'a>, FunctionError>;
/// A dynamic representation of a Rust function.
///
/// Internally this stores a function pointer and associated info.
///
/// You will generally not need to construct this manually.
/// Instead, many functions and closures can be automatically converted using the [`IntoFunction`] trait.
///
/// # Example
///
/// Most of the time, a [`DynamicFunction`] can be created using the [`IntoFunction`] trait:
///
/// ```
/// # use bevy_reflect::func::args::ArgList;
/// # use bevy_reflect::func::{DynamicFunction, IntoFunction};
/// fn add(a: i32, b: i32) -> i32 {
/// a + b
/// }
///
/// // Convert the function into a dynamic function using `IntoFunction::into_function`
/// let mut func: DynamicFunction = add.into_function();
///
/// // Dynamically call the function:
/// let args = ArgList::default().push_owned(25_i32).push_owned(75_i32);
/// let value = func.call(args).unwrap().unwrap_owned();
///
/// // Check the result:
/// assert_eq!(value.downcast_ref::<i32>(), Some(&100));
/// ```
///
/// However, in some cases, these functions may need to be created manually:
///
/// ```
/// # use bevy_reflect::func::{ArgList, DynamicFunction, FunctionInfo, IntoFunction, Return, ReturnInfo};
/// # use bevy_reflect::func::args::ArgInfo;
/// fn append(value: String, list: &mut Vec<String>) -> &mut String {
/// list.push(value);
/// list.last_mut().unwrap()
/// }
///
/// // Due to the return value being a reference that is not tied to the first argument,
/// // this will fail to compile:
/// // let mut func: DynamicFunction = append.into_function();
///
/// // Instead, we need to define the function manually.
/// // We start by defining the shape of the function:
/// let info = FunctionInfo::new()
/// .with_name("append")
/// .with_args(vec![
/// ArgInfo::new::<String>(0).with_name("value"),
/// ArgInfo::new::<&mut Vec<String>>(1).with_name("list"),
/// ])
/// .with_return_info(
/// ReturnInfo::new::<&mut String>()
/// );
///
/// // Then we define the dynamic function, which will be used to call our `append` function:
/// let mut func = DynamicFunction::new(|mut args, info| {
/// // Arguments are popped from the list in reverse order:
/// let arg1 = args.pop().unwrap().take_mut::<Vec<String>>(&info.args()[1]).unwrap();
/// let arg0 = args.pop().unwrap().take_owned::<String>(&info.args()[0]).unwrap();
///
/// // Then we can call our function and return the result:
/// Ok(Return::Mut(append(arg0, arg1)))
/// }, info);
///
/// let mut list = Vec::<String>::new();
///
/// // Dynamically call the function:
/// let args = ArgList::default().push_owned("Hello, World".to_string()).push_mut(&mut list);
/// let value = func.call(args).unwrap().unwrap_mut();
///
/// // Mutate the return value:
/// value.downcast_mut::<String>().unwrap().push_str("!!!");
///
/// // Check the result:
/// assert_eq!(list, vec!["Hello, World!!!"]);
/// ```
pub struct DynamicFunction<'env> {
info: FunctionInfo,
func: Box<dyn for<'a> FnMut(ArgList<'a>, &FunctionInfo) -> FunctionResult<'a> + 'env>,
}
impl<'env> DynamicFunction<'env> {
/// Create a new dynamic [`DynamicFunction`].
///
/// The given function can be used to call out to a regular function, closure, or method.
///
/// It's important that the function signature matches the provided [`FunctionInfo`].
/// This info is used to validate the arguments and return value.
pub fn new<F: for<'a> FnMut(ArgList<'a>, &FunctionInfo) -> FunctionResult<'a> + 'env>(
func: F,
info: FunctionInfo,
) -> Self {
Self {
info,
func: Box::new(func),
}
}
/// Set the name of the function.
///
/// For [`DynamicFunctions`] created using [`IntoFunction`],
/// the default name will always be the full path to the function as returned by [`std::any::type_name`].
///
/// [`DynamicFunctions`]: DynamicFunction
pub fn with_name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
self.info = self.info.with_name(name);
self
}
/// Set the arguments of the function.
///
/// It is very important that the arguments match the intended function signature,
/// as this is used to validate arguments passed to the function.
pub fn with_args(mut self, args: Vec<ArgInfo>) -> Self {
self.info = self.info.with_args(args);
self
}
/// Set the return information of the function.
pub fn with_return_info(mut self, return_info: ReturnInfo) -> Self {
self.info = self.info.with_return_info(return_info);
self
}
/// Call the function with the given arguments.
///
/// # Example
///
/// ```
/// # use bevy_reflect::func::{IntoFunction, ArgList};
/// fn add(left: i32, right: i32) -> i32 {
/// left + right
/// }
///
/// let mut func = add.into_function();
/// let args = ArgList::new().push_owned(25_i32).push_owned(75_i32);
/// let result = func.call(args).unwrap().unwrap_owned();
/// assert_eq!(result.take::<i32>().unwrap(), 100);
/// ```
pub fn call<'a>(&mut self, args: ArgList<'a>) -> FunctionResult<'a> {
(self.func.deref_mut())(args, &self.info)
}
/// Call the function with the given arguments and consume the function.
///
/// This is useful for closures that capture their environment because otherwise
/// any captured variables would still be borrowed by this function.
///
/// # Example
///
/// ```
/// # use bevy_reflect::func::{IntoFunction, ArgList};
/// let mut count = 0;
/// let increment = |amount: i32| {
/// count += amount;
/// };
/// let increment_function = increment.into_function();
/// let args = ArgList::new().push_owned(5_i32);
/// // We need to drop `increment_function` here so that we
/// // can regain access to `count`.
/// increment_function.call_once(args).unwrap();
/// assert_eq!(count, 5);
/// ```
pub fn call_once(mut self, args: ArgList) -> FunctionResult {
(self.func.deref_mut())(args, &self.info)
}
/// Returns the function info.
pub fn info(&self) -> &FunctionInfo {
&self.info
}
}
/// Outputs the function signature.
///
/// This takes the format: `DynamicFunction(fn {name}({arg1}: {type1}, {arg2}: {type2}, ...) -> {return_type})`.
///
/// Names for arguments and the function itself are optional and will default to `_` if not provided.
impl<'env> Debug for DynamicFunction<'env> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
let name = self.info.name().unwrap_or("_");
write!(f, "DynamicFunction(fn {name}(")?;
for (index, arg) in self.info.args().iter().enumerate() {
let name = arg.name().unwrap_or("_");
let ty = arg.type_path();
write!(f, "{name}: {ty}")?;
if index + 1 < self.info.args().len() {
write!(f, ", ")?;
}
}
let ret = self.info.return_info().type_path();
write!(f, ") -> {ret})")
}
}
impl<'env> IntoFunction<'env, ()> for DynamicFunction<'env> {
#[inline]
fn into_function(self) -> DynamicFunction<'env> {
self
}
}