Skip to content

Commit

Permalink
bevy_reflect: Add Function trait (#15205)
Browse files Browse the repository at this point in the history
# Objective

While #13152 added function reflection, it didn't really make functions
reflectable. Instead, it made it so that they can be called with
reflected arguments and return reflected data. But functions themselves
cannot be reflected.

In other words, we can't go from `DynamicFunction` to `dyn
PartialReflect`.

## Solution

Allow `DynamicFunction` to actually be reflected.

This PR adds the `Function` reflection subtrait (and corresponding
`ReflectRef`, `ReflectKind`, etc.). With this new trait, we're able to
implement `PartialReflect` on `DynamicFunction`.

### Implementors

`Function` is currently only implemented for `DynamicFunction<'static>`.
This is because we can't implement it generically over all
functions—even those that implement `IntoFunction`.

What about `DynamicFunctionMut`? Well, this PR does **not** implement
`Function` for `DynamicFunctionMut`.

The reasons for this are a little complicated, but it boils down to
mutability. `DynamicFunctionMut` requires `&mut self` to be invoked
since it wraps a `FnMut`. However, we can't really model this well with
`Function`. And if we make `DynamicFunctionMut` wrap its internal
`FnMut` in a `Mutex` to allow for `&self` invocations, then we run into
either concurrency issues or recursion issues (or, in the worst case,
both).

So for the time-being, we won't implement `Function` for
`DynamicFunctionMut`. It will be better to evaluate it on its own. And
we may even consider the possibility of removing it altogether if it
adds too much complexity to the crate.

### Dynamic vs Concrete

One of the issues with `DynamicFunction` is the fact that it's both a
dynamic representation (like `DynamicStruct` or `DynamicList`) and the
only way to represent a function.

Because of this, it's in a weird middle ground where we can't easily
implement full-on `Reflect`. That would require `Typed`, but what static
`TypeInfo` could it provide? Just that it's a `DynamicFunction`? None of
the other dynamic types implement `Typed`.

However, by not implementing `Reflect`, we lose the ability to downcast
back to our `DynamicStruct`. Our only option is to call
`Function::clone_dynamic`, which clones the data rather than by simply
downcasting. This works in favor of the `PartialReflect::try_apply`
implementation since it would have to clone anyways, but is definitely
not ideal. This is also the reason I had to add `Debug` as a supertrait
on `Function`.

For now, this PR chooses not to implement `Reflect` for
`DynamicFunction`. We may want to explore this in a followup PR (or even
this one if people feel strongly that it's strictly required).

The same is true for `FromReflect`. We may decide to add an
implementation there as well, but it's likely out-of-scope of this PR.

## Testing

You can test locally by running:

```
cargo test --package bevy_reflect --all-features
```

---

## Showcase

You can now pass around a `DynamicFunction` as a `dyn PartialReflect`!
This also means you can use it as a field on a reflected type without
having to ignore it (though you do need to opt out of `FromReflect`).

```rust
#[derive(Reflect)]
#[reflect(from_reflect = false)]
struct ClickEvent {
    callback: DynamicFunction<'static>,
}

let event: Box<dyn Struct> = Box::new(ClickEvent {
    callback: (|| println!("Clicked!")).into_function(),
});

// We can access our `DynamicFunction` as a `dyn PartialReflect`
let callback: &dyn PartialReflect = event.field("callback").unwrap();

// And access function-related methods via the new `Function` trait
let ReflectRef::Function(callback) = callback.reflect_ref() else {
    unreachable!()
};

// Including calling the function
callback.reflect_call(ArgList::new()).unwrap(); // Prints: Clicked!
```
  • Loading branch information
MrGVSV authored Sep 22, 2024
1 parent e3b6b12 commit 59c0521
Show file tree
Hide file tree
Showing 10 changed files with 385 additions and 15 deletions.
28 changes: 27 additions & 1 deletion benches/benches/bevy_reflect/function.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use bevy_reflect::func::{ArgList, IntoFunction, TypedFunction};
use bevy_reflect::func::{ArgList, IntoFunction, IntoFunctionMut, TypedFunction};
use bevy_reflect::prelude::*;
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};

Expand All @@ -18,6 +18,11 @@ fn typed(c: &mut Criterion) {
let capture = 25;
let closure = |a: i32| a + capture;
b.iter(|| closure.get_function_info());
})
.bench_function("closure_mut", |b| {
let mut capture = 25;
let closure = |a: i32| capture += a;
b.iter(|| closure.get_function_info());
});
}

Expand All @@ -30,11 +35,23 @@ fn into(c: &mut Criterion) {
let capture = 25;
let closure = |a: i32| a + capture;
b.iter(|| closure.into_function());
})
.bench_function("closure_mut", |b| {
let mut _capture = 25;
let closure = move |a: i32| _capture += a;
b.iter(|| closure.into_function_mut());
});
}

fn call(c: &mut Criterion) {
c.benchmark_group("call")
.bench_function("trait_object", |b| {
b.iter_batched(
|| Box::new(add) as Box<dyn Fn(i32, i32) -> i32>,
|func| func(75, 25),
BatchSize::SmallInput,
);
})
.bench_function("function", |b| {
let add = add.into_function();
b.iter_batched(
Expand All @@ -51,6 +68,15 @@ fn call(c: &mut Criterion) {
|args| add.call(args),
BatchSize::SmallInput,
);
})
.bench_function("closure_mut", |b| {
let mut capture = 25;
let mut add = (|a: i32| capture += a).into_function_mut();
b.iter_batched(
|| ArgList::new().push_owned(75_i32),
|args| add.call(args),
BatchSize::SmallInput,
);
});
}

Expand Down
159 changes: 158 additions & 1 deletion crates/bevy_reflect/src/func/dynamic_function.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
use crate as bevy_reflect;
use crate::__macro_exports::RegisterForReflection;
use crate::func::args::{ArgInfo, ArgList};
use crate::func::info::FunctionInfo;
use crate::func::{DynamicFunctionMut, FunctionResult, IntoFunction, IntoFunctionMut, ReturnInfo};
use crate::func::{
DynamicFunctionMut, Function, FunctionResult, IntoFunction, IntoFunctionMut, ReturnInfo,
};
use crate::serde::Serializable;
use crate::{
ApplyError, MaybeTyped, PartialReflect, Reflect, ReflectKind, ReflectMut, ReflectOwned,
ReflectRef, TypeInfo, TypePath,
};
use alloc::borrow::Cow;
use bevy_reflect_derive::impl_type_path;
use core::fmt::{Debug, Formatter};
use std::sync::Arc;

Expand Down Expand Up @@ -136,6 +146,108 @@ impl<'env> DynamicFunction<'env> {
}
}

impl Function for DynamicFunction<'static> {
fn info(&self) -> &FunctionInfo {
self.info()
}

fn reflect_call<'a>(&self, args: ArgList<'a>) -> FunctionResult<'a> {
self.call(args)
}

fn clone_dynamic(&self) -> DynamicFunction<'static> {
self.clone()
}
}

impl PartialReflect for DynamicFunction<'static> {
fn get_represented_type_info(&self) -> Option<&'static TypeInfo> {
None
}

fn into_partial_reflect(self: Box<Self>) -> Box<dyn PartialReflect> {
self
}

fn as_partial_reflect(&self) -> &dyn PartialReflect {
self
}

fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect {
self
}

fn try_into_reflect(self: Box<Self>) -> Result<Box<dyn Reflect>, Box<dyn PartialReflect>> {
Err(self)
}

fn try_as_reflect(&self) -> Option<&dyn Reflect> {
None
}

fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> {
None
}

fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> {
match value.reflect_ref() {
ReflectRef::Function(func) => {
*self = func.clone_dynamic();
Ok(())
}
_ => Err(ApplyError::MismatchedTypes {
from_type: value.reflect_type_path().into(),
to_type: Self::type_path().into(),
}),
}
}

fn reflect_kind(&self) -> ReflectKind {
ReflectKind::Function
}

fn reflect_ref(&self) -> ReflectRef {
ReflectRef::Function(self)
}

fn reflect_mut(&mut self) -> ReflectMut {
ReflectMut::Function(self)
}

fn reflect_owned(self: Box<Self>) -> ReflectOwned {
ReflectOwned::Function(self)
}

fn clone_value(&self) -> Box<dyn PartialReflect> {
Box::new(self.clone())
}

fn reflect_hash(&self) -> Option<u64> {
None
}

fn reflect_partial_eq(&self, _value: &dyn PartialReflect) -> Option<bool> {
None
}

fn debug(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
Debug::fmt(self, f)
}

fn serializable(&self) -> Option<Serializable> {
None
}

fn is_dynamic(&self) -> bool {
true
}
}

impl MaybeTyped for DynamicFunction<'static> {}
impl RegisterForReflection for DynamicFunction<'static> {}

impl_type_path!((in bevy_reflect) DynamicFunction<'env>);

/// Outputs the function's signature.
///
/// This takes the format: `DynamicFunction(fn {name}({arg1}: {type1}, {arg2}: {type2}, ...) -> {return_type})`.
Expand Down Expand Up @@ -187,6 +299,7 @@ impl<'env> IntoFunctionMut<'env, ()> for DynamicFunction<'env> {
#[cfg(test)]
mod tests {
use super::*;
use crate::func::IntoReturn;

#[test]
fn should_overwrite_function_name() {
Expand Down Expand Up @@ -229,4 +342,48 @@ mod tests {

assert_eq!(clone_value, "Hello, world!");
}

#[test]
fn should_apply_function() {
let mut func: Box<dyn Function> = Box::new((|a: i32, b: i32| a + b).into_function());
func.apply(&((|a: i32, b: i32| a * b).into_function()));

let args = ArgList::new().push_owned(5_i32).push_owned(5_i32);
let result = func.reflect_call(args).unwrap().unwrap_owned();
assert_eq!(result.try_take::<i32>().unwrap(), 25);
}

#[test]
fn should_allow_recursive_dynamic_function() {
let factorial = DynamicFunction::new(
|mut args| {
let curr = args.pop::<i32>()?;
if curr == 0 {
return Ok(1_i32.into_return());
}

let arg = args.pop_arg()?;
let this = arg.value();

match this.reflect_ref() {
ReflectRef::Function(func) => {
let result = func.reflect_call(
ArgList::new()
.push_ref(this.as_partial_reflect())
.push_owned(curr - 1),
);
let value = result.unwrap().unwrap_owned().try_take::<i32>().unwrap();
Ok((curr * value).into_return())
}
_ => panic!("expected function"),
}
},
// The `FunctionInfo` doesn't really matter for this test
FunctionInfo::anonymous(),
);

let args = ArgList::new().push_ref(&factorial).push_owned(5_i32);
let value = factorial.call(args).unwrap().unwrap_owned();
assert_eq!(value.try_take::<i32>().unwrap(), 120);
}
}
78 changes: 78 additions & 0 deletions crates/bevy_reflect/src/func/function.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use crate::func::{ArgList, DynamicFunction, FunctionInfo, FunctionResult};
use crate::PartialReflect;
use alloc::borrow::Cow;
use core::fmt::Debug;

/// A trait used to power [function-like] operations via [reflection].
///
/// This trait allows types to be called like regular functions
/// with [`Reflect`]-based [arguments] and return values.
///
/// By default, this trait is currently only implemented for [`DynamicFunction`],
/// however, it is possible to implement this trait for custom function-like types.
///
/// # Example
///
/// ```
/// # use bevy_reflect::func::{IntoFunction, ArgList, Function};
/// fn add(a: i32, b: i32) -> i32 {
/// a + b
/// }
///
/// let func: Box<dyn Function> = Box::new(add.into_function());
/// let args = ArgList::new().push_owned(25_i32).push_owned(75_i32);
/// let value = func.reflect_call(args).unwrap().unwrap_owned();
/// assert_eq!(value.try_take::<i32>().unwrap(), 100);
/// ```
///
/// [function-like]: crate::func
/// [reflection]: crate::Reflect
/// [`Reflect`]: crate::Reflect
/// [arguments]: crate::func::args
/// [`DynamicFunction`]: crate::func::DynamicFunction
pub trait Function: PartialReflect + Debug {
/// The name of the function, if any.
///
/// For [`DynamicFunctions`] created using [`IntoFunction`],
/// the default name will always be the full path to the function as returned by [`std::any::type_name`],
/// unless the function is a closure, anonymous function, or function pointer,
/// in which case the name will be `None`.
///
/// [`DynamicFunctions`]: crate::func::DynamicFunction
/// [`IntoFunction`]: crate::func::IntoFunction
fn name(&self) -> Option<&Cow<'static, str>> {
self.info().name()
}

/// The number of arguments this function accepts.
fn arg_count(&self) -> usize {
self.info().arg_count()
}

/// The [`FunctionInfo`] for this function.
fn info(&self) -> &FunctionInfo;

/// Call this function with the given arguments.
fn reflect_call<'a>(&self, args: ArgList<'a>) -> FunctionResult<'a>;

/// Clone this function into a [`DynamicFunction`].
fn clone_dynamic(&self) -> DynamicFunction<'static>;
}

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

#[test]
fn should_call_dyn_function() {
fn add(a: i32, b: i32) -> i32 {
a + b
}

let func: Box<dyn Function> = Box::new(add.into_function());
let args = ArgList::new().push_owned(25_i32).push_owned(75_i32);
let value = func.reflect_call(args).unwrap().unwrap_owned();
assert_eq!(value.try_take::<i32>().unwrap(), 100);
}
}
2 changes: 2 additions & 0 deletions crates/bevy_reflect/src/func/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ pub use args::{ArgError, ArgList, ArgValue};
pub use dynamic_function::*;
pub use dynamic_function_mut::*;
pub use error::*;
pub use function::*;
pub use info::*;
pub use into_function::*;
pub use into_function_mut::*;
Expand All @@ -144,6 +145,7 @@ pub mod args;
mod dynamic_function;
mod dynamic_function_mut;
mod error;
mod function;
mod info;
mod into_function;
mod into_function_mut;
Expand Down
5 changes: 4 additions & 1 deletion crates/bevy_reflect/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,12 @@
//! * [`Tuple`]
//! * [`Array`]
//! * [`List`]
//! * [`Set`]
//! * [`Map`]
//! * [`Struct`]
//! * [`TupleStruct`]
//! * [`Enum`]
//! * [`Function`] (requires the `functions` feature)
//!
//! As mentioned previously, the last three are automatically implemented by the [derive macro].
//!
Expand Down Expand Up @@ -516,6 +518,7 @@
//! [the language feature for dyn upcasting coercion]: https://github.com/rust-lang/rust/issues/65991
//! [derive macro]: derive@crate::Reflect
//! [`'static` lifetime]: https://doc.rust-lang.org/rust-by-example/scope/lifetime/static_lifetime.html#trait-bound
//! [`Function`]: func::Function
//! [derive macro documentation]: derive@crate::Reflect
//! [deriving `Reflect`]: derive@crate::Reflect
//! [type data]: TypeData
Expand Down Expand Up @@ -593,7 +596,7 @@ pub mod prelude {
};

#[cfg(feature = "functions")]
pub use crate::func::{IntoFunction, IntoFunctionMut};
pub use crate::func::{Function, IntoFunction, IntoFunctionMut};
}

pub use array::*;
Expand Down
Loading

0 comments on commit 59c0521

Please sign in to comment.