Skip to content

Commit

Permalink
bevy_reflect: Add DynamicClosure and DynamicClosureMut (#14141)
Browse files Browse the repository at this point in the history
# 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.
  • Loading branch information
MrGVSV authored Jul 16, 2024
1 parent 20c6bcd commit 1042f09
Show file tree
Hide file tree
Showing 23 changed files with 1,608 additions and 532 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ fn main() {
let _ = pass.into_function();

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

let _ = argument_not_reflect.into_function();
//~^ ERROR: no method named `into_function` found
//~^ E0599
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#![allow(unused)]

use bevy_reflect::func::IntoFunction;
use bevy_reflect::Reflect;

fn main() {
let value = String::from("Hello, World!");
let closure_capture_owned = move || println!("{}", value);

let _ = closure_capture_owned.into_function();
//~^ E0277

let value = String::from("Hello, World!");
let closure_capture_reference = || println!("{}", value);

let _ = closure_capture_reference.into_function();
// ↑ This should be an error (E0277) but `compile_fail_utils` fails to pick it up
// when the `closure_capture_owned` test is present
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ fn main() {
let _ = pass.into_function();

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

let _ = return_with_lifetime_pass.into_function();

let _ = return_with_invalid_lifetime.into_function();
//~^ ERROR: no method named `into_function` found
//~^ E0599
}
10 changes: 5 additions & 5 deletions crates/bevy_reflect/derive/src/impls/func/into_return.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ pub(crate) fn impl_into_return(

quote! {
impl #impl_generics #bevy_reflect::func::IntoReturn for #type_path #ty_generics #where_reflect_clause {
fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> {
fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> where Self: 'into_return {
#bevy_reflect::func::Return::Owned(Box::new(self))
}
}

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

impl #impl_generics #bevy_reflect::func::IntoReturn for &'static mut #type_path #ty_generics #where_reflect_clause {
fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> {
impl #impl_generics #bevy_reflect::func::IntoReturn for &mut #type_path #ty_generics #where_reflect_clause {
fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> where Self: 'into_return {
#bevy_reflect::func::Return::Mut(self)
}
}
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_reflect/src/func/args/arg.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::func::args::{ArgError, ArgInfo, Ownership};
use crate::Reflect;

/// Represents an argument that can be passed to a [`DynamicFunction`].
/// Represents an argument that can be passed to a [`DynamicFunction`] or [`DynamicClosure`].
///
/// [`DynamicFunction`]: crate::func::DynamicFunction
/// [`DynamicClosure`]: crate::func::DynamicClosure
#[derive(Debug)]
pub enum Arg<'a> {
Owned(Box<dyn Reflect>),
Expand Down
13 changes: 9 additions & 4 deletions crates/bevy_reflect/src/func/args/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ use alloc::borrow::Cow;
use crate::func::args::{GetOwnership, Ownership};
use crate::TypePath;

/// Type information for an [`Arg`] used in a [`DynamicFunction`].
/// Type information for an [`Arg`] used in a [`DynamicFunction`] or [`DynamicClosure`].
///
/// [`Arg`]: crate::func::Arg
/// [`DynamicFunction`]: crate::func::DynamicFunction
/// [`Arg`]: crate::func::args::Arg
/// [`DynamicFunction`]: crate::func::function::DynamicFunction
/// [`DynamicClosure`]: crate::func::closures::DynamicClosure
#[derive(Debug, Clone)]
pub struct ArgInfo {
/// The index of the argument within its function.
Expand Down Expand Up @@ -54,10 +55,14 @@ impl ArgInfo {
/// This is because the name needs to be manually set using [`Self::with_name`]
/// since the name can't be inferred from the function type alone.
///
/// For [`DynamicFunctions`] created using [`IntoFunction`], the name will always be `None`.
/// For [`DynamicFunctions`] created using [`IntoFunction`]
/// or [`DynamicClosures`] created using [`IntoClosure`],
/// the name will always be `None`.
///
/// [`DynamicFunctions`]: crate::func::DynamicFunction
/// [`IntoFunction`]: crate::func::IntoFunction
/// [`DynamicClosures`]: crate::func::DynamicClosure
/// [`IntoClosure`]: crate::func::IntoClosure
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_reflect/src/func/args/list.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::func::args::Arg;
use crate::Reflect;

/// A list of arguments that can be passed to a [`DynamicFunction`].
/// A list of arguments that can be passed to a [`DynamicFunction`] or [`DynamicClosure`].
///
/// # Example
///
Expand All @@ -24,6 +24,7 @@ use crate::Reflect;
/// ```
///
/// [`DynamicFunction`]: crate::func::DynamicFunction
/// [`DynamicClosure`]: crate::func::DynamicClosure
#[derive(Default, Debug)]
pub struct ArgList<'a>(Vec<Arg<'a>>);

Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_reflect/src/func/args/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Argument types and utilities for working with [`DynamicFunctions`].
//! Argument types and utilities for working with [`DynamicFunctions`] and [`DynamicClosures`].
//!
//! [`DynamicFunctions`]: crate::func::DynamicFunction
//! [`DynamicClosures`]: crate::func::DynamicClosure

pub use arg::*;
pub use error::*;
Expand Down
180 changes: 180 additions & 0 deletions crates/bevy_reflect/src/func/closures/dynamic_closure.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
use alloc::borrow::Cow;
use core::fmt::{Debug, Formatter};

use crate::func::args::{ArgInfo, ArgList};
use crate::func::info::FunctionInfo;
use crate::func::{FunctionResult, IntoClosure, ReturnInfo};

/// A dynamic representation of a Rust closure.
///
/// This type can be used to represent any Rust closure that captures its environment immutably.
/// For closures that need to capture their environment mutably,
/// see [`DynamicClosureMut`].
///
/// This type can be seen as a superset of [`DynamicFunction`].
///
/// See the [module-level documentation] for more information.
///
/// You will generally not need to construct this manually.
/// Instead, many functions and closures can be automatically converted using the [`IntoClosure`] trait.
///
/// # Example
///
/// Most of the time, a [`DynamicClosure`] can be created using the [`IntoClosure`] trait:
///
/// ```
/// # use bevy_reflect::func::{ArgList, DynamicClosure, FunctionInfo, IntoClosure};
/// #
/// let punct = String::from("!!!");
///
/// let punctuate = |text: &String| -> String {
/// format!("{}{}", text, punct)
/// };
///
/// // Convert the closure into a dynamic closure using `IntoClosure::into_closure`
/// let mut func: DynamicClosure = punctuate.into_closure();
///
/// // Dynamically call the closure:
/// let text = String::from("Hello, world");
/// let args = ArgList::default().push_ref(&text);
/// let value = func.call(args).unwrap().unwrap_owned();
///
/// // Check the result:
/// assert_eq!(value.take::<String>().unwrap(), "Hello, world!!!");
/// ```
///
/// [`DynamicClosureMut`]: crate::func::closures::DynamicClosureMut
/// [`DynamicFunction`]: crate::func::DynamicFunction
pub struct DynamicClosure<'env> {
info: FunctionInfo,
func: Box<dyn for<'a> Fn(ArgList<'a>, &FunctionInfo) -> FunctionResult<'a> + 'env>,
}

impl<'env> DynamicClosure<'env> {
/// Create a new [`DynamicClosure`].
///
/// The given function can be used to call out to a regular function, closure, or method.
///
/// It's important that the closure signature matches the provided [`FunctionInfo`].
/// This info is used to validate the arguments and return value.
pub fn new<F: for<'a> Fn(ArgList<'a>, &FunctionInfo) -> FunctionResult<'a> + 'env>(
func: F,
info: FunctionInfo,
) -> Self {
Self {
info,
func: Box::new(func),
}
}

/// Set the name of the closure.
///
/// For [`DynamicClosures`] created using [`IntoClosure`],
/// the default name will always be the full path to the closure as returned by [`std::any::type_name`].
///
/// This default name generally does not contain the actual name of the closure, only its module path.
/// It is therefore recommended to set the name manually using this method.
///
/// [`DynamicClosures`]: DynamicClosure
pub fn with_name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
self.info = self.info.with_name(name);
self
}

/// Set the arguments of the closure.
///
/// It is very important that the arguments match the intended closure signature,
/// as this is used to validate arguments passed to the closure.
pub fn with_args(mut self, args: Vec<ArgInfo>) -> Self {
self.info = self.info.with_args(args);
self
}

/// Set the return information of the closure.
pub fn with_return_info(mut self, return_info: ReturnInfo) -> Self {
self.info = self.info.with_return_info(return_info);
self
}

/// Call the closure with the given arguments.
///
/// # Example
///
/// ```
/// # use bevy_reflect::func::{IntoClosure, ArgList};
/// let c = 23;
/// let add = |a: i32, b: i32| -> i32 {
/// a + b + c
/// };
///
/// let mut func = add.into_closure().with_name("add");
/// let args = ArgList::new().push_owned(25_i32).push_owned(75_i32);
/// let result = func.call(args).unwrap().unwrap_owned();
/// assert_eq!(result.take::<i32>().unwrap(), 123);
/// ```
pub fn call<'a>(&self, args: ArgList<'a>) -> FunctionResult<'a> {
(self.func)(args, &self.info)
}

/// Returns the closure info.
pub fn info(&self) -> &FunctionInfo {
&self.info
}
}

/// Outputs the closure's signature.
///
/// This takes the format: `DynamicClosure(fn {name}({arg1}: {type1}, {arg2}: {type2}, ...) -> {return_type})`.
///
/// Names for arguments and the closure itself are optional and will default to `_` if not provided.
impl<'env> Debug for DynamicClosure<'env> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
let name = self.info.name().unwrap_or("_");
write!(f, "DynamicClosure(fn {name}(")?;

for (index, arg) in self.info.args().iter().enumerate() {
let name = arg.name().unwrap_or("_");
let ty = arg.type_path();
write!(f, "{name}: {ty}")?;

if index + 1 < self.info.args().len() {
write!(f, ", ")?;
}
}

let ret = self.info.return_info().type_path();
write!(f, ") -> {ret})")
}
}

impl<'env> IntoClosure<'env, ()> for DynamicClosure<'env> {
#[inline]
fn into_closure(self) -> DynamicClosure<'env> {
self
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn should_overwrite_closure_name() {
let c = 23;
let func = (|a: i32, b: i32| a + b + c)
.into_closure()
.with_name("my_closure");
assert_eq!(func.info().name(), Some("my_closure"));
}

#[test]
fn should_convert_dynamic_closure_with_into_closure() {
fn make_closure<'env, F: IntoClosure<'env, M>, M>(f: F) -> DynamicClosure<'env> {
f.into_closure()
}

let c = 23;
let closure: DynamicClosure = make_closure(|a: i32, b: i32| a + b + c);
let _: DynamicClosure = make_closure(closure);
}
}
Loading

0 comments on commit 1042f09

Please sign in to comment.