Skip to content

Commit

Permalink
bevy_reflect: Function reflection (bevyengine#13152)
Browse files Browse the repository at this point in the history
# 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:
https://github.com/bevyengine/bevy/blob/ba727898f2adff817838fc4cdb49871bbce37356/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>
  • Loading branch information
MrGVSV and cBournhonesque committed Jul 5, 2024
1 parent 2f513d5 commit fd55812
Show file tree
Hide file tree
Showing 43 changed files with 2,195 additions and 19 deletions.
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2152,6 +2152,17 @@ description = "How dynamic types are used with reflection"
category = "Reflection"
wasm = false

[[example]]
name = "function_reflection"
path = "examples/reflection/function_reflection.rs"
doc-scrape-examples = true

[package.metadata.example.function_reflection]
name = "Function Reflection"
description = "Demonstrates how functions can be called dynamically using reflection"
category = "Reflection"
wasm = false

[[example]]
name = "generic_reflection"
path = "examples/reflection/generic_reflection.rs"
Expand Down
4 changes: 4 additions & 0 deletions crates/bevy_reflect/compile_fail/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ compile_fail_utils = { path = "../../../tools/compile_fail_utils" }
[[test]]
name = "derive"
harness = false

[[test]]
name = "func"
harness = false
3 changes: 2 additions & 1 deletion crates/bevy_reflect/compile_fail/tests/derive.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
fn main() -> compile_fail_utils::ui_test::Result<()> {
compile_fail_utils::test("tests/reflect_derive")
// compile_fail_utils::test("tests/reflect_derive")
Ok(())
}
3 changes: 3 additions & 0 deletions crates/bevy_reflect/compile_fail/tests/func.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() -> compile_fail_utils::ui_test::Result<()> {
compile_fail_utils::test("tests/into_function")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#![allow(unused)]

use bevy_reflect::func::IntoFunction;
use bevy_reflect::Reflect;

fn pass(_: i32) {}

fn too_many_arguments(
arg0: i32,
arg1: i32,
arg2: i32,
arg3: i32,
arg4: i32,
arg5: i32,
arg6: i32,
arg7: i32,
arg8: i32,
arg9: i32,
arg10: i32,
arg11: i32,
arg12: i32,
arg13: i32,
arg14: i32,
arg15: i32,
) {
}

struct Foo;

fn argument_not_reflect(foo: Foo) {}

fn main() {
let _ = pass.into_function();

let _ = too_many_arguments.into_function();
//~^ ERROR: no method named `into_function` found

let _ = argument_not_reflect.into_function();
//~^ ERROR: no method named `into_function` found
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#![allow(unused)]

use bevy_reflect::func::IntoFunction;
use bevy_reflect::Reflect;

fn pass() -> i32 {
123
}

struct Foo;

fn return_not_reflect() -> Foo {
Foo
}

fn return_with_lifetime_pass<'a>(a: &'a String) -> &'a String {
a
}

fn return_with_invalid_lifetime<'a, 'b>(a: &'a String, b: &'b String) -> &'b String {
b
}

fn main() {
let _ = pass.into_function();

let _ = return_not_reflect.into_function();
//~^ ERROR: no method named `into_function` found

let _ = return_with_lifetime_pass.into_function();

let _ = return_with_invalid_lifetime.into_function();
//~^ ERROR: no method named `into_function` found
}
6 changes: 5 additions & 1 deletion crates/bevy_reflect/derive/src/impls/enums.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::derive_data::{EnumVariantFields, ReflectEnum, StructField};
use crate::enum_utility::{EnumVariantOutputData, TryApplyVariantBuilder, VariantBuilder};
use crate::impls::{impl_type_path, impl_typed};
use crate::impls::{impl_function_traits, impl_type_path, impl_typed};
use bevy_macro_utils::fq_std::{FQAny, FQBox, FQOption, FQResult};
use proc_macro2::{Ident, Span};
use quote::quote;
Expand Down Expand Up @@ -65,6 +65,8 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream

let type_path_impl = impl_type_path(reflect_enum.meta());

let function_impls = impl_function_traits(reflect_enum.meta(), &where_clause_options);

let get_type_registration_impl = reflect_enum.get_type_registration(&where_clause_options);

let (impl_generics, ty_generics, where_clause) =
Expand All @@ -79,6 +81,8 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream

#type_path_impl

#function_impls

impl #impl_generics #bevy_reflect_path::Enum for #enum_path #ty_generics #where_reflect_clause {
fn field(&self, #ref_name: &str) -> #FQOption<&dyn #bevy_reflect_path::Reflect> {
match self {
Expand Down
47 changes: 47 additions & 0 deletions crates/bevy_reflect/derive/src/impls/func/from_arg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use crate::derive_data::ReflectMeta;
use crate::utility::WhereClauseOptions;
use bevy_macro_utils::fq_std::FQResult;
use quote::quote;

pub(crate) fn impl_from_arg(
meta: &ReflectMeta,
where_clause_options: &WhereClauseOptions,
) -> proc_macro2::TokenStream {
let bevy_reflect = meta.bevy_reflect_path();
let type_path = meta.type_path();

let (impl_generics, ty_generics, where_clause) = type_path.generics().split_for_impl();
let where_reflect_clause = where_clause_options.extend_where_clause(where_clause);

quote! {
impl #impl_generics #bevy_reflect::func::args::FromArg for #type_path #ty_generics #where_reflect_clause {
type Item<'from_arg> = #type_path #ty_generics;
fn from_arg<'from_arg>(
arg: #bevy_reflect::func::args::Arg<'from_arg>,
info: &#bevy_reflect::func::args::ArgInfo,
) -> #FQResult<Self::Item<'from_arg>, #bevy_reflect::func::args::ArgError> {
arg.take_owned(info)
}
}

impl #impl_generics #bevy_reflect::func::args::FromArg for &'static #type_path #ty_generics #where_reflect_clause {
type Item<'from_arg> = &'from_arg #type_path #ty_generics;
fn from_arg<'from_arg>(
arg: #bevy_reflect::func::args::Arg<'from_arg>,
info: &#bevy_reflect::func::args::ArgInfo,
) -> #FQResult<Self::Item<'from_arg>, #bevy_reflect::func::args::ArgError> {
arg.take_ref(info)
}
}

impl #impl_generics #bevy_reflect::func::args::FromArg for &'static mut #type_path #ty_generics #where_reflect_clause {
type Item<'from_arg> = &'from_arg mut #type_path #ty_generics;
fn from_arg<'from_arg>(
arg: #bevy_reflect::func::args::Arg<'from_arg>,
info: &#bevy_reflect::func::args::ArgInfo,
) -> #FQResult<Self::Item<'from_arg>, #bevy_reflect::func::args::ArgError> {
arg.take_mut(info)
}
}
}
}
23 changes: 23 additions & 0 deletions crates/bevy_reflect/derive/src/impls/func/function_impls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use crate::derive_data::ReflectMeta;
use crate::impls::func::from_arg::impl_from_arg;
use crate::impls::func::get_ownership::impl_get_ownership;
use crate::impls::func::into_return::impl_into_return;
use crate::utility::WhereClauseOptions;
use quote::quote;

pub(crate) fn impl_function_traits(
meta: &ReflectMeta,
where_clause_options: &WhereClauseOptions,
) -> proc_macro2::TokenStream {
let get_ownership = impl_get_ownership(meta, where_clause_options);
let from_arg = impl_from_arg(meta, where_clause_options);
let into_return = impl_into_return(meta, where_clause_options);

quote! {
#get_ownership

#from_arg

#into_return
}
}
34 changes: 34 additions & 0 deletions crates/bevy_reflect/derive/src/impls/func/get_ownership.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use crate::derive_data::ReflectMeta;
use crate::utility::WhereClauseOptions;
use quote::quote;

pub(crate) fn impl_get_ownership(
meta: &ReflectMeta,
where_clause_options: &WhereClauseOptions,
) -> proc_macro2::TokenStream {
let bevy_reflect = meta.bevy_reflect_path();
let type_path = meta.type_path();

let (impl_generics, ty_generics, where_clause) = type_path.generics().split_for_impl();
let where_reflect_clause = where_clause_options.extend_where_clause(where_clause);

quote! {
impl #impl_generics #bevy_reflect::func::args::GetOwnership for #type_path #ty_generics #where_reflect_clause {
fn ownership() -> #bevy_reflect::func::args::Ownership {
#bevy_reflect::func::args::Ownership::Owned
}
}

impl #impl_generics #bevy_reflect::func::args::GetOwnership for &'_ #type_path #ty_generics #where_reflect_clause {
fn ownership() -> #bevy_reflect::func::args::Ownership {
#bevy_reflect::func::args::Ownership::Ref
}
}

impl #impl_generics #bevy_reflect::func::args::GetOwnership for &'_ mut #type_path #ty_generics #where_reflect_clause {
fn ownership() -> #bevy_reflect::func::args::Ownership {
#bevy_reflect::func::args::Ownership::Mut
}
}
}
}
34 changes: 34 additions & 0 deletions crates/bevy_reflect/derive/src/impls/func/into_return.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use crate::derive_data::ReflectMeta;
use crate::utility::WhereClauseOptions;
use quote::quote;

pub(crate) fn impl_into_return(
meta: &ReflectMeta,
where_clause_options: &WhereClauseOptions,
) -> proc_macro2::TokenStream {
let bevy_reflect = meta.bevy_reflect_path();
let type_path = meta.type_path();

let (impl_generics, ty_generics, where_clause) = type_path.generics().split_for_impl();
let where_reflect_clause = where_clause_options.extend_where_clause(where_clause);

quote! {
impl #impl_generics #bevy_reflect::func::IntoReturn for #type_path #ty_generics #where_reflect_clause {
fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> {
#bevy_reflect::func::Return::Owned(Box::new(self))
}
}

impl #impl_generics #bevy_reflect::func::IntoReturn for &'static #type_path #ty_generics #where_reflect_clause {
fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> {
#bevy_reflect::func::Return::Ref(self)
}
}

impl #impl_generics #bevy_reflect::func::IntoReturn for &'static mut #type_path #ty_generics #where_reflect_clause {
fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> {
#bevy_reflect::func::Return::Mut(self)
}
}
}
}
6 changes: 6 additions & 0 deletions crates/bevy_reflect/derive/src/impls/func/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub(crate) use function_impls::impl_function_traits;

mod from_arg;
mod function_impls;
mod get_ownership;
mod into_return;
5 changes: 3 additions & 2 deletions crates/bevy_reflect/derive/src/impls/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
mod enums;
mod func;
mod structs;
mod tuple_structs;
mod typed;
mod values;

pub(crate) use enums::impl_enum;
pub(crate) use func::impl_function_traits;
pub(crate) use structs::impl_struct;
pub(crate) use tuple_structs::impl_tuple_struct;
pub(crate) use typed::impl_type_path;
pub(crate) use typed::impl_typed;
pub(crate) use typed::{impl_type_path, impl_typed};
pub(crate) use values::impl_value;
6 changes: 5 additions & 1 deletion crates/bevy_reflect/derive/src/impls/structs.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::impls::{impl_type_path, impl_typed};
use crate::impls::{impl_function_traits, impl_type_path, impl_typed};
use crate::utility::ident_or_index;
use crate::ReflectStruct;
use bevy_macro_utils::fq_std::{FQAny, FQBox, FQDefault, FQOption, FQResult};
Expand Down Expand Up @@ -54,6 +54,8 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS

let type_path_impl = impl_type_path(reflect_struct.meta());

let function_impls = impl_function_traits(reflect_struct.meta(), &where_clause_options);

let get_type_registration_impl = reflect_struct.get_type_registration(&where_clause_options);

let (impl_generics, ty_generics, where_clause) = reflect_struct
Expand All @@ -71,6 +73,8 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS

#type_path_impl

#function_impls

impl #impl_generics #bevy_reflect_path::Struct for #struct_path #ty_generics #where_reflect_clause {
fn field(&self, name: &str) -> #FQOption<&dyn #bevy_reflect_path::Reflect> {
match name {
Expand Down
6 changes: 5 additions & 1 deletion crates/bevy_reflect/derive/src/impls/tuple_structs.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::impls::{impl_type_path, impl_typed};
use crate::impls::{impl_function_traits, impl_type_path, impl_typed};
use crate::ReflectStruct;
use bevy_macro_utils::fq_std::{FQAny, FQBox, FQDefault, FQOption, FQResult};
use quote::{quote, ToTokens};
Expand Down Expand Up @@ -46,6 +46,8 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2::

let type_path_impl = impl_type_path(reflect_struct.meta());

let function_impls = impl_function_traits(reflect_struct.meta(), &where_clause_options);

let (impl_generics, ty_generics, where_clause) = reflect_struct
.meta()
.type_path()
Expand All @@ -61,6 +63,8 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2::

#type_path_impl

#function_impls

impl #impl_generics #bevy_reflect_path::TupleStruct for #struct_path #ty_generics #where_reflect_clause {
fn field(&self, index: usize) -> #FQOption<&dyn #bevy_reflect_path::Reflect> {
match index {
Expand Down
6 changes: 5 additions & 1 deletion crates/bevy_reflect/derive/src/impls/values.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::impls::{impl_type_path, impl_typed};
use crate::impls::{impl_function_traits, impl_type_path, impl_typed};
use crate::utility::WhereClauseOptions;
use crate::ReflectMeta;
use bevy_macro_utils::fq_std::{FQAny, FQBox, FQClone, FQOption, FQResult};
Expand Down Expand Up @@ -33,6 +33,8 @@ pub(crate) fn impl_value(meta: &ReflectMeta) -> proc_macro2::TokenStream {

let type_path_impl = impl_type_path(meta);

let function_impls = impl_function_traits(meta, &where_clause_options);

let (impl_generics, ty_generics, where_clause) = type_path.generics().split_for_impl();
let where_reflect_clause = where_clause_options.extend_where_clause(where_clause);
let get_type_registration_impl = meta.get_type_registration(&where_clause_options);
Expand All @@ -44,6 +46,8 @@ pub(crate) fn impl_value(meta: &ReflectMeta) -> proc_macro2::TokenStream {

#typed_impl

#function_impls

impl #impl_generics #bevy_reflect_path::Reflect for #type_path #ty_generics #where_reflect_clause {
#[inline]
fn get_represented_type_info(&self) -> #FQOption<&'static #bevy_reflect_path::TypeInfo> {
Expand Down
8 changes: 6 additions & 2 deletions crates/bevy_reflect/derive/src/utility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,17 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> {
&self,
where_clause: Option<&WhereClause>,
) -> proc_macro2::TokenStream {
let type_path = self.meta.type_path();
let (_, ty_generics, _) = self.meta.type_path().generics().split_for_impl();

let required_bounds = self.required_bounds();

// Maintain existing where clause, if any.
let mut generic_where_clause = if let Some(where_clause) = where_clause {
let predicates = where_clause.predicates.iter();
quote! {where Self: #required_bounds, #(#predicates,)*}
quote! {where #type_path #ty_generics: #required_bounds, #(#predicates,)*}
} else {
quote!(where Self: #required_bounds,)
quote!(where #type_path #ty_generics: #required_bounds,)
};

// Add additional reflection trait bounds
Expand Down
Loading

0 comments on commit fd55812

Please sign in to comment.