Skip to content

Commit 5fb7cf8

Browse files
MrGVSValice-i-cecileMiniaczQ
authored
Add 0.15 release notes for function reflection (#1768)
Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: MiniaczQ <[email protected]>
1 parent 128669a commit 5fb7cf8

File tree

2 files changed

+237
-6
lines changed

2 files changed

+237
-6
lines changed

release-content/0.15/release-notes/_release-notes.toml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
title = "Cosmic text"
33
authors = ["@TotalKrill"]
44
contributors = [
5-
"@tigregalis",
6-
"@alice-i-cecile",
7-
"@nicoburns",
8-
"@rparrett",
9-
"@Dimchikkk",
10-
"@bytemunch",
5+
"@tigregalis",
6+
"@alice-i-cecile",
7+
"@nicoburns",
8+
"@rparrett",
9+
"@Dimchikkk",
10+
"@bytemunch",
1111
]
1212
prs = [10193]
1313
file_name = "10193_Cosmic_text.md"
@@ -304,6 +304,13 @@ contributors = ["@kristoff3r", "@IceSentry"]
304304
prs = [15419]
305305
file_name = "15419_Gpu_readback.md"
306306

307+
[[release_notes]]
308+
title = "Function reflection"
309+
authors = ["@MrGVSV", "@nixpulvis", "@hooded-shrimp"]
310+
contributors = []
311+
prs = [13152, 14098, 14141, 14174, 14201, 14641, 14647, 14666, 14704, 14813, 15086, 15145, 15147, 15148, 15205, 15484]
312+
file_name = "function_reflection.md"
313+
307314
[[release_notes]]
308315
title = "`TypeInfo` improvements"
309316
authors = ["@MrGVSV"]
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
Rust's options for working with functions in a dynamic context are limited.
2+
We're forced to either coerce the function to a function pointer (e.g. `fn(i32, i32) -> i32`)
3+
or turn it into a trait object (e.g. `Box<dyn Fn(i32, i32) -> i32>`).
4+
5+
In both cases, only functions with the same signature (both inputs and outputs) can be stored as an object of the same type.
6+
For truly dynamic contexts, such as working with scripting languages or fetching functions by name,
7+
this can be a significant limitation.
8+
9+
Bevy's [`bevy_reflect`] crate already removes the need for compile-time knowledge of types through
10+
reflection.
11+
In Bevy 0.15, functions can be reflected as well!
12+
13+
This feature is opt-in and requires the `reflect_functions` feature to be enabled on `bevy`
14+
(or the `functions` feature on `bevy_reflect` if using that crate directly).
15+
16+
It works by converting regular functions which arguments and return type derive [`Reflect`]
17+
into a [`DynamicFunction`] type using a new [`IntoFunction`] trait.
18+
19+
```rust
20+
fn add(a: i32, b: i32) -> i32 {
21+
a + b
22+
}
23+
24+
let function = add.into_function();
25+
```
26+
27+
With a `DynamicFunction`, we can then generate our list of arguments into an [`ArgList`]
28+
and call the function:
29+
30+
```rust
31+
let args = ArgList::new()
32+
.push_owned(25_i32)
33+
.push_owned(75_i32);
34+
35+
let result = function.call(args);
36+
```
37+
38+
Calling a function returns a [`FunctionResult`] which contains our [`Return`] data
39+
or a [`FunctionError`] if something went wrong.
40+
41+
```rust
42+
match result {
43+
Ok(Return::Owned(value)) => {
44+
let value = value.try_take::<i32>().unwrap();
45+
println!("Got: {}", value);
46+
}
47+
Err(err) => println!("Error: {:?}", err),
48+
_ => unreachable!("our function always returns an owned value"),
49+
}
50+
```
51+
52+
#### Closure Reflection
53+
54+
This feature doesn't just work for regular functions—it works on closures too!
55+
56+
For closures that capture their environment immutably, we can continue using `DynamicFunction`
57+
and `IntoFunction`. For closures that capture their environment mutably, there's
58+
[`DynamicFunctionMut`] and [`IntoFunctionMut`].
59+
60+
```rust
61+
let mut total = 0;
62+
63+
let increment = || total += 1;
64+
65+
let mut function = increment.into_function_mut();
66+
67+
function.call(ArgList::new()).unwrap();
68+
function.call(ArgList::new()).unwrap();
69+
function.call(ArgList::new()).unwrap();
70+
71+
// Drop the function to release the mutable borrow of `total`.
72+
// Alternatively, our last call could have used `call_once` instead.
73+
drop(function);
74+
75+
assert_eq!(total, 3);
76+
```
77+
78+
#### `FunctionInfo`
79+
80+
Reflected functions hold onto their type metadata via [`FunctionInfo`] which is automatically
81+
generated by the [`TypedFunction`] trait. This allows them to return information about the
82+
function including its name, arguments, and return type.
83+
84+
```rust
85+
let info = String::len.get_function_info();
86+
87+
assert_eq!(info.name().unwrap(), "alloc::string::String::len");
88+
assert_eq!(info.arg_count(), 1);
89+
assert!(info.args()[0].is::<&String>());
90+
assert!(info.return_info().is::<usize>());
91+
```
92+
93+
One thing to note is that closures, anonymous functions, and function pointers
94+
are not automatically given names. For these cases, names can be provided manually.
95+
96+
The same is true for all arguments including `self` arguments: names are not automatically
97+
generated and must be supplied manually if desired.
98+
99+
Using `FunctionInfo`, a `DynamicFunction` will print out its signature when debug-printed.
100+
101+
```rust
102+
dbg!(String::len.into_function());
103+
// Outputs:
104+
// DynamicFunction(fn alloc::string::String::len(_: &alloc::string::String) -> usize)
105+
```
106+
107+
#### Manual Construction
108+
109+
For cases where `IntoFunction` won't work, such as for functions with too many arguments
110+
or for functions with more complex lifetimes, `DynamicFunction` can also be constructed manually.
111+
112+
```rust
113+
// Note: This function would work with `IntoFunction`,
114+
// but for demonstration purposes, we'll construct it manually.
115+
let add_to = DynamicFunction::new(
116+
|mut args| {
117+
let a = args.take::<i32>()?;
118+
let b = args.take_mut::<i32>()?;
119+
120+
*b += a;
121+
122+
Ok(Return::unit())
123+
},
124+
FunctionInfo::named("add_to")
125+
.with_arg::<i32>("a")
126+
.with_arg::<&mut i32>("b")
127+
.with_return::<()>(),
128+
);
129+
```
130+
131+
#### The Function Registry
132+
133+
To make it easier to work with reflected functions, a dedicated [`FunctionRegistry`] has been added.
134+
This works similarly to the [`TypeRegistry`] where functions can be registered and retrieved by name.
135+
136+
```rust
137+
let mut registry = FunctionRegistry::default();
138+
registry
139+
// Named functions can be registered directly
140+
.register(add)?
141+
// Unnamed functions (e.g. closures) must be registered with a name
142+
.register_with_name("add_3", |a: i32, b: i32, c: i32| a + b + c)?;
143+
144+
let add = registry.get("my_crate::math::add").unwrap();
145+
let add_3 = registry.get("add_3").unwrap();
146+
```
147+
148+
For better integration with the rest of Bevy, a new [`AppFunctionRegistry`] resource has been added
149+
along with registration methods on [`App`].
150+
151+
#### The `Function` Trait
152+
153+
A new reflection trait—appropriately called [`Function`]—has been added to correspond to functions.
154+
155+
Due to limitations in Rust, we're unable to implement this trait for all functions,
156+
but it does make it possible to pass around a `DynamicFunction` as a [`PartialReflect`] trait object.
157+
158+
```rust
159+
#[derive(Reflect)]
160+
#[reflect(from_reflect = false)]
161+
struct EventHandler {
162+
callback: DynamicFunction<'static>,
163+
}
164+
165+
let event_handler: Box<dyn Struct> = Box::new(EventHandler {
166+
callback: (|| println!("Event fired!")).into_function(),
167+
});
168+
169+
let field = event_handler.field("callback").unwrap();
170+
171+
if let ReflectRef::Function(callback) = field.reflect_ref() {
172+
callback.reflect_call(ArgList::new()).unwrap();
173+
}
174+
```
175+
176+
#### Limitations
177+
178+
While this feature is quite powerful already, there are still a number of limitations.
179+
180+
Firstly, `IntoFunction`/`IntoFunctionMut` only work for functions with up to 16 arguments,
181+
and only support returning borrowed data where the lifetime is tied to the first argument
182+
(normally `self` in methods).
183+
184+
Secondly, the `Function` trait can't be implemented for all functions due to how the function
185+
reflection traits are defined.
186+
187+
Thirdly, all arguments and return types must have derived `Reflect`.
188+
This can be confusing for certain types such as `&str` since only `&'static str` implements
189+
`Reflect` and its borrowed version would be `&&'static str`.
190+
191+
Lastly, while generic functions are supported, they must first be manually monomorphized.
192+
This means that if you have a generic function like `fn foo<T>()`, you have to create the
193+
`DynamicFunction` like `foo::<i32>.into_function()`.
194+
195+
Most of these limitations are due to Rust itself.
196+
The [lack of variadics] and [issues with coherence] are among the two biggest difficulties
197+
to work around.
198+
Despite this, we will be looking into ways of improving the ergonomics and capabilities
199+
of this feature in future releases.
200+
201+
We already have a [PR](https://github.com/bevyengine/bevy/pull/15074) up to add support for overloaded functions: functions with a variable
202+
number of arguments and argument types.
203+
204+
[`bevy_reflect`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/
205+
[`Reflect`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/trait.Reflect.html
206+
[`DynamicFunction`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/struct.DynamicFunction.html
207+
[`IntoFunction`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/trait.IntoFunction.html
208+
[`ArgList`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/args/struct.ArgList.html
209+
[`FunctionResult`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/type.FunctionResult.html
210+
[`Return`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/enum.Return.html
211+
[`FunctionError`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/enum.FunctionError.html
212+
[`DynamicFunctionMut`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/struct.DynamicFunctionMut.html
213+
[`IntoFunctionMut`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/trait.IntoFunctionMut.html
214+
[`FunctionInfo`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/struct.FunctionInfo.html
215+
[`TypedFunction`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/trait.TypedFunction.html
216+
[`FunctionRegistry`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/struct.FunctionRegistry.html
217+
[`TypeRegistry`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/struct.TypeRegistry.html
218+
[`AppFunctionRegistry`]: https://docs.rs/bevy_reflect/0.15/bevy_ecs/reflect/struct.AppTypeRegistry.html
219+
[`App`]: https://docs.rs/bevy_reflect/0.15/bevy_app/struct.App.html
220+
[`Function`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/func/trait.Function.html
221+
[`PartialReflect`]: https://docs.rs/bevy_reflect/0.15/bevy_reflect/trait.PartialReflect.html
222+
[lack of variadics]: https://poignardazur.github.io/2024/05/25/report-on-rustnl-variadics/
223+
[issues with coherence]: https://doc.rust-lang.org/rustc/lints/listing/warn-by-default.html#coherence-leak-check
224+

0 commit comments

Comments
 (0)