-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
bevy_reflect: Add DynamicClosure
and DynamicClosureMut
#14141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
bevy_reflect: Add DynamicClosure
and DynamicClosureMut
#14141
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't get through a full review of this tonight, but I think these changes are really good and important for hooking up assets to code efficiently.
The design is great, but the changes may take a bit for me to work through.
912d502
to
272af95
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent docs and implementation quality. I'm a bit nervous about using slightly different semantics than Rust, but I think it's the right choice. Altering the names might help make this more comfortable,
I would love to see a table explaining all of the different traits, but that can definitely wait.
Also adds ReflectFn, ReflectFnMut, TypedFunction, and IntoClosure
272af95
to
99e8958
Compare
DynamicClosure
DynamicClosure
and DynamicClosureMut
DynamicFunction is now further restricted to standard function types (well, almost, Rust doesn't like impls on `fn` types)
99e8958
to
fb58ba7
Compare
Updated the PR to further split |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the right way to model it. Lots of code / API, but this is fundamentally complex.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor quibbles over notation aside, I think I'm pretty happy with this split.
Avoid using function-pointer syntax when discussin general function signatures. Also fixes a broken doc link
Objective
As mentioned in this comment, creating a function registry (see #14098) is a bit difficult due to the requirements of
DynamicFunction
. Internally, aDynamicFunction
contains aBox<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 anFnMut
isn't really feasible (wrapping it in anArc
would allow it to be cloned but we wouldn't be able to call the clone since we need a mutable reference to theFnMut
, which we can't get with multipleArc
s still alive, requiring us to also slap in aMutex
, which adds additional overhead).And we don't want to just replace the
dyn FnMut
withdyn 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
andDynamicClosureMut
. Similar to howIntoFunction
exists forDynamicFunction
, two new traits were introduced:IntoClosure
andIntoClosureMut
.Now
DynamicFunction
stores adyn Fn
with a'static
lifetime.DynamicClosure
also uses adyn Fn
but has a lifetime,'env
, tied to its environment.DynamicClosureMut
is most like the oldDynamicFunction
, keeping thedyn FnMut
and also typing its lifetime,'env
, to the environmentHere are some comparison tables:
DynamicFunction
DynamicClosure
DynamicClosureMut
&self
&mut self
'static
lifetimesIntoFunction
IntoClosure
IntoClosureMut
fn
functionsfn
methodsReflectFn
/ReflectFnMut
To make extending the function reflection system easier (the blanket impls for
IntoFunction
,IntoClosure
, andIntoClosureMut
are all incredibly short), this PR generalizes callables with two new traits:ReflectFn
andReflectFnMut
.These traits mimic
Fn
andFnMut
but allow for being called via reflection. In fact, their blanket implementations are identical save forReflectFn
being implemented overFn
types andReflectFnMut
being implemented overFnMut
types.And just as
Fn
is a subtrait ofFnMut
,ReflectFn
is a subtrait ofReflectFnMut
. So anywhere that expects aReflectFnMut
can also be given aReflectFn
.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
andReflectFnMut
, we needed a new way to access the function type information. This PR moves that concept over intoTypedFunction
.Much like
Typed
, this provides a way to access a function'sFunctionInfo
.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 anall_tuples!
macro invocation. Because we needed the same functionality for these new traits, this PR has copy+pasted that code forReflectFn
,ReflectFnMut
, andTypedFunction
— 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:
Changelog
DynamicClosure
structDynamicClosureMut
structIntoClosure
traitIntoClosureMut
traitReflectFn
traitReflectFnMut
traitTypedFunction
traitIntoFunction
now only works for standard Rust functionsIntoFunction
no longer takes a lifetime parameterDynamicFunction::call
now only requires&self
DynamicFunction::call_once
IntoReturn::into_return
signature to include a where clauseInternal 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. CallingIntoFunction::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 aDynamicClosure
orIntoClosureMut::into_closure_mut
to create aDynamicClosureMut
, depending on your needs:IntoFunction
lifetimeAdditionally,
IntoFunction
no longer takes a lifetime parameter as it always expects a'static
lifetime. Usages will need to remove any lifetime parameters:IntoReturn
IntoReturn::into_return
now has a where clause. Any manual implementors will need to add this where clause to their implementation.Footnotes
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 aDynamicFunction
as well. To both future-proof and reduce confusion, though, we'll just pretend like this isn't a thing. ↩