-
Notifications
You must be signed in to change notification settings - Fork 382
Add 0.15 release notes for function reflection #1768
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
alice-i-cecile
merged 4 commits into
bevyengine:main
from
MrGVSV:mrgvsv/notes/function-reflection
Nov 5, 2024
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
224 changes: 224 additions & 0 deletions
224
release-content/0.15/release-notes/function_reflection.md
This file contains hidden or 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,224 @@ | ||
Rust's options for working with functions in a dynamic context are limited. | ||
We're forced to either coerce the function to a function pointer (e.g. `fn(i32, i32) -> i32`) | ||
or turn it into a trait object (e.g. `Box<dyn Fn(i32, i32) -> i32>`). | ||
|
||
In both cases, only functions with the same signature (both inputs and outputs) can be stored as an object of the same type. | ||
For truly dynamic contexts, such as working with scripting languages or fetching functions by name, | ||
this can be a significant limitation. | ||
|
||
Bevy's [`bevy_reflect`] crate already removes the need for compile-time knowledge of types through | ||
reflection. | ||
In Bevy 0.15, functions can be reflected as well! | ||
|
||
This feature is opt-in and requires the `reflect_functions` feature to be enabled on `bevy` | ||
(or the `functions` feature on `bevy_reflect` if using that crate directly). | ||
|
||
It works by converting regular functions which arguments and return type derive [`Reflect`] | ||
into a [`DynamicFunction`] type using a new [`IntoFunction`] trait. | ||
|
||
```rust | ||
fn add(a: i32, b: i32) -> i32 { | ||
a + b | ||
} | ||
|
||
let function = add.into_function(); | ||
``` | ||
|
||
With a `DynamicFunction`, we can then generate our list of arguments into an [`ArgList`] | ||
and call the function: | ||
|
||
```rust | ||
let args = ArgList::new() | ||
.push_owned(25_i32) | ||
.push_owned(75_i32); | ||
|
||
let result = function.call(args); | ||
``` | ||
|
||
Calling a function returns a [`FunctionResult`] which contains our [`Return`] data | ||
or a [`FunctionError`] if something went wrong. | ||
|
||
```rust | ||
match result { | ||
Ok(Return::Owned(value)) => { | ||
let value = value.try_take::<i32>().unwrap(); | ||
println!("Got: {}", value); | ||
} | ||
Err(err) => println!("Error: {:?}", err), | ||
_ => unreachable!("our function always returns an owned value"), | ||
} | ||
``` | ||
|
||
#### Closure Reflection | ||
|
||
This feature doesn't just work for regular functions—it works on closures too! | ||
|
||
For closures that capture their environment immutably, we can continue using `DynamicFunction` | ||
and `IntoFunction`. For closures that capture their environment mutably, there's | ||
[`DynamicFunctionMut`] and [`IntoFunctionMut`]. | ||
|
||
```rust | ||
let mut total = 0; | ||
|
||
let increment = || total += 1; | ||
|
||
let mut function = increment.into_function_mut(); | ||
|
||
function.call(ArgList::new()).unwrap(); | ||
function.call(ArgList::new()).unwrap(); | ||
function.call(ArgList::new()).unwrap(); | ||
|
||
// Drop the function to release the mutable borrow of `total`. | ||
// Alternatively, our last call could have used `call_once` instead. | ||
drop(function); | ||
|
||
assert_eq!(total, 3); | ||
``` | ||
|
||
#### `FunctionInfo` | ||
|
||
Reflected functions hold onto their type metadata via [`FunctionInfo`] which is automatically | ||
generated by the [`TypedFunction`] trait. This allows them to return information about the | ||
function including its name, arguments, and return type. | ||
|
||
```rust | ||
let info = String::len.get_function_info(); | ||
|
||
assert_eq!(info.name().unwrap(), "alloc::string::String::len"); | ||
assert_eq!(info.arg_count(), 1); | ||
assert!(info.args()[0].is::<&String>()); | ||
assert!(info.return_info().is::<usize>()); | ||
``` | ||
|
||
One thing to note is that closures, anonymous functions, and function pointers | ||
are not automatically given names. For these cases, names can be provided manually. | ||
|
||
The same is true for all arguments including `self` arguments: names are not automatically | ||
generated and must be supplied manually if desired. | ||
|
||
Using `FunctionInfo`, a `DynamicFunction` will print out its signature when debug-printed. | ||
|
||
```rust | ||
dbg!(String::len.into_function()); | ||
// Outputs: | ||
// DynamicFunction(fn alloc::string::String::len(_: &alloc::string::String) -> usize) | ||
``` | ||
|
||
#### Manual Construction | ||
|
||
For cases where `IntoFunction` won't work, such as for functions with too many arguments | ||
or for functions with more complex lifetimes, `DynamicFunction` can also be constructed manually. | ||
|
||
```rust | ||
// Note: This function would work with `IntoFunction`, | ||
// but for demonstration purposes, we'll construct it manually. | ||
let add_to = DynamicFunction::new( | ||
|mut args| { | ||
let a = args.take::<i32>()?; | ||
let b = args.take_mut::<i32>()?; | ||
|
||
*b += a; | ||
|
||
Ok(Return::unit()) | ||
}, | ||
FunctionInfo::named("add_to") | ||
.with_arg::<i32>("a") | ||
.with_arg::<&mut i32>("b") | ||
.with_return::<()>(), | ||
); | ||
``` | ||
|
||
#### The Function Registry | ||
|
||
To make it easier to work with reflected functions, a dedicated [`FunctionRegistry`] has been added. | ||
This works similarly to the [`TypeRegistry`] where functions can be registered and retrieved by name. | ||
|
||
```rust | ||
let mut registry = FunctionRegistry::default(); | ||
registry | ||
// Named functions can be registered directly | ||
.register(add)? | ||
// Unnamed functions (e.g. closures) must be registered with a name | ||
.register_with_name("add_3", |a: i32, b: i32, c: i32| a + b + c)?; | ||
|
||
let add = registry.get("my_crate::math::add").unwrap(); | ||
let add_3 = registry.get("add_3").unwrap(); | ||
``` | ||
|
||
For better integration with the rest of Bevy, a new [`AppFunctionRegistry`] resource has been added | ||
along with registration methods on [`App`]. | ||
|
||
#### The `Function` Trait | ||
|
||
A new reflection trait—appropriately called [`Function`]—has been added to correspond to functions. | ||
|
||
Due to limitations in Rust, we're unable to implement this trait for all functions, | ||
but it does make it possible to pass around a `DynamicFunction` as a [`PartialReflect`] trait object. | ||
|
||
```rust | ||
#[derive(Reflect)] | ||
#[reflect(from_reflect = false)] | ||
struct EventHandler { | ||
callback: DynamicFunction<'static>, | ||
} | ||
|
||
let event_handler: Box<dyn Struct> = Box::new(EventHandler { | ||
callback: (|| println!("Event fired!")).into_function(), | ||
}); | ||
|
||
let field = event_handler.field("callback").unwrap(); | ||
|
||
if let ReflectRef::Function(callback) = field.reflect_ref() { | ||
callback.reflect_call(ArgList::new()).unwrap(); | ||
} | ||
``` | ||
|
||
#### Limitations | ||
|
||
While this feature is quite powerful already, there are still a number of limitations. | ||
|
||
Firstly, `IntoFunction`/`IntoFunctionMut` only work for functions with up to 16 arguments, | ||
and only support returning borrowed data where the lifetime is tied to the first argument | ||
(normally `self` in methods). | ||
|
||
Secondly, the `Function` trait can't be implemented for all functions due to how the function | ||
reflection traits are defined. | ||
|
||
Thirdly, all arguments and return types must have derived `Reflect`. | ||
This can be confusing for certain types such as `&str` since only `&'static str` implements | ||
`Reflect` and its borrowed version would be `&&'static str`. | ||
|
||
Lastly, while generic functions are supported, they must first be manually monomorphized. | ||
This means that if you have a generic function like `fn foo<T>()`, you have to create the | ||
`DynamicFunction` like `foo::<i32>.into_function()`. | ||
|
||
Most of these limitations are due to Rust itself. | ||
The [lack of variadics] and [issues with coherence] are among the two biggest difficulties | ||
to work around. | ||
Despite this, we will be looking into ways of improving the ergonomics and capabilities | ||
of this feature in future releases. | ||
|
||
We already have a [PR](https://github.com/bevyengine/bevy/pull/15074) up to add support for overloaded functions: functions with a variable | ||
number of arguments and argument types. | ||
|
||
[`bevy_reflect`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/ | ||
[`Reflect`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/trait.Reflect.html | ||
[`DynamicFunction`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/struct.DynamicFunction.html | ||
[`IntoFunction`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/trait.IntoFunction.html | ||
[`ArgList`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/args/struct.ArgList.html | ||
[`FunctionResult`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/type.FunctionResult.html | ||
[`Return`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/enum.Return.html | ||
[`FunctionError`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/enum.FunctionError.html | ||
[`DynamicFunctionMut`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/struct.DynamicFunctionMut.html | ||
[`IntoFunctionMut`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/trait.IntoFunctionMut.html | ||
[`FunctionInfo`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/struct.FunctionInfo.html | ||
[`TypedFunction`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/trait.TypedFunction.html | ||
[`FunctionRegistry`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/struct.FunctionRegistry.html | ||
[`TypeRegistry`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/struct.TypeRegistry.html | ||
[`AppFunctionRegistry`]: https://docs.rs/bevy_reflect/0.15/bevy_ecs/reflect/struct.AppTypeRegistry.html | ||
[`App`]: https://docs.rs/bevy_reflect/0.15/bevy_app/struct.App.html | ||
[`Function`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/trait.Function.html | ||
[`PartialReflect`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/trait.PartialReflect.html | ||
[lack of variadics]: https://poignardazur.github.io/2024/05/25/report-on-rustnl-variadics/ | ||
[issues with coherence]: https://doc.rust-lang.org/rustc/lints/listing/warn-by-default.html#coherence-leak-check | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just added this because I think it's really cool we can still print out the full signature, but let me know if it's too much to include for the release post.