bevy/crates
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
..
bevy_a11y Update accesskit and accesskit_winit (#13841) 2024-06-17 15:08:53 +00:00
bevy_animation Remove unnecessary compute for rotation interpolation (#14019) 2024-06-25 21:14:37 +00:00
bevy_app feat(bevy_app): expose an API to perform updates for a specific sub-app. (#14009) 2024-06-25 14:04:31 +00:00
bevy_asset Allow phase items not associated with meshes to be binned. (#14029) 2024-06-27 16:13:03 +00:00
bevy_audio Fix a few "repeated word" typos (#13955) 2024-06-20 21:35:20 +00:00
bevy_color Allow bevy_color use without bevy_reflect support (#13870) 2024-06-16 15:47:30 +00:00
bevy_core Don't show .to_bits in Display impl for Entity (#14011) 2024-06-25 17:08:24 +00:00
bevy_core_pipeline Fix compile failure in WASM without wgpu backend (#14081) 2024-06-30 22:58:43 +00:00
bevy_derive Deprecate dynamic plugins (#13080) 2024-05-20 20:01:28 +00:00
bevy_dev_tools Fix lints introduced in Rust beta 1.80 (#13899) 2024-06-17 17:22:01 +00:00
bevy_diagnostic Poll system information in separate tasks (#13693) 2024-06-10 19:06:22 +00:00
bevy_dylib Add README.md to all crates (#13184) 2024-05-02 18:56:00 +00:00
bevy_dynamic_plugin Deprecate dynamic plugins (#13080) 2024-05-20 20:01:28 +00:00
bevy_ecs Fix error in AnyOf (#14027) 2024-06-27 20:20:50 +00:00
bevy_encase_derive Add README.md to all crates (#13184) 2024-05-02 18:56:00 +00:00
bevy_gilrs Add README.md to all crates (#13184) 2024-05-02 18:56:00 +00:00
bevy_gizmos Use u32 for all resolution/subdivision fields in bevy_gizmos (#13927) 2024-06-19 17:28:10 +00:00
bevy_gltf Add labels to Gltf Node and Mesh assets (#13558) 2024-06-05 23:10:33 +00:00
bevy_hierarchy Fix EntityCommands::despawn docs (#13774) 2024-06-09 17:59:19 +00:00
bevy_input Fix a few "repeated word" typos (#13955) 2024-06-20 21:35:20 +00:00
bevy_internal don't crash without features bevy_pbr, ktx2, zstd (#14020) 2024-06-26 03:08:23 +00:00
bevy_log Document use of NO_COLOR in LogPlugin (#13984) 2024-06-24 21:04:55 +00:00
bevy_macro_utils Add README.md to all crates (#13184) 2024-05-02 18:56:00 +00:00
bevy_math Fix a few "repeated word" typos (#13955) 2024-06-20 21:35:20 +00:00
bevy_mikktspace Fixed a link to Blender's mikktspace docs (#13924) 2024-06-19 12:37:10 +00:00
bevy_pbr don't put previous skin/morph in the morphed_skinned_mesh_layout (#14065) 2024-06-29 01:03:51 +00:00
bevy_picking Upstream CorePlugin from bevy_mod_picking (#13677) 2024-06-15 11:59:57 +00:00
bevy_ptr add Debug for ptr types (#13498) 2024-05-24 21:25:11 +00:00
bevy_reflect bevy_reflect: Function reflection (#13152) 2024-07-01 13:49:08 +00:00
bevy_render Allow phase items not associated with meshes to be binned. (#14029) 2024-06-27 16:13:03 +00:00
bevy_scene Fix lints introduced in Rust beta 1.80 (#13899) 2024-06-17 17:22:01 +00:00
bevy_sprite Made Material2dBindGroupId instantiable (#14053) 2024-06-29 00:41:15 +00:00
bevy_state Move StateTransitionSteps registration to states plugin (#13939) 2024-06-20 00:57:40 +00:00
bevy_tasks fix: upgrade to winit v0.30 (#13366) 2024-06-03 13:06:48 +00:00
bevy_text Fix lints introduced in Rust beta 1.80 (#13899) 2024-06-17 17:22:01 +00:00
bevy_time Make time_system public (#13879) 2024-06-16 18:07:41 +00:00
bevy_transform impl BuildChildrenTransformExt for EntityWorldMut (#14022) 2024-06-26 14:59:20 +00:00
bevy_ui add PartialEq to Outline (#14055) 2024-06-27 20:03:07 +00:00
bevy_utils Fix parameter name of all_tuples's document (#13896) 2024-06-17 15:17:24 +00:00
bevy_window apply window scale to window size when creating it (#13967) 2024-06-21 18:04:57 +00:00
bevy_winit only run one update per frame drawn (#14023) 2024-06-26 20:23:17 +00:00