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:

  1. User declares an impl INode which overrides virtual functions, and in that defines ready.
  2. User declares impl INode, but does not override ready -> gdext needs to manually supply a ready that does the init.
  3. User declares only the struct without an impl -> gdext still needs to manually register the ready 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:

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.