forked from bevyengine/bevy
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
bevy_reflect: Function reflection (bevyengine#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: 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
Showing
43 changed files
with
2,195 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
40 changes: 40 additions & 0 deletions
40
crates/bevy_reflect/compile_fail/tests/into_function/arguments_fail.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
34 changes: 34 additions & 0 deletions
34
crates/bevy_reflect/compile_fail/tests/into_function/return_fail.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
23
crates/bevy_reflect/derive/src/impls/func/function_impls.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
34
crates/bevy_reflect/derive/src/impls/func/get_ownership.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.