OnReady<T>
for late-init fields by Bromeon · Pull Request #534 · godot-rust/gdext (original) (raw)
Alternative designs
Self
type parameter
I was considering OnReady<T, C>
where C
would be Self
of the containing class (or default ()
if not needed).
The idea was to support initializations like this:
fn init(_base: Base<Node>) -> Self {
Self {
auto: OnReady::new_self(|this: &mut Self| { // [1]
self.compute_thing()
}),
}
}
The problem here is that it's not possible to hand out &mut self
and &mut self.field
(the one being initialized) at the same time. To work around this, we'd need interior mutability or some way to partially borrow, which would make the whole thing considerably more complex.
Dependencies between fields are not ideal, but they do happen, and I think they are best solved in a procedural way inside the actual ready()
function. I'm generally wondering if this "auto-init" is that useful in practice, maybe it turns out that the "auto-deref" is the actual feature here, and the container sees more use via OnReady::manual()
.
Before-ready vs. Lazy
I was considering to just mimick Lazy
's semantics, with the added option of manual initialization (and panic on access). However, I wanted to stay as close as possible to Godot's @onready
attribute, which means the value is initialized before ready. We also ensure initialization in the order of declaration (not sure if often needed in practice, though).
This was actually non-trivial to implement, because I needed to consider 3 cases:
- User declares an
impl INode
which overrides virtual functions, and in that definesready
. - User declares
impl INode
, but does not overrideready
-> gdext needs to manually supply aready
that does the init. - User declares only the
struct
without animpl
-> gdext still needs to manually register theready
lifecycle hook, otherwise the fields are uninitialized.
After-ready checks
What is not yet implemented but possible, would be to verify that manual OnReady
instances are truly initialized after ready()
is invoked. This would mean:
- Classes that have manual
OnReady
fields but noready
method would always panic, when added to the scene tree. - "Forgot initialization" errors are revealed at a deterministic time, not only at the point of access.
- It might make some prototyping slightly more cumbersome, as it's not possible to leave variables temporarily uninitialized.
Auto-detecting type in proc-macro
I first had an explicit #[onready]
attribute, but would like to experiment with detecting the type from the signature in the field. I also have some escape hatches planned if this heuristic ever fails (e.g. type aliases, foreign types with same name).
This detection not only leads to more concise code, but also prevents the user from forgetting an attribute and then wondering why variables are not initialized. If this works well, we could apply the same principle to #[base]
, effectively detecting Base<T>
types.
Extra checks
We could also check whether the class truly inherits Node
, as the fields make no sense otherwise. Some of these checks might be more straightforward with the Rust type system than proc-macro impls, so they could also be postponed to builder APIs.
Wider initialization support
This purely provides a value container at the moment. There were discussions about mapping scene trees to Rust data structures, see e.g. #130 (comment). This is something that can be discussed for the future, depending on the experience with this addition.
Besides OnReady
, I'll rework some of the initialization. I'm not happy with #[init(default)]
which doesn't solve a real problem (beyond syntax) and causes confusion in presence of init
function, for example. Or the fact that #[class(init)]
can be easily forgotten, leading to weird runtime errors.
[1]: Note that |this|
won't work, we need |this: &mut Self|
, even though the type is unambiguous.
See Rust discussion about type inference rules as well as Discord thread.