-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
NthTensor
left a comment
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
alice-i-cecile
left a comment
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
DynamicClosureDynamicClosure 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 |
alice-i-cecile
left a comment
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.
mweatherley
left a comment
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, aDynamicFunctioncontains aBox<dyn FnMut>(the function that reifies reflected arguments and calls the actual function), which requires&mut selfin 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
DynamicFunctioneither because cloning anFnMutisn't really feasible (wrapping it in anArcwould 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 multipleArcs still alive, requiring us to also slap in aMutex, which adds additional overhead).And we don't want to just replace the
dyn FnMutwithdyn Fnas 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
DynamicClosureandDynamicClosureMut. Similar to howIntoFunctionexists forDynamicFunction, two new traits were introduced:IntoClosureandIntoClosureMut.Now
DynamicFunctionstores adyn Fnwith a'staticlifetime.DynamicClosurealso uses adyn Fnbut has a lifetime,'env, tied to its environment.DynamicClosureMutis most like the oldDynamicFunction, keeping thedyn FnMutand also typing its lifetime,'env, to the environmentHere are some comparison tables:
DynamicFunctionDynamicClosureDynamicClosureMut&self&mut self'staticlifetimesIntoFunctionIntoClosureIntoClosureMutfnfunctionsfnmethodsReflectFn/ReflectFnMutTo make extending the function reflection system easier (the blanket impls for
IntoFunction,IntoClosure, andIntoClosureMutare all incredibly short), this PR generalizes callables with two new traits:ReflectFnandReflectFnMut.These traits mimic
FnandFnMutbut allow for being called via reflection. In fact, their blanket implementations are identical save forReflectFnbeing implemented overFntypes andReflectFnMutbeing implemented overFnMuttypes.And just as
Fnis a subtrait ofFnMut,ReflectFnis a subtrait ofReflectFnMut. So anywhere that expects aReflectFnMutcan 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.
TypedFunctionBecause of the split into
ReflectFnandReflectFnMut, 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
IntoFunctionusing 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
DynamicClosurestructDynamicClosureMutstructIntoClosuretraitIntoClosureMuttraitReflectFntraitReflectFnMuttraitTypedFunctiontraitIntoFunctionnow only works for standard Rust functionsIntoFunctionno longer takes a lifetime parameterDynamicFunction::callnow only requires&selfDynamicFunction::call_onceIntoReturn::into_returnsignature 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
mainduring this cycle, and is not a breaking change coming from 0.14.IntoClosureIntoFunctionnow only works for standard Rust functions. CallingIntoFunction::into_functionon 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_closureto create aDynamicClosureorIntoClosureMut::into_closure_mutto create aDynamicClosureMut, depending on your needs:IntoFunctionlifetimeAdditionally,
IntoFunctionno longer takes a lifetime parameter as it always expects a'staticlifetime. Usages will need to remove any lifetime parameters:IntoReturnIntoReturn::into_returnnow has a where clause. Any manual implementors will need to add this where clause to their implementation.Footnotes
Due to limitations in Rust,
IntoFunctioncan'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 aDynamicFunctionas well. To both future-proof and reduce confusion, though, we'll just pretend like this isn't a thing. ↩