Skip to content

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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions release-content/0.15/release-notes/_release-notes.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
title = "Cosmic text"
authors = ["@TotalKrill"]
contributors = [
"@tigregalis",
"@alice-i-cecile",
"@nicoburns",
"@rparrett",
"@Dimchikkk",
"@bytemunch",
"@tigregalis",
"@alice-i-cecile",
"@nicoburns",
"@rparrett",
"@Dimchikkk",
"@bytemunch",
]
prs = [10193]
file_name = "10193_Cosmic_text.md"
Expand Down Expand Up @@ -311,6 +311,13 @@ contributors = ["@kristoff3r", "@IceSentry"]
prs = [15419]
file_name = "15419_Gpu_readback.md"

[[release_notes]]
title = "Function reflection"
authors = ["@MrGVSV", "@nixpulvis", "@hooded-shrimp"]
contributors = []
prs = [13152, 14098, 14141, 14174, 14201, 14641, 14647, 14666, 14704, 14813, 15086, 15145, 15147, 15148, 15205, 15484]
file_name = "function_reflection.md"

[[release_notes]]
title = "`TypeInfo` improvements"
authors = ["@MrGVSV"]
Expand Down
224 changes: 224 additions & 0 deletions release-content/0.15/release-notes/function_reflection.md
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)
```
Comment on lines +99 to +105
Copy link
Member Author

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.


#### 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