Skip to content

Commit 1042f09

Browse files
authored
bevy_reflect: Add DynamicClosure and DynamicClosureMut (#14141)
# Objective As mentioned in [this](#13152 (comment)) comment, creating a function registry (see #14098) is a bit difficult due to the requirements of `DynamicFunction`. Internally, a `DynamicFunction` contains a `Box<dyn FnMut>` (the function that reifies reflected arguments and calls the actual function), which requires `&mut self` in order to be called. This means that users would require a mutable reference to the function registry for it to be useful— which isn't great. And they can't clone the `DynamicFunction` either because cloning an `FnMut` isn't really feasible (wrapping it in an `Arc` would allow it to be cloned but we wouldn't be able to call the clone since we need a mutable reference to the `FnMut`, which we can't get with multiple `Arc`s still alive, requiring us to also slap in a `Mutex`, which adds additional overhead). And we don't want to just replace the `dyn FnMut` with `dyn Fn` as that would prevent reflecting closures that mutate their environment. Instead, we need to introduce a new type to split the requirements of `DynamicFunction`. ## Solution Introduce new types for representing closures. Specifically, this PR introduces `DynamicClosure` and `DynamicClosureMut`. Similar to how `IntoFunction` exists for `DynamicFunction`, two new traits were introduced: `IntoClosure` and `IntoClosureMut`. Now `DynamicFunction` stores a `dyn Fn` with a `'static` lifetime. `DynamicClosure` also uses a `dyn Fn` but has a lifetime, `'env`, tied to its environment. `DynamicClosureMut` is most like the old `DynamicFunction`, keeping the `dyn FnMut` and also typing its lifetime, `'env`, to the environment Here are some comparison tables: | | `DynamicFunction` | `DynamicClosure` | `DynamicClosureMut` | | - | ----------------- | ---------------- | ------------------- | | Callable with `&self` | ✅ | ✅ | ❌ | | Callable with `&mut self` | ✅ | ✅ | ✅ | | Allows for non-`'static` lifetimes | ❌ | ✅ | ✅ | | | `IntoFunction` | `IntoClosure` | `IntoClosureMut` | | - | -------------- | ------------- | ---------------- | | Convert `fn` functions | ✅ | ✅ | ✅ | | Convert `fn` methods | ✅ | ✅ | ✅ | | Convert anonymous functions | ✅ | ✅ | ✅ | | Convert closures that capture immutable references | ❌ | ✅ | ✅ | | Convert closures that capture mutable references | ❌ | ❌ | ✅ | | Convert closures that capture owned values | ❌[^1] | ✅ | ✅ | [^1]: Due to limitations in Rust, `IntoFunction` can't be implemented for just functions (unless we forced users to manually coerce them to function pointers first). So closures that meet the trait requirements _can technically_ be converted into a `DynamicFunction` as well. To both future-proof and reduce confusion, though, we'll just pretend like this isn't a thing. ```rust let mut list: Vec<i32> = vec![1, 2, 3]; // `replace` is a closure that captures a mutable reference to `list` let mut replace = |index: usize, value: i32| -> i32 { let old_value = list[index]; list[index] = value; old_value }; // Convert the closure into a dynamic closure using `IntoClosureMut::into_closure_mut` let mut func: DynamicClosureMut = replace.into_closure_mut(); // Dynamically call the closure: let args = ArgList::default().push_owned(1_usize).push_owned(-2_i32); let value = func.call_once(args).unwrap().unwrap_owned(); // Check the result: assert_eq!(value.take::<i32>().unwrap(), 2); assert_eq!(list, vec![1, -2, 3]); ``` ### `ReflectFn`/`ReflectFnMut` To make extending the function reflection system easier (the blanket impls for `IntoFunction`, `IntoClosure`, and `IntoClosureMut` are all incredibly short), this PR generalizes callables with two new traits: `ReflectFn` and `ReflectFnMut`. These traits mimic `Fn` and `FnMut` but allow for being called via reflection. In fact, their blanket implementations are identical save for `ReflectFn` being implemented over `Fn` types and `ReflectFnMut` being implemented over `FnMut` types. And just as `Fn` is a subtrait of `FnMut`, `ReflectFn` is a subtrait of `ReflectFnMut`. So anywhere that expects a `ReflectFnMut` can also be given a `ReflectFn`. To reiterate, these traits aren't 100% necessary. They were added in purely for extensibility. If we decide to split things up differently or add new traits/types in the future, then those changes should be much simpler to implement. ### `TypedFunction` Because of the split into `ReflectFn` and `ReflectFnMut`, we needed a new way to access the function type information. This PR moves that concept over into `TypedFunction`. Much like `Typed`, this provides a way to access a function's `FunctionInfo`. By splitting this trait out, it helps to ensure the other traits are focused on a single responsibility. ### Internal Macros The original function PR (#13152) implemented `IntoFunction` using a macro which was passed into an `all_tuples!` macro invocation. Because we needed the same functionality for these new traits, this PR has copy+pasted that code for `ReflectFn`, `ReflectFnMut`, and `TypedFunction`— albeit with some differences between them. Originally, I was going to try and macro-ify the impls and where clauses such that we wouldn't have to straight up duplicate a lot of this logic. However, aside from being more complex in general, autocomplete just does not play nice with such heavily nested macros (tried in both RustRover and VSCode). And both of those problems told me that it just wasn't worth it: we need to ensure the crate is easily maintainable, even at the cost of duplicating code. So instead, I made sure to simplify the macro code by removing all fully-qualified syntax and cutting the where clauses down to the bare essentials, which helps to clean up a lot of the visual noise. I also tried my best to document the macro logic in certain areas (I may even add a bit more) to help with maintainability for future devs. ### Documentation Documentation for this module was a bit difficult for me. So many of these traits and types are very interconnected. And each trait/type has subtle differences that make documenting it in a single place, like at the module level, difficult to do cleanly. Describing the valid signatures is also challenging to do well. Hopefully what I have here is okay. I think I did an okay job, but let me know if there any thoughts on ways to improve it. We can also move such a task to a followup PR for more focused discussion. ## Testing You can test locally by running: ``` cargo test --package bevy_reflect ``` --- ## Changelog - Added `DynamicClosure` struct - Added `DynamicClosureMut` struct - Added `IntoClosure` trait - Added `IntoClosureMut` trait - Added `ReflectFn` trait - Added `ReflectFnMut` trait - Added `TypedFunction` trait - `IntoFunction` now only works for standard Rust functions - `IntoFunction` no longer takes a lifetime parameter - `DynamicFunction::call` now only requires `&self` - Removed `DynamicFunction::call_once` - Changed the `IntoReturn::into_return` signature to include a where clause ## Internal Migration Guide > [!important] > Function reflection was introduced as part of the 0.15 dev cycle. This migration guide was written for developers relying on `main` during this cycle, and is not a breaking change coming from 0.14. ### `IntoClosure` `IntoFunction` now only works for standard Rust functions. Calling `IntoFunction::into_function` on a closure that captures references to its environment (either mutable or immutable), will no longer compile. Instead, you will need to use either `IntoClosure::into_closure` to create a `DynamicClosure` or `IntoClosureMut::into_closure_mut` to create a `DynamicClosureMut`, depending on your needs: ```rust let punct = String::from("!"); let print = |value: String| { println!("{value}{punct}"); }; // BEFORE let func: DynamicFunction = print.into_function(); // AFTER let func: DynamicClosure = print.into_closure(); ``` ### `IntoFunction` lifetime Additionally, `IntoFunction` no longer takes a lifetime parameter as it always expects a `'static` lifetime. Usages will need to remove any lifetime parameters: ```rust // BEFORE fn execute<'env, F: IntoFunction<'env, Marker>, Marker>(f: F) {/* ... */} // AFTER fn execute<F: IntoFunction<Marker>, Marker>(f: F) {/* ... */} ``` ### `IntoReturn` `IntoReturn::into_return` now has a where clause. Any manual implementors will need to add this where clause to their implementation.
1 parent 20c6bcd commit 1042f09

23 files changed

+1608
-532
lines changed

crates/bevy_reflect/compile_fail/tests/into_function/arguments_fail.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ fn main() {
3333
let _ = pass.into_function();
3434

3535
let _ = too_many_arguments.into_function();
36-
//~^ ERROR: no method named `into_function` found
36+
//~^ E0599
3737

3838
let _ = argument_not_reflect.into_function();
39-
//~^ ERROR: no method named `into_function` found
39+
//~^ E0599
4040
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#![allow(unused)]
2+
3+
use bevy_reflect::func::IntoFunction;
4+
use bevy_reflect::Reflect;
5+
6+
fn main() {
7+
let value = String::from("Hello, World!");
8+
let closure_capture_owned = move || println!("{}", value);
9+
10+
let _ = closure_capture_owned.into_function();
11+
//~^ E0277
12+
13+
let value = String::from("Hello, World!");
14+
let closure_capture_reference = || println!("{}", value);
15+
16+
let _ = closure_capture_reference.into_function();
17+
// ↑ This should be an error (E0277) but `compile_fail_utils` fails to pick it up
18+
// when the `closure_capture_owned` test is present
19+
}

crates/bevy_reflect/compile_fail/tests/into_function/return_fail.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ fn main() {
2525
let _ = pass.into_function();
2626

2727
let _ = return_not_reflect.into_function();
28-
//~^ ERROR: no method named `into_function` found
28+
//~^ E0599
2929

3030
let _ = return_with_lifetime_pass.into_function();
3131

3232
let _ = return_with_invalid_lifetime.into_function();
33-
//~^ ERROR: no method named `into_function` found
33+
//~^ E0599
3434
}

crates/bevy_reflect/derive/src/impls/func/into_return.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,19 @@ pub(crate) fn impl_into_return(
1414

1515
quote! {
1616
impl #impl_generics #bevy_reflect::func::IntoReturn for #type_path #ty_generics #where_reflect_clause {
17-
fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> {
17+
fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> where Self: 'into_return {
1818
#bevy_reflect::func::Return::Owned(Box::new(self))
1919
}
2020
}
2121

22-
impl #impl_generics #bevy_reflect::func::IntoReturn for &'static #type_path #ty_generics #where_reflect_clause {
23-
fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> {
22+
impl #impl_generics #bevy_reflect::func::IntoReturn for &#type_path #ty_generics #where_reflect_clause {
23+
fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> where Self: 'into_return {
2424
#bevy_reflect::func::Return::Ref(self)
2525
}
2626
}
2727

28-
impl #impl_generics #bevy_reflect::func::IntoReturn for &'static mut #type_path #ty_generics #where_reflect_clause {
29-
fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> {
28+
impl #impl_generics #bevy_reflect::func::IntoReturn for &mut #type_path #ty_generics #where_reflect_clause {
29+
fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> where Self: 'into_return {
3030
#bevy_reflect::func::Return::Mut(self)
3131
}
3232
}

crates/bevy_reflect/src/func/args/arg.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use crate::func::args::{ArgError, ArgInfo, Ownership};
22
use crate::Reflect;
33

4-
/// Represents an argument that can be passed to a [`DynamicFunction`].
4+
/// Represents an argument that can be passed to a [`DynamicFunction`] or [`DynamicClosure`].
55
///
66
/// [`DynamicFunction`]: crate::func::DynamicFunction
7+
/// [`DynamicClosure`]: crate::func::DynamicClosure
78
#[derive(Debug)]
89
pub enum Arg<'a> {
910
Owned(Box<dyn Reflect>),

crates/bevy_reflect/src/func/args/info.rs

+9-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ use alloc::borrow::Cow;
33
use crate::func::args::{GetOwnership, Ownership};
44
use crate::TypePath;
55

6-
/// Type information for an [`Arg`] used in a [`DynamicFunction`].
6+
/// Type information for an [`Arg`] used in a [`DynamicFunction`] or [`DynamicClosure`].
77
///
8-
/// [`Arg`]: crate::func::Arg
9-
/// [`DynamicFunction`]: crate::func::DynamicFunction
8+
/// [`Arg`]: crate::func::args::Arg
9+
/// [`DynamicFunction`]: crate::func::function::DynamicFunction
10+
/// [`DynamicClosure`]: crate::func::closures::DynamicClosure
1011
#[derive(Debug, Clone)]
1112
pub struct ArgInfo {
1213
/// The index of the argument within its function.
@@ -54,10 +55,14 @@ impl ArgInfo {
5455
/// This is because the name needs to be manually set using [`Self::with_name`]
5556
/// since the name can't be inferred from the function type alone.
5657
///
57-
/// For [`DynamicFunctions`] created using [`IntoFunction`], the name will always be `None`.
58+
/// For [`DynamicFunctions`] created using [`IntoFunction`]
59+
/// or [`DynamicClosures`] created using [`IntoClosure`],
60+
/// the name will always be `None`.
5861
///
5962
/// [`DynamicFunctions`]: crate::func::DynamicFunction
6063
/// [`IntoFunction`]: crate::func::IntoFunction
64+
/// [`DynamicClosures`]: crate::func::DynamicClosure
65+
/// [`IntoClosure`]: crate::func::IntoClosure
6166
pub fn name(&self) -> Option<&str> {
6267
self.name.as_deref()
6368
}

crates/bevy_reflect/src/func/args/list.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::func::args::Arg;
22
use crate::Reflect;
33

4-
/// A list of arguments that can be passed to a [`DynamicFunction`].
4+
/// A list of arguments that can be passed to a [`DynamicFunction`] or [`DynamicClosure`].
55
///
66
/// # Example
77
///
@@ -24,6 +24,7 @@ use crate::Reflect;
2424
/// ```
2525
///
2626
/// [`DynamicFunction`]: crate::func::DynamicFunction
27+
/// [`DynamicClosure`]: crate::func::DynamicClosure
2728
#[derive(Default, Debug)]
2829
pub struct ArgList<'a>(Vec<Arg<'a>>);
2930

crates/bevy_reflect/src/func/args/mod.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
//! Argument types and utilities for working with [`DynamicFunctions`].
1+
//! Argument types and utilities for working with [`DynamicFunctions`] and [`DynamicClosures`].
22
//!
33
//! [`DynamicFunctions`]: crate::func::DynamicFunction
4+
//! [`DynamicClosures`]: crate::func::DynamicClosure
45
56
pub use arg::*;
67
pub use error::*;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
use alloc::borrow::Cow;
2+
use core::fmt::{Debug, Formatter};
3+
4+
use crate::func::args::{ArgInfo, ArgList};
5+
use crate::func::info::FunctionInfo;
6+
use crate::func::{FunctionResult, IntoClosure, ReturnInfo};
7+
8+
/// A dynamic representation of a Rust closure.
9+
///
10+
/// This type can be used to represent any Rust closure that captures its environment immutably.
11+
/// For closures that need to capture their environment mutably,
12+
/// see [`DynamicClosureMut`].
13+
///
14+
/// This type can be seen as a superset of [`DynamicFunction`].
15+
///
16+
/// See the [module-level documentation] for more information.
17+
///
18+
/// You will generally not need to construct this manually.
19+
/// Instead, many functions and closures can be automatically converted using the [`IntoClosure`] trait.
20+
///
21+
/// # Example
22+
///
23+
/// Most of the time, a [`DynamicClosure`] can be created using the [`IntoClosure`] trait:
24+
///
25+
/// ```
26+
/// # use bevy_reflect::func::{ArgList, DynamicClosure, FunctionInfo, IntoClosure};
27+
/// #
28+
/// let punct = String::from("!!!");
29+
///
30+
/// let punctuate = |text: &String| -> String {
31+
/// format!("{}{}", text, punct)
32+
/// };
33+
///
34+
/// // Convert the closure into a dynamic closure using `IntoClosure::into_closure`
35+
/// let mut func: DynamicClosure = punctuate.into_closure();
36+
///
37+
/// // Dynamically call the closure:
38+
/// let text = String::from("Hello, world");
39+
/// let args = ArgList::default().push_ref(&text);
40+
/// let value = func.call(args).unwrap().unwrap_owned();
41+
///
42+
/// // Check the result:
43+
/// assert_eq!(value.take::<String>().unwrap(), "Hello, world!!!");
44+
/// ```
45+
///
46+
/// [`DynamicClosureMut`]: crate::func::closures::DynamicClosureMut
47+
/// [`DynamicFunction`]: crate::func::DynamicFunction
48+
pub struct DynamicClosure<'env> {
49+
info: FunctionInfo,
50+
func: Box<dyn for<'a> Fn(ArgList<'a>, &FunctionInfo) -> FunctionResult<'a> + 'env>,
51+
}
52+
53+
impl<'env> DynamicClosure<'env> {
54+
/// Create a new [`DynamicClosure`].
55+
///
56+
/// The given function can be used to call out to a regular function, closure, or method.
57+
///
58+
/// It's important that the closure signature matches the provided [`FunctionInfo`].
59+
/// This info is used to validate the arguments and return value.
60+
pub fn new<F: for<'a> Fn(ArgList<'a>, &FunctionInfo) -> FunctionResult<'a> + 'env>(
61+
func: F,
62+
info: FunctionInfo,
63+
) -> Self {
64+
Self {
65+
info,
66+
func: Box::new(func),
67+
}
68+
}
69+
70+
/// Set the name of the closure.
71+
///
72+
/// For [`DynamicClosures`] created using [`IntoClosure`],
73+
/// the default name will always be the full path to the closure as returned by [`std::any::type_name`].
74+
///
75+
/// This default name generally does not contain the actual name of the closure, only its module path.
76+
/// It is therefore recommended to set the name manually using this method.
77+
///
78+
/// [`DynamicClosures`]: DynamicClosure
79+
pub fn with_name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
80+
self.info = self.info.with_name(name);
81+
self
82+
}
83+
84+
/// Set the arguments of the closure.
85+
///
86+
/// It is very important that the arguments match the intended closure signature,
87+
/// as this is used to validate arguments passed to the closure.
88+
pub fn with_args(mut self, args: Vec<ArgInfo>) -> Self {
89+
self.info = self.info.with_args(args);
90+
self
91+
}
92+
93+
/// Set the return information of the closure.
94+
pub fn with_return_info(mut self, return_info: ReturnInfo) -> Self {
95+
self.info = self.info.with_return_info(return_info);
96+
self
97+
}
98+
99+
/// Call the closure with the given arguments.
100+
///
101+
/// # Example
102+
///
103+
/// ```
104+
/// # use bevy_reflect::func::{IntoClosure, ArgList};
105+
/// let c = 23;
106+
/// let add = |a: i32, b: i32| -> i32 {
107+
/// a + b + c
108+
/// };
109+
///
110+
/// let mut func = add.into_closure().with_name("add");
111+
/// let args = ArgList::new().push_owned(25_i32).push_owned(75_i32);
112+
/// let result = func.call(args).unwrap().unwrap_owned();
113+
/// assert_eq!(result.take::<i32>().unwrap(), 123);
114+
/// ```
115+
pub fn call<'a>(&self, args: ArgList<'a>) -> FunctionResult<'a> {
116+
(self.func)(args, &self.info)
117+
}
118+
119+
/// Returns the closure info.
120+
pub fn info(&self) -> &FunctionInfo {
121+
&self.info
122+
}
123+
}
124+
125+
/// Outputs the closure's signature.
126+
///
127+
/// This takes the format: `DynamicClosure(fn {name}({arg1}: {type1}, {arg2}: {type2}, ...) -> {return_type})`.
128+
///
129+
/// Names for arguments and the closure itself are optional and will default to `_` if not provided.
130+
impl<'env> Debug for DynamicClosure<'env> {
131+
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
132+
let name = self.info.name().unwrap_or("_");
133+
write!(f, "DynamicClosure(fn {name}(")?;
134+
135+
for (index, arg) in self.info.args().iter().enumerate() {
136+
let name = arg.name().unwrap_or("_");
137+
let ty = arg.type_path();
138+
write!(f, "{name}: {ty}")?;
139+
140+
if index + 1 < self.info.args().len() {
141+
write!(f, ", ")?;
142+
}
143+
}
144+
145+
let ret = self.info.return_info().type_path();
146+
write!(f, ") -> {ret})")
147+
}
148+
}
149+
150+
impl<'env> IntoClosure<'env, ()> for DynamicClosure<'env> {
151+
#[inline]
152+
fn into_closure(self) -> DynamicClosure<'env> {
153+
self
154+
}
155+
}
156+
157+
#[cfg(test)]
158+
mod tests {
159+
use super::*;
160+
161+
#[test]
162+
fn should_overwrite_closure_name() {
163+
let c = 23;
164+
let func = (|a: i32, b: i32| a + b + c)
165+
.into_closure()
166+
.with_name("my_closure");
167+
assert_eq!(func.info().name(), Some("my_closure"));
168+
}
169+
170+
#[test]
171+
fn should_convert_dynamic_closure_with_into_closure() {
172+
fn make_closure<'env, F: IntoClosure<'env, M>, M>(f: F) -> DynamicClosure<'env> {
173+
f.into_closure()
174+
}
175+
176+
let c = 23;
177+
let closure: DynamicClosure = make_closure(|a: i32, b: i32| a + b + c);
178+
let _: DynamicClosure = make_closure(closure);
179+
}
180+
}

0 commit comments

Comments
 (0)