Skip to content

Commit ad6897f

Browse files
committed
Add release notes for function reflection
1 parent 6b04166 commit ad6897f

File tree

2 files changed

+227
-6
lines changed

2 files changed

+227
-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"
@@ -311,6 +311,13 @@ contributors = ["@kristoff3r", "@IceSentry"]
311311
prs = [15419]
312312
file_name = "15419_Gpu_readback.md"
313313

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

0 commit comments

Comments
 (0)