Skip to content
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

OnReady<T> for late-init fields #534

Merged
merged 4 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions godot-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ pub mod private {
}
}

pub fn auto_init<T>(l: &mut crate::obj::OnReady<T>) {
l.init_auto();
}

fn print_panic_message(msg: &str) {
// If the message contains newlines, print all of the lines after a line break, and indent them.
let lbegin = "\n ";
Expand Down
2 changes: 2 additions & 0 deletions godot-core/src/obj/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ mod base;
mod gd;
mod guards;
mod instance_id;
mod onready;
mod raw;
mod traits;

pub use base::*;
pub use gd::*;
pub use guards::*;
pub use instance_id::*;
pub use onready::*;
pub use raw::*;
pub use traits::*;

Expand Down
196 changes: 196 additions & 0 deletions godot-core/src/obj/onready.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use std::mem;

/// Ergonomic late-initialization container with `ready()` support.
///
/// While deferred initialization is generally seen as bad practice, it is often inevitable in game development.
/// Godot in particular encourages initialization inside `ready()`, e.g. to access the scene tree after a node is inserted into it.
/// The alternative to using this pattern is [`Option<T>`][option], which needs to be explicitly unwrapped with `unwrap()` or `expect()` each time.
///
/// `OnReady<T>` should always be used as a field. There are two modes to use it:
///
/// 1. **Automatic mode, using [`new()`](Self::new).**<br>
/// Before `ready()` is called, all `OnReady` fields constructed with `new()` are automatically initialized, in the order of
/// declaration. This means that you can safely access them in `ready()`.<br><br>
/// 2. **Manual mode, using [`manual()`](Self::manual).**<br>
/// These fields are left uninitialized until you call [`init()`][Self::init] on them. This is useful if you need more complex
/// initialization scenarios than a closure allows. If you forget initialization, a panic will occur on first access.
///
/// Conceptually, `OnReady<T>` is very close to [once_cell's `Lazy<T>`][lazy], with additional hooks into the Godot lifecycle.
/// The absence of methods to check initialization state is deliberate: you don't need them if you follow the above two patterns.
/// This container is not designed as a general late-initialization solution, but tailored to the `ready()` semantics of Godot.
///
/// 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.
///
/// [option]: std::option::Option
/// [lazy]: https://docs.rs/once_cell/1/once_cell/unsync/struct.Lazy.html
///
/// # Example
/// ```
/// use godot::prelude::*;
///
/// #[derive(GodotClass)]
/// #[class(base = Node)]
/// struct MyClass {
/// auto: OnReady<i32>,
/// manual: OnReady<i32>,
/// }
///
/// #[godot_api]
/// impl INode for MyClass {
/// fn init(_base: Base<Node>) -> Self {
/// Self {
/// auto: OnReady::new(|| 11),
/// manual: OnReady::manual(),
/// }
/// }
///
/// fn ready(&mut self) {
/// // self.auto is now ready with value 11.
/// assert_eq!(*self.auto, 11);
///
/// // self.manual needs to be initialized manually.
/// self.manual.init(22);
/// assert_eq!(*self.manual, 22);
/// }
/// }
pub struct OnReady<T> {
state: InitState<T>,
}

impl<T> OnReady<T> {
/// Schedule automatic initialization before `ready()`.
///
/// This guarantees that the value is initialized once `ready()` starts running.
/// Until then, accessing the object may panic. In particular, the object is _not_ initialized on first use.
///
/// The value is also initialized when you don't override `ready()`.
///
/// For more control over initialization, use the [`OnReady::manual()`] constructor, followed by a [`self.init()`][OnReady::init]
/// call during `ready()`.
pub fn new<F>(init_fn: F) -> Self
where
F: FnOnce() -> T + 'static,
{
Self {
state: InitState::AutoPrepared {
initializer: Box::new(init_fn),
},
}
}

/// Leave uninitialized, expects manual initialization during `ready()`.
///
/// If you use this method, you _must_ call [`init()`][Self::init] during the `ready()` callback, otherwise a panic will occur.
pub fn manual() -> Self {
Self {
state: InitState::ManualUninitialized,
}
}

/// Runs manual initialization.
///
/// # Panics
/// - If `init()` was called before.
/// - If this object was already provided with a closure during construction, in [`Self::new()`].
pub fn init(&mut self, value: T) {
match &self.state {
InitState::ManualUninitialized { .. } => {
self.state = InitState::Initialized { value };
}
InitState::AutoPrepared { .. } => {
panic!("cannot call init() on auto-initialized OnReady objects")
}
InitState::AutoInitializing => {
// SAFETY: Loading is ephemeral state that is only set in init_auto() and immediately overwritten.
unsafe { std::hint::unreachable_unchecked() }
}
InitState::Initialized { .. } => {
panic!("already initialized; did you call init() more than once?")
}
};
}

/// Runs initialization.
///
/// # Panics
/// If the value is already initialized.
pub(crate) fn init_auto(&mut self) {
// Two branches needed, because mem::replace() could accidentally overwrite an already initialized value.
match &self.state {
InitState::ManualUninitialized => return, // skipped
InitState::AutoPrepared { .. } => {} // handled below
InitState::AutoInitializing => {
// SAFETY: Loading is ephemeral state that is only set below and immediately overwritten.
unsafe { std::hint::unreachable_unchecked() }
}
InitState::Initialized { .. } => panic!("OnReady object already initialized"),
};

// Temporarily replace with dummy state, as it's not possible to take ownership of the initializer closure otherwise.
let InitState::AutoPrepared { initializer } =
mem::replace(&mut self.state, InitState::AutoInitializing)
else {
// SAFETY: condition checked above.
unsafe { std::hint::unreachable_unchecked() }
};

self.state = InitState::Initialized {
value: initializer(),
};
}
}

// Panicking Deref is not best practice according to Rust, but constant get() calls are significantly less ergonomic and make it harder to
// migrate between T and LateInit<T>, because all the accesses need to change.
impl<T> std::ops::Deref for OnReady<T> {
type Target = T;

/// Returns a shared reference to the value.
///
/// # Panics
/// If the value is not yet initialized.
fn deref(&self) -> &Self::Target {
match &self.state {
InitState::ManualUninitialized => {
panic!("OnReady manual value uninitialized, did you call init()?")
}
InitState::AutoPrepared { .. } => {
panic!("OnReady automatic value uninitialized, is only available in ready()")
}
InitState::AutoInitializing => unreachable!(),
InitState::Initialized { value } => value,
}
}
}

impl<T> std::ops::DerefMut for OnReady<T> {
/// Returns an exclusive reference to the value.
///
/// # Panics
/// If the value is not yet initialized.
fn deref_mut(&mut self) -> &mut Self::Target {
match &mut self.state {
InitState::Initialized { value } => value,
InitState::ManualUninitialized { .. } | InitState::AutoPrepared { .. } => {
panic!("value not yet initialized")
}
InitState::AutoInitializing => unreachable!(),
}
}
}

// ----------------------------------------------------------------------------------------------------------------------------------------------

enum InitState<T> {
ManualUninitialized,
AutoPrepared { initializer: Box<dyn FnOnce() -> T> },
AutoInitializing, // needed because state cannot be empty
Initialized { value: T },
}
11 changes: 11 additions & 0 deletions godot-core/src/obj/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,17 @@ pub trait UserClass: GodotClass<Declarer = dom::UserDomain> {
{
Gd::default_instance()
}

#[doc(hidden)]
fn __config() -> crate::private::ClassConfig;

#[doc(hidden)]
fn __before_ready(&mut self);

#[doc(hidden)]
fn __default_virtual_call(_method_name: &str) -> sys::GDExtensionClassCallVirtual {
None
}
}

/// Auto-implemented for all engine-provided classes.
Expand Down
41 changes: 38 additions & 3 deletions godot-core/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ pub enum PluginComponent {
_class_user_data: *mut std::ffi::c_void,
instance: sys::GDExtensionClassInstancePtr,
),

/// Calls `__before_ready()`, if there is at least one `OnReady` field. Used if there is no `#[godot_api] impl` block
/// overriding ready.
default_get_virtual_fn: Option<
unsafe extern "C" fn(
p_userdata: *mut std::os::raw::c_void,
p_name: sys::GDExtensionConstStringNamePtr,
) -> sys::GDExtensionClassCallVirtual,
>,
},

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

/// Godot low-level class creation parameters.
#[cfg(before_api = "4.2")]
Expand Down Expand Up @@ -224,6 +235,8 @@ pub fn register_class<
user_register_fn: Some(ErasedRegisterFn {
raw: callbacks::register_class_by_builder::<T>,
}),
user_virtual_fn: None,
default_virtual_fn: None,
godot_params,
init_level: T::INIT_LEVEL.unwrap_or_else(|| {
panic!("Unknown initialization level for class {}", T::class_name())
Expand Down Expand Up @@ -276,8 +289,8 @@ pub fn auto_register_classes(init_level: InitLevel) {
.entry(init_level)
.or_default()
.push(info.class_name);
register_class_raw(info);

register_class_raw(info);
out!("Class {} loaded", class_name);
}

Expand Down Expand Up @@ -320,6 +333,7 @@ fn fill_class_info(component: PluginComponent, c: &mut ClassRegistrationInfo) {
generated_recreate_fn,
register_properties_fn,
free_fn,
default_get_virtual_fn,
} => {
c.parent_class_name = Some(base_class_name);

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

c.godot_params.free_instance_func = Some(free_fn);
c.default_virtual_fn = default_get_virtual_fn;
c.register_properties_fn = Some(register_properties_fn);
}

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

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

/// Registers a class with given the dynamic type information `info`.
fn register_class_raw(info: ClassRegistrationInfo) {
fn register_class_raw(mut info: ClassRegistrationInfo) {
// First register class...

let class_name = info.class_name;
let parent_class_name = info
.parent_class_name
.expect("class defined (parent_class_name)");

// Register virtual functions -- if the user provided some via #[godot_api], take those; otherwise, use the
// ones generated alongside #[derive(GodotClass)]. The latter can also be null, if no OnReady is provided.
if info.godot_params.get_virtual_func.is_none() {
info.godot_params.get_virtual_func = info.user_virtual_fn.or(info.default_virtual_fn);
}

unsafe {
// Try to register class...

Expand Down Expand Up @@ -587,6 +608,18 @@ pub mod callbacks {
T::__virtual_call(method_name.as_str())
}

pub unsafe extern "C" fn default_get_virtual<T: UserClass>(
_class_user_data: *mut std::ffi::c_void,
name: sys::GDExtensionConstStringNamePtr,
) -> sys::GDExtensionClassCallVirtual {
// This string is not ours, so we cannot call the destructor on it.
let borrowed_string = StringName::from_string_sys(sys::force_mut_ptr(name));
let method_name = borrowed_string.to_string();
std::mem::forget(borrowed_string);

T::__default_virtual_call(method_name.as_str())
}

pub unsafe extern "C" fn to_string<T: cap::GodotToString>(
instance: sys::GDExtensionClassInstancePtr,
_is_valid: *mut sys::GDExtensionBool,
Expand Down Expand Up @@ -691,6 +724,8 @@ fn default_registration_info(class_name: ClassName) -> ClassRegistrationInfo {
register_methods_constants_fn: None,
register_properties_fn: None,
user_register_fn: None,
default_virtual_fn: None,
user_virtual_fn: None,
godot_params: default_creation_info(),
init_level: InitLevel::Scene,
is_editor_plugin: false,
Expand Down
2 changes: 2 additions & 0 deletions godot-macros/src/class/data_models/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub struct Field {
pub default: Option<TokenStream>,
pub var: Option<FieldVar>,
pub export: Option<FieldExport>,
pub is_onready: bool,
}

impl Field {
Expand All @@ -24,6 +25,7 @@ impl Field {
default: None,
var: None,
export: None,
is_onready: false,
}
}
}
Expand Down
Loading
Loading