diff --git a/godot-core/src/lib.rs b/godot-core/src/lib.rs index 4779a7954..739becbd0 100644 --- a/godot-core/src/lib.rs +++ b/godot-core/src/lib.rs @@ -92,6 +92,10 @@ pub mod private { } } + pub fn auto_init(l: &mut crate::obj::OnReady) { + 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 "; diff --git a/godot-core/src/obj/mod.rs b/godot-core/src/obj/mod.rs index 9bdfe2d83..68c712bfa 100644 --- a/godot-core/src/obj/mod.rs +++ b/godot-core/src/obj/mod.rs @@ -15,6 +15,7 @@ mod base; mod gd; mod guards; mod instance_id; +mod onready; mod raw; mod traits; @@ -22,6 +23,7 @@ pub use base::*; pub use gd::*; pub use guards::*; pub use instance_id::*; +pub use onready::*; pub use raw::*; pub use traits::*; diff --git a/godot-core/src/obj/onready.rs b/godot-core/src/obj/onready.rs new file mode 100644 index 000000000..7afb02a67 --- /dev/null +++ b/godot-core/src/obj/onready.rs @@ -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`][option], which needs to be explicitly unwrapped with `unwrap()` or `expect()` each time. +/// +/// `OnReady` should always be used as a field. There are two modes to use it: +/// +/// 1. **Automatic mode, using [`new()`](Self::new).**
+/// 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()`.

+/// 2. **Manual mode, using [`manual()`](Self::manual).**
+/// 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` is very close to [once_cell's `Lazy`][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, +/// manual: OnReady, +/// } +/// +/// #[godot_api] +/// impl INode for MyClass { +/// fn init(_base: Base) -> 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 { + state: InitState, +} + +impl OnReady { + /// 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(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, because all the accesses need to change. +impl std::ops::Deref for OnReady { + 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 std::ops::DerefMut for OnReady { + /// 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 { + ManualUninitialized, + AutoPrepared { initializer: Box T> }, + AutoInitializing, // needed because state cannot be empty + Initialized { value: T }, +} diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index 542e44430..aadb4238e 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -160,6 +160,17 @@ pub trait UserClass: GodotClass { { 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. diff --git a/godot-core/src/registry.rs b/godot-core/src/registry.rs index 51f30140c..ef71e97aa 100644 --- a/godot-core/src/registry.rs +++ b/godot-core/src/registry.rs @@ -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` @@ -165,6 +174,8 @@ struct ClassRegistrationInfo { register_methods_constants_fn: Option, register_properties_fn: Option, user_register_fn: Option, + 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")] @@ -224,6 +235,8 @@ pub fn register_class< user_register_fn: Some(ErasedRegisterFn { raw: callbacks::register_class_by_builder::, }), + 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()) @@ -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); } @@ -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); @@ -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); } @@ -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 => { @@ -404,7 +419,7 @@ fn fill_into(dst: &mut Option, src: Option) -> 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; @@ -412,6 +427,12 @@ fn register_class_raw(info: ClassRegistrationInfo) { .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... @@ -587,6 +608,18 @@ pub mod callbacks { T::__virtual_call(method_name.as_str()) } + pub unsafe extern "C" fn default_get_virtual( + _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( instance: sys::GDExtensionClassInstancePtr, _is_valid: *mut sys::GDExtensionBool, @@ -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, diff --git a/godot-macros/src/class/data_models/field.rs b/godot-macros/src/class/data_models/field.rs index 39e93689c..c2b6f4cfa 100644 --- a/godot-macros/src/class/data_models/field.rs +++ b/godot-macros/src/class/data_models/field.rs @@ -14,6 +14,7 @@ pub struct Field { pub default: Option, pub var: Option, pub export: Option, + pub is_onready: bool, } impl Field { @@ -24,6 +25,7 @@ impl Field { default: None, var: None, export: None, + is_onready: false, } } } diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 41726205a..563792a7d 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -6,6 +6,7 @@ */ use crate::util; +use crate::util::ident; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; @@ -24,14 +25,14 @@ pub struct FuncDefinition { // // There are currently no virtual static methods. Additionally, virtual static methods dont really make a lot // of sense. Therefore there is no need to support them. -pub fn make_virtual_method_callback( +pub fn make_virtual_callback( class_name: &Ident, - method_signature: &venial::Function, + signature_info: SignatureInfo, + before_kind: BeforeKind, ) -> TokenStream { - let signature_info = get_signature_info(method_signature, false); - let method_name = &method_signature.name; + let method_name = &signature_info.method_name; - let wrapped_method = make_forwarding_closure(class_name, &signature_info); + let wrapped_method = make_forwarding_closure(class_name, &signature_info, before_kind); let sig_tuple = util::make_signature_tuple_type(&signature_info.ret_type, &signature_info.param_types); @@ -67,7 +68,8 @@ pub fn make_method_registration( let method_flags = make_method_flags(signature_info.receiver_type); - let forwarding_closure = make_forwarding_closure(class_name, &signature_info); + let forwarding_closure = + make_forwarding_closure(class_name, &signature_info, BeforeKind::Without); let varcall_func = make_varcall_func(method_name, &sig_tuple, &forwarding_closure); let ptrcall_func = make_ptrcall_func(method_name, &sig_tuple, &forwarding_closure); @@ -135,14 +137,14 @@ pub fn make_method_registration( // Implementation #[derive(Copy, Clone, Eq, PartialEq)] -enum ReceiverType { +pub enum ReceiverType { Ref, Mut, GdSelf, Static, } -struct SignatureInfo { +pub struct SignatureInfo { pub method_name: Ident, pub receiver_type: ReceiverType, pub param_idents: Vec, @@ -150,8 +152,35 @@ struct SignatureInfo { pub ret_type: TokenStream, } +impl SignatureInfo { + pub fn fn_ready() -> Self { + Self { + method_name: ident("ready"), + receiver_type: ReceiverType::Mut, + param_idents: vec![], + param_types: vec![], + ret_type: quote! { () }, + } + } +} + +pub enum BeforeKind { + /// Default: just call the method. + Without, + + /// Call `before_{method}` before calling the method itself. + WithBefore, + + /// Call **only** `before_{method}`, not the method itself. + OnlyBefore, +} + /// Returns a closure expression that forwards the parameters to the Rust instance. -fn make_forwarding_closure(class_name: &Ident, signature_info: &SignatureInfo) -> TokenStream { +fn make_forwarding_closure( + class_name: &Ident, + signature_info: &SignatureInfo, + before_kind: BeforeKind, +) -> TokenStream { let method_name = &signature_info.method_name; let params = &signature_info.param_idents; @@ -165,21 +194,40 @@ fn make_forwarding_closure(class_name: &Ident, signature_info: &SignatureInfo) - _ => quote! {}, }; + let before_method_call = match before_kind { + BeforeKind::WithBefore | BeforeKind::OnlyBefore => { + let before_method = format_ident!("__before_{}", method_name); + quote! { instance.#before_method(); } + } + BeforeKind::Without => TokenStream::new(), + }; + match signature_info.receiver_type { ReceiverType::Ref | ReceiverType::Mut => { + // Generated default virtual methods (e.g. for ready) may not have an actual implementation (user code), so + // all they need to do is call the __before_ready() method. This means the actual method call may be optional. + let method_call = if matches!(before_kind, BeforeKind::OnlyBefore) { + TokenStream::new() + } else { + quote! { instance.#method_name(#(#params),*) } + }; + quote! { |instance_ptr, params| { let ( #(#params,)* ) = params; let storage = unsafe { ::godot::private::as_storage::<#class_name>(instance_ptr) }; - #instance_decl - instance.#method_name(#(#params),*) + #instance_decl + #before_method_call + #method_call } } } ReceiverType::GdSelf => { + // Method call is always present, since GdSelf implies that the user declares the method. + // (Absent method is only used in the case of a generated default virtual method, e.g. for ready()). quote! { |instance_ptr, params| { let ( #(#params,)* ) = params; @@ -187,11 +235,13 @@ fn make_forwarding_closure(class_name: &Ident, signature_info: &SignatureInfo) - let storage = unsafe { ::godot::private::as_storage::<#class_name>(instance_ptr) }; + #before_method_call <#class_name>::#method_name(storage.get_gd(), #(#params),*) } } } ReceiverType::Static => { + // No before-call needed, since static methods are not virtual. quote! { |_, params| { let ( #(#params,)* ) = params; @@ -202,7 +252,7 @@ fn make_forwarding_closure(class_name: &Ident, signature_info: &SignatureInfo) - } } -fn get_signature_info(signature: &venial::Function, has_gd_self: bool) -> SignatureInfo { +pub(crate) fn get_signature_info(signature: &venial::Function, has_gd_self: bool) -> SignatureInfo { let method_name = signature.name.clone(); let mut receiver_type = if has_gd_self { ReceiverType::GdSelf diff --git a/godot-macros/src/class/data_models/property.rs b/godot-macros/src/class/data_models/property.rs index abe9b4d29..e4a96cf9b 100644 --- a/godot-macros/src/class/data_models/property.rs +++ b/godot-macros/src/class/data_models/property.rs @@ -73,7 +73,6 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream { let field_class_name = util::property_variant_class_name(field_type); let field_name = field_ident.to_string(); - // rustfmt wont format this if we put it in the let-else. let FieldVar { getter, setter, @@ -141,37 +140,16 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream { }, }; - let getter_name = if let Some(getter_impl) = getter.to_impl(class_name, GetSet::Get, field) - { - let GetterSetterImpl { - function_name, - function_impl, - export_token, - } = getter_impl; - - getter_setter_impls.push(function_impl); - export_tokens.push(export_token); - - function_name.to_string() - } else { - String::new() - }; - - let setter_name = if let Some(setter_impl) = setter.to_impl(class_name, GetSet::Set, field) - { - let GetterSetterImpl { - function_name, - function_impl, - export_token, - } = setter_impl; - - getter_setter_impls.push(function_impl); - export_tokens.push(export_token); - - function_name.to_string() - } else { - String::new() - }; + let getter_name = make_getter_setter( + getter.to_impl(class_name, GetSet::Get, field), + &mut getter_setter_impls, + &mut export_tokens, + ); + let setter_name = make_getter_setter( + setter.to_impl(class_name, GetSet::Set, field), + &mut getter_setter_impls, + &mut export_tokens, + ); export_tokens.push(quote! { use ::godot::sys::GodotFfi; @@ -221,3 +199,24 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream { } } } + +fn make_getter_setter( + getter_setter_impl: Option, + getter_setter_impls: &mut Vec, + export_tokens: &mut Vec, +) -> String { + if let Some(getter_impl) = getter_setter_impl { + let GetterSetterImpl { + function_name, + function_impl, + export_token, + } = getter_impl; + + getter_setter_impls.push(function_impl); + export_tokens.push(export_token); + + function_name.to_string() + } else { + String::new() + } +} diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index 8539e7038..489fa893a 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -9,8 +9,11 @@ use proc_macro2::{Ident, Punct, TokenStream}; use quote::{format_ident, quote}; use venial::{Declaration, NamedField, Struct, StructFields}; -use crate::class::{make_property_impl, Field, FieldExport, FieldVar, Fields}; -use crate::util::{bail, ident, KvParser}; +use crate::class::{ + make_property_impl, make_virtual_callback, BeforeKind, Field, FieldExport, FieldVar, Fields, + SignatureInfo, +}; +use crate::util::{bail, ident, path_ends_with_complex, KvParser}; use crate::{util, ParseResult}; pub fn derive_godot_class(decl: Declaration) -> ParseResult { @@ -63,6 +66,9 @@ pub fn derive_godot_class(decl: Declaration) -> ParseResult { quote! {} }; + let (user_class_impl, has_default_virtual) = + make_user_class_impl(class_name, struct_cfg.is_tool, &fields.all_fields); + let (godot_init_impl, create_fn, recreate_fn); if struct_cfg.has_generated_init { godot_init_impl = make_godot_init_impl(class_name, fields); @@ -78,7 +84,11 @@ pub fn derive_godot_class(decl: Declaration) -> ParseResult { recreate_fn = quote! { None }; }; - let config_impl = make_config_impl(class_name, struct_cfg.is_tool); + let default_get_virtual_fn = if has_default_virtual { + quote! { Some(#prv::callbacks::default_get_virtual::<#class_name>) } + } else { + quote! { None } + }; Ok(quote! { unsafe impl ::godot::obj::GodotClass for #class_name { @@ -91,12 +101,11 @@ pub fn derive_godot_class(decl: Declaration) -> ParseResult { ::godot::builtin::meta::ClassName::from_ascii_cstr(#class_name_cstr) } } - impl ::godot::obj::UserClass for #class_name {} #godot_init_impl #godot_withbase_impl #godot_exports_impl - #config_impl + #user_class_impl ::godot::sys::plugin_add!(__GODOT_PLUGIN_REGISTRY in #prv; #prv::ClassPlugin { class_name: #class_name_obj, @@ -108,6 +117,7 @@ pub fn derive_godot_class(decl: Declaration) -> ParseResult { raw: #prv::callbacks::register_user_properties::<#class_name>, }, free_fn: #prv::callbacks::free::<#class_name>, + default_get_virtual_fn: #default_get_virtual_fn, }, init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL, }); @@ -118,6 +128,62 @@ pub fn derive_godot_class(decl: Declaration) -> ParseResult { }) } +fn make_user_class_impl( + class_name: &Ident, + is_tool: bool, + all_fields: &[Field], +) -> (TokenStream, bool) { + let onready_field_inits = all_fields + .iter() + .filter(|&field| field.is_onready) + .map(|field| { + let field = &field.name; + quote! { + ::godot::private::auto_init(&mut self.#field); + } + }); + + let default_virtual_fn = if all_fields.iter().any(|field| field.is_onready) { + let tool_check = util::make_virtual_tool_check(); + let signature_info = SignatureInfo::fn_ready(); + + let callback = make_virtual_callback(class_name, signature_info, BeforeKind::OnlyBefore); + let default_virtual_fn = quote! { + fn __default_virtual_call(name: &str) -> ::godot::sys::GDExtensionClassCallVirtual { + use ::godot::obj::UserClass as _; + #tool_check + + if name == "_ready" { + #callback + } else { + None + } + } + }; + Some(default_virtual_fn) + } else { + None + }; + + let user_class_impl = quote! { + impl ::godot::obj::UserClass for #class_name { + fn __config() -> ::godot::private::ClassConfig { + ::godot::private::ClassConfig { + is_tool: #is_tool, + } + } + + fn __before_ready(&mut self) { + #( #onready_field_inits )* + } + + #default_virtual_fn + } + }; + + (user_class_impl, default_virtual_fn.is_some()) +} + /// Checks at compile time that a function with the given name exists on `Self`. #[must_use] pub fn make_existence_check(ident: &Ident) -> TokenStream { @@ -194,16 +260,21 @@ fn parse_fields(class: &Struct) -> ParseResult { // #[base] if let Some(parser) = KvParser::parse(&named_field.attributes, "base")? { if let Some(prev_base) = base_field.as_ref() { - bail!( + return bail!( parser.span(), "#[base] allowed for at most 1 field, already applied to `{}`", prev_base.name - )?; + ); } is_base = true; parser.finish()?; } + // OnReady type inference + if path_ends_with_complex(&field.ty, "OnReady") { + field.is_onready = true; + } + // #[init] if let Some(mut parser) = KvParser::parse(&named_field.attributes, "init")? { let default = parser.handle_expr("default")?; @@ -233,6 +304,9 @@ fn parse_fields(class: &Struct) -> ParseResult { } } + // TODO detect #[base] based on type instead of attribute + // Edge cases (type aliases, user types with same name, ...) could be handled with #[hint(base)] or #[hint(no_base)]. + Ok(Fields { all_fields, base_field, @@ -277,16 +351,3 @@ fn make_godot_init_impl(class_name: &Ident, fields: Fields) -> TokenStream { } } } - -fn make_config_impl(class_name: &Ident, is_tool: bool) -> TokenStream { - quote! { - impl #class_name { - #[doc(hidden)] - pub fn __config() -> ::godot::private::ClassConfig { - ::godot::private::ClassConfig { - is_tool: #is_tool, - } - } - } - } -} diff --git a/godot-macros/src/class/godot_api.rs b/godot-macros/src/class/godot_api.rs index 2d2f209ac..48eccadc8 100644 --- a/godot-macros/src/class/godot_api.rs +++ b/godot-macros/src/class/godot_api.rs @@ -13,7 +13,10 @@ use venial::{ TyExpr, }; -use crate::class::{make_method_registration, make_virtual_method_callback, FuncDefinition}; +use crate::class::{ + get_signature_info, make_method_registration, make_virtual_callback, BeforeKind, + FuncDefinition, SignatureInfo, +}; use crate::util; use crate::util::{bail, KvParser}; @@ -571,6 +574,7 @@ fn transform_trait_impl(original_impl: Impl) -> Result { #(#cfg_attrs)* impl ::godot::obj::cap::GodotNotification for #class_name { fn __godot_notification(&mut self, what: i32) { + use ::godot::obj::UserClass as _; if ::godot::private::is_class_inactive(Self::__config().is_tool) { return; } @@ -603,20 +607,46 @@ fn transform_trait_impl(original_impl: Impl) -> Result { } else { format!("_{method_name}") }; + + let signature_info = get_signature_info(&method, false); + + // Overridden ready() methods additionally have an additional `__before_ready()` call (for OnReady inits). + let before_kind = if method_name == "ready" { + BeforeKind::WithBefore + } else { + BeforeKind::Without + }; + // Note that, if the same method is implemented multiple times (with different cfg attr combinations), // then there will be multiple match arms annotated with the same cfg attr combinations, thus they will // be reduced to just one arm (at most, if the implementations aren't all removed from compilation) for // each distinct method. virtual_method_cfg_attrs.push(cfg_attrs); virtual_method_names.push(virtual_method_name); - virtual_methods.push(method); + virtual_methods.push((signature_info, before_kind)); } } } - let virtual_method_callbacks: Vec = virtual_methods + // If there is no ready() method explicitly overridden, we need to add one, to ensure that __before_ready() is called to + // initialize the OnReady fields. + if !virtual_methods .iter() - .map(|method| make_virtual_method_callback(&class_name, method)) + .any(|(sig, _)| sig.method_name == "ready") + { + let signature_info = SignatureInfo::fn_ready(); + + virtual_method_cfg_attrs.push(vec![]); + virtual_method_names.push("_ready".to_string()); + virtual_methods.push((signature_info, BeforeKind::OnlyBefore)); + } + + let tool_check = util::make_virtual_tool_check(); + let virtual_method_callbacks: Vec = virtual_methods + .into_iter() + .map(|(signature_info, before_kind)| { + make_virtual_callback(&class_name, signature_info, before_kind) + }) .collect(); // Use 'match' as a way to only emit 'Some(...)' if the given cfg attrs allow. @@ -642,10 +672,8 @@ fn transform_trait_impl(original_impl: Impl) -> Result { impl ::godot::obj::cap::ImplementsGodotVirtual for #class_name { fn __virtual_call(name: &str) -> ::godot::sys::GDExtensionClassCallVirtual { //println!("virtual_call: {}.{}", std::any::type_name::(), name); - - if ::godot::private::is_class_inactive(Self::__config().is_tool) { - return None; - } + use ::godot::obj::UserClass as _; + #tool_check match name { #( diff --git a/godot-macros/src/util/mod.rs b/godot-macros/src/util/mod.rs index 7c5eddd1b..579c04f58 100644 --- a/godot-macros/src/util/mod.rs +++ b/godot-macros/src/util/mod.rs @@ -222,12 +222,22 @@ pub(crate) fn path_is_single(path: &[TokenTree], expected: &str) -> bool { } pub(crate) fn path_ends_with(path: &[TokenTree], expected: &str) -> bool { - // could also use TyExpr::as_path() + // Could also use TyExpr::as_path(), or fn below this one. path.last() .map(|last| last.to_string() == expected) .unwrap_or(false) } +pub(crate) fn path_ends_with_complex(path: &venial::TyExpr, expected: &str) -> bool { + path.as_path() + .map(|path| { + path.segments + .last() + .map_or(false, |seg| seg.ident == expected) + }) + .unwrap_or(false) +} + pub(crate) fn extract_cfg_attrs( attrs: &[venial::Attribute], ) -> impl IntoIterator { @@ -270,3 +280,11 @@ pub(crate) fn decl_get_info(decl: &venial::Declaration) -> DeclInfo { name_string, } } + +pub fn make_virtual_tool_check() -> TokenStream { + quote! { + if ::godot::private::is_class_inactive(Self::__config().is_tool) { + return None; + } + } +} diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 4c8a4dfb3..625f3d7a1 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -231,7 +231,9 @@ pub mod prelude { }; pub use super::init::{gdextension, ExtensionLibrary, InitLevel}; pub use super::log::*; - pub use super::obj::{Base, Gd, GdMut, GdRef, GodotClass, Inherits, InstanceId, Share}; + pub use super::obj::{ + Base, Gd, GdMut, GdRef, GodotClass, Inherits, InstanceId, OnReady, Share, + }; // Make trait methods available pub use super::engine::NodeExt as _; diff --git a/itest/rust/src/object_tests/mod.rs b/itest/rust/src/object_tests/mod.rs index 77fa19fae..6cb0b4454 100644 --- a/itest/rust/src/object_tests/mod.rs +++ b/itest/rust/src/object_tests/mod.rs @@ -8,6 +8,7 @@ mod base_test; mod class_rename_test; mod object_test; +mod onready_test; mod property_template_test; mod property_test; mod singleton_test; diff --git a/itest/rust/src/object_tests/onready_test.rs b/itest/rust/src/object_tests/onready_test.rs new file mode 100644 index 000000000..7606f15ee --- /dev/null +++ b/itest/rust/src/object_tests/onready_test.rs @@ -0,0 +1,201 @@ +/* + * 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 crate::framework::{expect_panic, itest}; +use godot::bind::{godot_api, GodotClass}; +use godot::engine::notify::NodeNotification; +use godot::engine::INode; + +use godot::obj::{Gd, OnReady}; + +#[itest] +fn onready_deref() { + let mut l = OnReady::::new(|| 42); + godot::private::auto_init(&mut l); + + // DerefMut + let mut_ref: &mut i32 = &mut l; + assert_eq!(*mut_ref, 42); + + // Deref + let l = l; + let shared_ref: &i32 = &l; + assert_eq!(*shared_ref, 42); +} + +#[itest] +fn onready_deref_on_uninit() { + expect_panic("Deref on uninit fails", || { + let l = OnReady::::new(|| 42); + let _ref: &i32 = &l; + }); + + expect_panic("DerefMut on uninit fails", || { + let mut l = OnReady::::new(|| 42); + let _ref: &mut i32 = &mut l; + }); +} + +#[itest] +fn onready_multi_init() { + expect_panic("init() on already initialized container fails", || { + let mut l = OnReady::::new(|| 42); + godot::private::auto_init(&mut l); + godot::private::auto_init(&mut l); + }); +} + +#[itest(skip)] // Not yet implemented. +fn onready_lifecycle_forget() { + let mut forgetful = OnReadyWithImpl::create(false); + let forgetful_copy = forgetful.clone(); + + expect_panic( + "Forgetting to initialize OnReady during ready() panics", + move || { + forgetful.notify(NodeNotification::Ready); + }, + ); + + forgetful_copy.free(); +} + +#[itest] +fn onready_lifecycle() { + let mut obj = OnReadyWithImpl::create(true); + + obj.notify(NodeNotification::Ready); + + { + let mut obj = obj.bind_mut(); + assert_eq!(*obj.auto, 11); + assert_eq!(*obj.manual, 22); + + *obj.auto = 33; + assert_eq!(*obj.auto, 33); + } + + obj.free(); +} + +#[itest] +fn onready_lifecycle_without_impl() { + let mut obj = OnReadyWithoutImpl::create(); + + obj.notify(NodeNotification::Ready); + + { + let mut obj = obj.bind_mut(); + assert_eq!(*obj.auto, 44); + + *obj.auto = 55; + assert_eq!(*obj.auto, 55); + } + + obj.free(); +} + +#[itest] +fn onready_lifecycle_with_impl_without_ready() { + let mut obj = OnReadyWithImplWithoutReady::create(); + + obj.notify(NodeNotification::Ready); + + { + let mut obj = obj.bind_mut(); + assert_eq!(*obj.auto, 66); + + *obj.auto = 77; + assert_eq!(*obj.auto, 77); + } + + obj.free(); +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +#[derive(GodotClass)] +#[class(base=Node)] +struct OnReadyWithImpl { + auto: OnReady, + manual: OnReady, + runs_manual_init: bool, +} + +impl OnReadyWithImpl { + fn create(runs_manual_init: bool) -> Gd { + let obj = Self { + auto: OnReady::new(|| 11), + manual: OnReady::manual(), + runs_manual_init, + }; + + Gd::from_object(obj) + } +} + +#[godot_api] +impl INode for OnReadyWithImpl { + fn ready(&mut self) { + assert_eq!(*self.auto, 11); + + if self.runs_manual_init { + self.manual.init(22); + assert_eq!(*self.manual, 22); + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +// Class that doesn't have a #[godot_api] impl. Used to test whether variables are still initialized. +#[derive(GodotClass)] +#[class(base=Node)] +struct OnReadyWithoutImpl { + auto: OnReady, + // No manual one, since those cannot be initialized without a ready() override. + // (Technically they _can_ at the moment, but in the future we might ensure initialization after ready, so this is not a supported workflow). +} + +// Rust-only impl, no proc macros. +impl OnReadyWithoutImpl { + fn create() -> Gd { + let obj = Self { + auto: OnReady::new(|| 44), + }; + + Gd::from_object(obj) + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +// Class that has a #[godot_api] impl, but does not override ready. Used to test whether variables are still initialized. +#[derive(GodotClass)] +#[class(base=Node)] +struct OnReadyWithImplWithoutReady { + auto: OnReady, + // No manual one, since those cannot be initialized without a ready() override. + // (Technically they _can_ at the moment, but in the future we might ensure initialization after ready, so this is not a supported workflow). +} + +// Rust-only impl, no proc macros. +impl OnReadyWithImplWithoutReady { + fn create() -> Gd { + let obj = Self { + auto: OnReady::new(|| 66), + }; + + Gd::from_object(obj) + } +} + +#[godot_api] +impl INode for OnReadyWithoutImpl { + // Declare another function to ensure virtual getter must be provided. + fn process(&mut self, _delta: f64) {} +}