
# 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>
221 lines
7.6 KiB
Rust
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
|
|
}
|
|
}
|