Skip to content

Commit ef5f388

Browse files
authored
Merge pull request #534 from godot-rust/feature/onready
`OnReady<T>` for late-init fields
2 parents 01045be + d4b6ea8 commit ef5f388

File tree

14 files changed

+687
-77
lines changed

14 files changed

+687
-77
lines changed

godot-core/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ pub mod private {
9292
}
9393
}
9494

95+
pub fn auto_init<T>(l: &mut crate::obj::OnReady<T>) {
96+
l.init_auto();
97+
}
98+
9599
fn print_panic_message(msg: &str) {
96100
// If the message contains newlines, print all of the lines after a line break, and indent them.
97101
let lbegin = "\n ";

godot-core/src/obj/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ mod base;
1515
mod gd;
1616
mod guards;
1717
mod instance_id;
18+
mod onready;
1819
mod raw;
1920
mod traits;
2021

2122
pub use base::*;
2223
pub use gd::*;
2324
pub use guards::*;
2425
pub use instance_id::*;
26+
pub use onready::*;
2527
pub use raw::*;
2628
pub use traits::*;
2729

godot-core/src/obj/onready.rs

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
use std::mem;
9+
10+
/// Ergonomic late-initialization container with `ready()` support.
11+
///
12+
/// While deferred initialization is generally seen as bad practice, it is often inevitable in game development.
13+
/// Godot in particular encourages initialization inside `ready()`, e.g. to access the scene tree after a node is inserted into it.
14+
/// The alternative to using this pattern is [`Option<T>`][option], which needs to be explicitly unwrapped with `unwrap()` or `expect()` each time.
15+
///
16+
/// `OnReady<T>` should always be used as a field. There are two modes to use it:
17+
///
18+
/// 1. **Automatic mode, using [`new()`](Self::new).**<br>
19+
/// Before `ready()` is called, all `OnReady` fields constructed with `new()` are automatically initialized, in the order of
20+
/// declaration. This means that you can safely access them in `ready()`.<br><br>
21+
/// 2. **Manual mode, using [`manual()`](Self::manual).**<br>
22+
/// These fields are left uninitialized until you call [`init()`][Self::init] on them. This is useful if you need more complex
23+
/// initialization scenarios than a closure allows. If you forget initialization, a panic will occur on first access.
24+
///
25+
/// Conceptually, `OnReady<T>` is very close to [once_cell's `Lazy<T>`][lazy], with additional hooks into the Godot lifecycle.
26+
/// The absence of methods to check initialization state is deliberate: you don't need them if you follow the above two patterns.
27+
/// This container is not designed as a general late-initialization solution, but tailored to the `ready()` semantics of Godot.
28+
///
29+
/// This type is not thread-safe. `ready()` runs on the main thread and you are expected to access its value on the main thread, as well.
30+
///
31+
/// [option]: std::option::Option
32+
/// [lazy]: https://docs.rs/once_cell/1/once_cell/unsync/struct.Lazy.html
33+
///
34+
/// # Example
35+
/// ```
36+
/// use godot::prelude::*;
37+
///
38+
/// #[derive(GodotClass)]
39+
/// #[class(base = Node)]
40+
/// struct MyClass {
41+
/// auto: OnReady<i32>,
42+
/// manual: OnReady<i32>,
43+
/// }
44+
///
45+
/// #[godot_api]
46+
/// impl INode for MyClass {
47+
/// fn init(_base: Base<Node>) -> Self {
48+
/// Self {
49+
/// auto: OnReady::new(|| 11),
50+
/// manual: OnReady::manual(),
51+
/// }
52+
/// }
53+
///
54+
/// fn ready(&mut self) {
55+
/// // self.auto is now ready with value 11.
56+
/// assert_eq!(*self.auto, 11);
57+
///
58+
/// // self.manual needs to be initialized manually.
59+
/// self.manual.init(22);
60+
/// assert_eq!(*self.manual, 22);
61+
/// }
62+
/// }
63+
pub struct OnReady<T> {
64+
state: InitState<T>,
65+
}
66+
67+
impl<T> OnReady<T> {
68+
/// Schedule automatic initialization before `ready()`.
69+
///
70+
/// This guarantees that the value is initialized once `ready()` starts running.
71+
/// Until then, accessing the object may panic. In particular, the object is _not_ initialized on first use.
72+
///
73+
/// The value is also initialized when you don't override `ready()`.
74+
///
75+
/// For more control over initialization, use the [`OnReady::manual()`] constructor, followed by a [`self.init()`][OnReady::init]
76+
/// call during `ready()`.
77+
pub fn new<F>(init_fn: F) -> Self
78+
where
79+
F: FnOnce() -> T + 'static,
80+
{
81+
Self {
82+
state: InitState::AutoPrepared {
83+
initializer: Box::new(init_fn),
84+
},
85+
}
86+
}
87+
88+
/// Leave uninitialized, expects manual initialization during `ready()`.
89+
///
90+
/// If you use this method, you _must_ call [`init()`][Self::init] during the `ready()` callback, otherwise a panic will occur.
91+
pub fn manual() -> Self {
92+
Self {
93+
state: InitState::ManualUninitialized,
94+
}
95+
}
96+
97+
/// Runs manual initialization.
98+
///
99+
/// # Panics
100+
/// - If `init()` was called before.
101+
/// - If this object was already provided with a closure during construction, in [`Self::new()`].
102+
pub fn init(&mut self, value: T) {
103+
match &self.state {
104+
InitState::ManualUninitialized { .. } => {
105+
self.state = InitState::Initialized { value };
106+
}
107+
InitState::AutoPrepared { .. } => {
108+
panic!("cannot call init() on auto-initialized OnReady objects")
109+
}
110+
InitState::AutoInitializing => {
111+
// SAFETY: Loading is ephemeral state that is only set in init_auto() and immediately overwritten.
112+
unsafe { std::hint::unreachable_unchecked() }
113+
}
114+
InitState::Initialized { .. } => {
115+
panic!("already initialized; did you call init() more than once?")
116+
}
117+
};
118+
}
119+
120+
/// Runs initialization.
121+
///
122+
/// # Panics
123+
/// If the value is already initialized.
124+
pub(crate) fn init_auto(&mut self) {
125+
// Two branches needed, because mem::replace() could accidentally overwrite an already initialized value.
126+
match &self.state {
127+
InitState::ManualUninitialized => return, // skipped
128+
InitState::AutoPrepared { .. } => {} // handled below
129+
InitState::AutoInitializing => {
130+
// SAFETY: Loading is ephemeral state that is only set below and immediately overwritten.
131+
unsafe { std::hint::unreachable_unchecked() }
132+
}
133+
InitState::Initialized { .. } => panic!("OnReady object already initialized"),
134+
};
135+
136+
// Temporarily replace with dummy state, as it's not possible to take ownership of the initializer closure otherwise.
137+
let InitState::AutoPrepared { initializer } =
138+
mem::replace(&mut self.state, InitState::AutoInitializing)
139+
else {
140+
// SAFETY: condition checked above.
141+
unsafe { std::hint::unreachable_unchecked() }
142+
};
143+
144+
self.state = InitState::Initialized {
145+
value: initializer(),
146+
};
147+
}
148+
}
149+
150+
// Panicking Deref is not best practice according to Rust, but constant get() calls are significantly less ergonomic and make it harder to
151+
// migrate between T and LateInit<T>, because all the accesses need to change.
152+
impl<T> std::ops::Deref for OnReady<T> {
153+
type Target = T;
154+
155+
/// Returns a shared reference to the value.
156+
///
157+
/// # Panics
158+
/// If the value is not yet initialized.
159+
fn deref(&self) -> &Self::Target {
160+
match &self.state {
161+
InitState::ManualUninitialized => {
162+
panic!("OnReady manual value uninitialized, did you call init()?")
163+
}
164+
InitState::AutoPrepared { .. } => {
165+
panic!("OnReady automatic value uninitialized, is only available in ready()")
166+
}
167+
InitState::AutoInitializing => unreachable!(),
168+
InitState::Initialized { value } => value,
169+
}
170+
}
171+
}
172+
173+
impl<T> std::ops::DerefMut for OnReady<T> {
174+
/// Returns an exclusive reference to the value.
175+
///
176+
/// # Panics
177+
/// If the value is not yet initialized.
178+
fn deref_mut(&mut self) -> &mut Self::Target {
179+
match &mut self.state {
180+
InitState::Initialized { value } => value,
181+
InitState::ManualUninitialized { .. } | InitState::AutoPrepared { .. } => {
182+
panic!("value not yet initialized")
183+
}
184+
InitState::AutoInitializing => unreachable!(),
185+
}
186+
}
187+
}
188+
189+
// ----------------------------------------------------------------------------------------------------------------------------------------------
190+
191+
enum InitState<T> {
192+
ManualUninitialized,
193+
AutoPrepared { initializer: Box<dyn FnOnce() -> T> },
194+
AutoInitializing, // needed because state cannot be empty
195+
Initialized { value: T },
196+
}

godot-core/src/obj/traits.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,17 @@ pub trait UserClass: GodotClass<Declarer = dom::UserDomain> {
160160
{
161161
Gd::default_instance()
162162
}
163+
164+
#[doc(hidden)]
165+
fn __config() -> crate::private::ClassConfig;
166+
167+
#[doc(hidden)]
168+
fn __before_ready(&mut self);
169+
170+
#[doc(hidden)]
171+
fn __default_virtual_call(_method_name: &str) -> sys::GDExtensionClassCallVirtual {
172+
None
173+
}
163174
}
164175

165176
/// Auto-implemented for all engine-provided classes.

godot-core/src/registry.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ pub enum PluginComponent {
8989
_class_user_data: *mut std::ffi::c_void,
9090
instance: sys::GDExtensionClassInstancePtr,
9191
),
92+
93+
/// Calls `__before_ready()`, if there is at least one `OnReady` field. Used if there is no `#[godot_api] impl` block
94+
/// overriding ready.
95+
default_get_virtual_fn: Option<
96+
unsafe extern "C" fn(
97+
p_userdata: *mut std::os::raw::c_void,
98+
p_name: sys::GDExtensionConstStringNamePtr,
99+
) -> sys::GDExtensionClassCallVirtual,
100+
>,
92101
},
93102

94103
/// Collected from `#[godot_api] impl MyClass`
@@ -165,6 +174,8 @@ struct ClassRegistrationInfo {
165174
register_methods_constants_fn: Option<ErasedRegisterFn>,
166175
register_properties_fn: Option<ErasedRegisterFn>,
167176
user_register_fn: Option<ErasedRegisterFn>,
177+
default_virtual_fn: sys::GDExtensionClassGetVirtual, // Option (set if there is at least one OnReady field)
178+
user_virtual_fn: sys::GDExtensionClassGetVirtual, // Option (set if there is a `#[godot_api] impl I*`)
168179

169180
/// Godot low-level class creation parameters.
170181
#[cfg(before_api = "4.2")]
@@ -224,6 +235,8 @@ pub fn register_class<
224235
user_register_fn: Some(ErasedRegisterFn {
225236
raw: callbacks::register_class_by_builder::<T>,
226237
}),
238+
user_virtual_fn: None,
239+
default_virtual_fn: None,
227240
godot_params,
228241
init_level: T::INIT_LEVEL.unwrap_or_else(|| {
229242
panic!("Unknown initialization level for class {}", T::class_name())
@@ -276,8 +289,8 @@ pub fn auto_register_classes(init_level: InitLevel) {
276289
.entry(init_level)
277290
.or_default()
278291
.push(info.class_name);
279-
register_class_raw(info);
280292

293+
register_class_raw(info);
281294
out!("Class {} loaded", class_name);
282295
}
283296

@@ -320,6 +333,7 @@ fn fill_class_info(component: PluginComponent, c: &mut ClassRegistrationInfo) {
320333
generated_recreate_fn,
321334
register_properties_fn,
322335
free_fn,
336+
default_get_virtual_fn,
323337
} => {
324338
c.parent_class_name = Some(base_class_name);
325339

@@ -350,6 +364,7 @@ fn fill_class_info(component: PluginComponent, c: &mut ClassRegistrationInfo) {
350364
assert!(generated_recreate_fn.is_none()); // not used
351365

352366
c.godot_params.free_instance_func = Some(free_fn);
367+
c.default_virtual_fn = default_get_virtual_fn;
353368
c.register_properties_fn = Some(register_properties_fn);
354369
}
355370

@@ -382,7 +397,7 @@ fn fill_class_info(component: PluginComponent, c: &mut ClassRegistrationInfo) {
382397

383398
c.godot_params.to_string_func = user_to_string_fn;
384399
c.godot_params.notification_func = user_on_notification_fn;
385-
c.godot_params.get_virtual_func = Some(get_virtual_fn);
400+
c.user_virtual_fn = Some(get_virtual_fn);
386401
}
387402
#[cfg(since_api = "4.1")]
388403
PluginComponent::EditorPlugin => {
@@ -404,14 +419,20 @@ fn fill_into<T>(dst: &mut Option<T>, src: Option<T>) -> Result<(), ()> {
404419
}
405420

406421
/// Registers a class with given the dynamic type information `info`.
407-
fn register_class_raw(info: ClassRegistrationInfo) {
422+
fn register_class_raw(mut info: ClassRegistrationInfo) {
408423
// First register class...
409424

410425
let class_name = info.class_name;
411426
let parent_class_name = info
412427
.parent_class_name
413428
.expect("class defined (parent_class_name)");
414429

430+
// Register virtual functions -- if the user provided some via #[godot_api], take those; otherwise, use the
431+
// ones generated alongside #[derive(GodotClass)]. The latter can also be null, if no OnReady is provided.
432+
if info.godot_params.get_virtual_func.is_none() {
433+
info.godot_params.get_virtual_func = info.user_virtual_fn.or(info.default_virtual_fn);
434+
}
435+
415436
unsafe {
416437
// Try to register class...
417438

@@ -587,6 +608,18 @@ pub mod callbacks {
587608
T::__virtual_call(method_name.as_str())
588609
}
589610

611+
pub unsafe extern "C" fn default_get_virtual<T: UserClass>(
612+
_class_user_data: *mut std::ffi::c_void,
613+
name: sys::GDExtensionConstStringNamePtr,
614+
) -> sys::GDExtensionClassCallVirtual {
615+
// This string is not ours, so we cannot call the destructor on it.
616+
let borrowed_string = StringName::from_string_sys(sys::force_mut_ptr(name));
617+
let method_name = borrowed_string.to_string();
618+
std::mem::forget(borrowed_string);
619+
620+
T::__default_virtual_call(method_name.as_str())
621+
}
622+
590623
pub unsafe extern "C" fn to_string<T: cap::GodotToString>(
591624
instance: sys::GDExtensionClassInstancePtr,
592625
_is_valid: *mut sys::GDExtensionBool,
@@ -691,6 +724,8 @@ fn default_registration_info(class_name: ClassName) -> ClassRegistrationInfo {
691724
register_methods_constants_fn: None,
692725
register_properties_fn: None,
693726
user_register_fn: None,
727+
default_virtual_fn: None,
728+
user_virtual_fn: None,
694729
godot_params: default_creation_info(),
695730
init_level: InitLevel::Scene,
696731
is_editor_plugin: false,

godot-macros/src/class/data_models/field.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub struct Field {
1414
pub default: Option<TokenStream>,
1515
pub var: Option<FieldVar>,
1616
pub export: Option<FieldExport>,
17+
pub is_onready: bool,
1718
}
1819

1920
impl Field {
@@ -24,6 +25,7 @@ impl Field {
2425
default: None,
2526
var: None,
2627
export: None,
28+
is_onready: false,
2729
}
2830
}
2931
}

0 commit comments

Comments
 (0)