diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index 7419174dc..dc415cca8 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -102,8 +102,8 @@ fn gdext_on_level_init(level: InitLevel) { } /// Tasks needed to be done by gdext internally upon unloading an initialization level. Called after user code. -fn gdext_on_level_deinit(_level: InitLevel) { - // No logic at the moment. +fn gdext_on_level_deinit(level: InitLevel) { + crate::unregister_classes(level); } // ---------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/godot-core/src/registry.rs b/godot-core/src/registry.rs index cd9aac365..cc5390c64 100644 --- a/godot-core/src/registry.rs +++ b/godot-core/src/registry.rs @@ -7,10 +7,10 @@ #![allow(dead_code)] // FIXME use crate::init::InitLevel; -use crate::log; use crate::obj::*; use crate::private::as_storage; use crate::storage::InstanceStorage; +use crate::{godot_print, log}; use godot_ffi as sys; use sys::interface_fn; @@ -20,8 +20,16 @@ use crate::builtin::StringName; use crate::out; use std::any::Any; use std::collections::HashMap; +use std::sync::{Mutex, MutexGuard, TryLockError}; use std::{fmt, ptr}; +// For now, that variable is needed for class unregistering. It's populated during class +// registering. There is no actual concurrency here, because Godot call register/unregister in main +// thread - Mutex is just casual way to ensure safety in this performance non-critical path. +// Note that we panic on concurrent access instead of blocking - that's fail fast approach. If that +// happen, most likely something changed on Godot side and analysis required to adopt these changes. +static LOADED_CLASSES: Mutex>>> = Mutex::new(None); + // TODO(bromeon): some information coming from the proc-macro API is deferred through PluginComponent, while others is directly // translated to code. Consider moving more code to the PluginComponent, which allows for more dynamic registration and will // be easier for a future builder API. @@ -63,6 +71,13 @@ pub enum PluginComponent { ) -> sys::GDExtensionObjectPtr, >, + generated_recreate_fn: Option< + unsafe extern "C" fn( + p_class_userdata: *mut std::ffi::c_void, + p_object: sys::GDExtensionObjectPtr, + ) -> sys::GDExtensionClassInstancePtr, + >, + free_fn: unsafe extern "C" fn( _class_user_data: *mut std::ffi::c_void, instance: sys::GDExtensionClassInstancePtr, @@ -89,6 +104,13 @@ pub enum PluginComponent { ) -> sys::GDExtensionObjectPtr, >, + user_recreate_fn: Option< + unsafe extern "C" fn( + p_class_userdata: *mut ::std::os::raw::c_void, + p_object: sys::GDExtensionObjectPtr, + ) -> sys::GDExtensionClassInstancePtr, + >, + /// User-defined `to_string` function user_to_string_fn: Option< unsafe extern "C" fn( @@ -99,12 +121,21 @@ pub enum PluginComponent { >, /// User-defined `on_notification` function + #[cfg(before_api = "4.2")] user_on_notification_fn: Option< unsafe extern "C" fn( p_instance: sys::GDExtensionClassInstancePtr, // p_what: i32, ), >, + #[cfg(since_api = "4.2")] + user_on_notification_fn: Option< + unsafe extern "C" fn( + p_instance: sys::GDExtensionClassInstancePtr, // + p_what: i32, + p_reversed: sys::GDExtensionBool, + ), + >, /// Callback for other virtuals get_virtual_fn: unsafe extern "C" fn( @@ -125,7 +156,10 @@ struct ClassRegistrationInfo { parent_class_name: Option, generated_register_fn: Option, user_register_fn: Option, + #[cfg(before_api = "4.2")] godot_params: sys::GDExtensionClassCreationInfo, + #[cfg(since_api = "4.2")] + godot_params: sys::GDExtensionClassCreationInfo2, init_level: InitLevel, is_editor_plugin: bool, } @@ -143,6 +177,7 @@ pub fn register_class< out!("Manually register class {}", std::any::type_name::()); + #[cfg(before_api = "4.2")] let godot_params = sys::GDExtensionClassCreationInfo { to_string_func: Some(callbacks::to_string::), notification_func: Some(callbacks::on_notification::), @@ -155,6 +190,20 @@ pub fn register_class< class_userdata: ptr::null_mut(), // will be passed to create fn, but global per class ..default_creation_info() }; + #[cfg(since_api = "4.2")] + let godot_params = sys::GDExtensionClassCreationInfo2 { + to_string_func: Some(callbacks::to_string::), + notification_func: Some(callbacks::on_notification::), + reference_func: Some(callbacks::reference::), + unreference_func: Some(callbacks::unreference::), + create_instance_func: Some(callbacks::create::), + recreate_instance_func: Some(callbacks::recreate::), + free_instance_func: Some(callbacks::free::), + get_virtual_func: Some(callbacks::get_virtual::), + get_rid_func: None, + class_userdata: ptr::null_mut(), // will be passed to create fn, but global per class + ..default_creation_info() + }; register_class_raw(ClassRegistrationInfo { class_name: T::class_name(), @@ -200,19 +249,52 @@ pub fn auto_register_classes(init_level: InitLevel) { fill_class_info(elem.component.clone(), class_info); }); - //out!("Class-map: {map:#?}"); + let mut loaded_classes_guard = get_loaded_classes_with_mutex(); + let loaded_classes_by_level = loaded_classes_guard.get_or_insert_with(HashMap::default); for info in map.into_values() { out!( "Register class: {} at level `{init_level:?}`", info.class_name ); + let class_name = info.class_name; + loaded_classes_by_level + .entry(init_level) + .or_default() + .push(info.class_name); register_class_raw(info); + + godot_print!("Class {} loaded", class_name); } out!("All classes for level `{init_level:?}` auto-registered."); } +pub fn unregister_classes(init_level: InitLevel) { + let mut loaded_classes_guard = get_loaded_classes_with_mutex(); + let loaded_classes_by_level = loaded_classes_guard.get_or_insert_with(HashMap::default); + let loaded_classes_current_level = loaded_classes_by_level + .remove(&init_level) + .unwrap_or_default(); + out!("Unregistering classes of level {init_level:?}..."); + for class_name in loaded_classes_current_level.iter().rev() { + unregister_class_raw(class_name); + } +} + +fn get_loaded_classes_with_mutex() -> MutexGuard<'static, Option>>> +{ + match LOADED_CLASSES.try_lock() { + Ok(it) => it, + Err(err) => match err { + TryLockError::Poisoned(_err) => panic!( + "LOADED_CLASSES poisoned. seems like class registration or deregistration panicked." + ), + TryLockError::WouldBlock => panic!("unexpected concurrent access detected to CLASSES"), + }, + } +} + /// Populate `c` with all the relevant data from `component` (depending on component type). fn fill_class_info(component: PluginComponent, c: &mut ClassRegistrationInfo) { // out!("| reg (before): {c:?}"); @@ -221,9 +303,11 @@ fn fill_class_info(component: PluginComponent, c: &mut ClassRegistrationInfo) { PluginComponent::ClassDef { base_class_name, generated_create_fn, + generated_recreate_fn, free_fn, } => { c.parent_class_name = Some(base_class_name); + fill_into( &mut c.godot_params.create_instance_func, generated_create_fn, @@ -234,6 +318,22 @@ fn fill_class_info(component: PluginComponent, c: &mut ClassRegistrationInfo) { c.class_name, ) ); + + #[cfg(since_api = "4.2")] + fill_into( + &mut c.godot_params.recreate_instance_func, + generated_recreate_fn, + ) + .unwrap_or_else(|_| + panic!( + "Godot class `{}` is defined multiple times in Rust; you can rename them with #[class(rename=NewName)]", + c.class_name, + ) + ); + + #[cfg(before_api = "4.2")] + assert!(generated_recreate_fn.is_none()); // not used + c.godot_params.free_instance_func = Some(free_fn); } @@ -246,14 +346,24 @@ fn fill_class_info(component: PluginComponent, c: &mut ClassRegistrationInfo) { PluginComponent::UserVirtuals { user_register_fn, user_create_fn, + user_recreate_fn, user_to_string_fn, user_on_notification_fn, get_virtual_fn, } => { c.user_register_fn = user_register_fn; - // this shouldn't panic since rustc will error if there's + + // following unwraps of fill_into shouldn't panic since rustc will error if there's // multiple `impl {Class}Virtual for Thing` definitions + fill_into(&mut c.godot_params.create_instance_func, user_create_fn).unwrap(); + + #[cfg(since_api = "4.2")] + fill_into(&mut c.godot_params.recreate_instance_func, user_recreate_fn).unwrap(); + + #[cfg(before_api = "4.2")] + assert!(user_recreate_fn.is_none()); // not used + 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); @@ -288,7 +398,10 @@ fn register_class_raw(info: ClassRegistrationInfo) { unsafe { // Try to register class... - #[allow(clippy::let_unit_value)] // notifies us if Godot API ever adds a return type. + + #[cfg(before_api = "4.2")] + #[allow(clippy::let_unit_value)] + // notifies us if Godot API ever adds a return type. let _: () = interface_fn!(classdb_register_extension_class)( sys::get_library(), class_name.string_sys(), @@ -296,6 +409,16 @@ fn register_class_raw(info: ClassRegistrationInfo) { ptr::addr_of!(info.godot_params), ); + #[cfg(since_api = "4.2")] + #[allow(clippy::let_unit_value)] + // notifies us if Godot API ever adds a return type. + let _: () = interface_fn!(classdb_register_extension_class2)( + sys::get_library(), + class_name.string_sys(), + parent_class_name.string_sys(), + ptr::addr_of!(info.godot_params), + ); + // ...then see if it worked. // This is necessary because the above registration does not report errors (apart from console output). let tag = interface_fn!(classdb_get_class_tag)(class_name.string_sys()); @@ -327,6 +450,18 @@ fn register_class_raw(info: ClassRegistrationInfo) { assert!(!info.is_editor_plugin); } +fn unregister_class_raw(class_name: &ClassName) { + out!("Unregister class: {class_name}"); + unsafe { + #[allow(clippy::let_unit_value)] + let _: () = interface_fn!(classdb_unregister_extension_class)( + sys::get_library(), + class_name.string_sys(), + ); + } + godot_print!("Class {class_name} unloaded"); +} + /// Callbacks that are passed as function pointers to Godot upon class registration. /// /// Re-exported to `crate::private` @@ -342,19 +477,47 @@ pub mod callbacks { create_custom(T::__godot_init) } + #[cfg(since_api = "4.2")] + pub unsafe extern "C" fn recreate( + _class_userdata: *mut std::ffi::c_void, + object: sys::GDExtensionObjectPtr, + ) -> sys::GDExtensionClassInstancePtr { + create_rust_part_for_existing_godot_part(T::__godot_init, object) + } + pub(crate) fn create_custom(make_user_instance: F) -> sys::GDExtensionObjectPtr where T: GodotClass, F: FnOnce(Base) -> T, { - let class_name = T::class_name(); let base_class_name = T::Base::class_name(); - //out!("create callback: {}", class_name.backing); - let base_ptr = unsafe { interface_fn!(classdb_construct_object)(base_class_name.string_sys()) }; + create_rust_part_for_existing_godot_part(make_user_instance, base_ptr); + + // std::mem::forget(base_class_name); + base_ptr + } + + // with GDExt, custom object consists from two parts: Godot object and Rust object, that are + // bound to each other. this method takes the first by pointer, creates the second with + // supplied state and binds them together. that's used for both brand-new objects creation and + // hot reload - during hot-reload, Rust objects are disposed and then created again with a + // updated code, so that's necessary to link them to Godot objects again. + fn create_rust_part_for_existing_godot_part( + make_user_instance: F, + base_ptr: sys::GDExtensionObjectPtr, + ) -> sys::GDExtensionClassInstancePtr + where + T: GodotClass, + F: FnOnce(Base) -> T, + { + let class_name = T::class_name(); + + //out!("create callback: {}", class_name.backing); + let base = unsafe { Base::from_sys(base_ptr) }; let user_instance = make_user_instance(unsafe { Base::from_base(&base) }); @@ -374,8 +537,7 @@ pub mod callbacks { } // std::mem::forget(class_name); - // std::mem::forget(base_class_name); - base_ptr + instance_ptr } pub unsafe extern "C" fn free( @@ -418,6 +580,7 @@ pub mod callbacks { string.move_string_ptr(out_string); } + #[cfg(before_api = "4.2")] pub unsafe extern "C" fn on_notification( instance: sys::GDExtensionClassInstancePtr, what: i32, @@ -428,6 +591,18 @@ pub mod callbacks { T::__godot_notification(&mut *instance, what); } + #[cfg(since_api = "4.2")] + pub unsafe extern "C" fn on_notification( + instance: sys::GDExtensionClassInstancePtr, + what: i32, + _reversed: sys::GDExtensionBool, + ) { + let storage = as_storage::(instance); + let mut instance = storage.get_mut(); + + T::__godot_notification(&mut *instance, what); + } + pub unsafe extern "C" fn reference(instance: sys::GDExtensionClassInstancePtr) { let storage = as_storage::(instance); storage.on_inc_ref(); @@ -495,6 +670,7 @@ fn default_registration_info(class_name: ClassName) -> ClassRegistrationInfo { } } +#[cfg(before_api = "4.2")] fn default_creation_info() -> sys::GDExtensionClassCreationInfo { sys::GDExtensionClassCreationInfo { is_abstract: false as u8, @@ -516,3 +692,31 @@ fn default_creation_info() -> sys::GDExtensionClassCreationInfo { class_userdata: ptr::null_mut(), } } + +#[cfg(since_api = "4.2")] +fn default_creation_info() -> sys::GDExtensionClassCreationInfo2 { + sys::GDExtensionClassCreationInfo2 { + is_abstract: false as u8, + is_virtual: false as u8, + set_func: None, + get_func: None, + get_property_list_func: None, + free_property_list_func: None, + property_can_revert_func: None, + property_get_revert_func: None, + validate_property_func: None, + notification_func: None, + to_string_func: None, + reference_func: None, + unreference_func: None, + create_instance_func: None, + free_instance_func: None, + recreate_instance_func: None, + get_virtual_func: None, + get_virtual_call_data_func: None, + call_virtual_with_data_func: None, + get_rid_func: None, + class_userdata: ptr::null_mut(), + is_exposed: true as u8, + } +} diff --git a/godot-macros/Cargo.toml b/godot-macros/Cargo.toml index d9172e7cb..535778997 100644 --- a/godot-macros/Cargo.toml +++ b/godot-macros/Cargo.toml @@ -7,6 +7,9 @@ license = "MPL-2.0" keywords = ["gamedev", "godot", "engine", "derive", "macro"] categories = ["game-engines", "graphics"] +[features] +custom-godot = ["godot-bindings/custom-godot"] + [lib] proc-macro = true @@ -20,3 +23,6 @@ proc-macro2 = "1.0.63" quote = "1.0.29" venial = "0.5" + +[build-dependencies] +godot-bindings = { path = "../godot-bindings" } # emit_godot_version_cfg diff --git a/godot-macros/build.rs b/godot-macros/build.rs new file mode 100644 index 000000000..93870a368 --- /dev/null +++ b/godot-macros/build.rs @@ -0,0 +1,9 @@ +/* + * 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/. + */ + +fn main() { + godot_bindings::emit_godot_version_cfg(); +} diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index e8997d367..eba169f44 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -50,13 +50,19 @@ pub fn derive_godot_class(decl: Declaration) -> ParseResult { quote! {} }; - let (godot_init_impl, create_fn); + let (godot_init_impl, create_fn, recreate_fn); if struct_cfg.has_generated_init { godot_init_impl = make_godot_init_impl(class_name, fields); create_fn = quote! { Some(#prv::callbacks::create::<#class_name>) }; + if cfg!(since_api = "4.2") { + recreate_fn = quote! { Some(#prv::callbacks::recreate::<#class_name>) }; + } else { + recreate_fn = quote! { None }; + } } else { godot_init_impl = TokenStream::new(); create_fn = quote! { None }; + recreate_fn = quote! { None }; }; let config_impl = make_config_impl(class_name, struct_cfg.is_tool); @@ -82,6 +88,7 @@ pub fn derive_godot_class(decl: Declaration) -> ParseResult { component: #prv::PluginComponent::ClassDef { base_class_name: #base_class_name_obj, generated_create_fn: #create_fn, + generated_recreate_fn: #recreate_fn, free_fn: #prv::callbacks::free::<#class_name>, }, init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL, diff --git a/godot-macros/src/class/godot_api.rs b/godot-macros/src/class/godot_api.rs index 223d43995..2aabdfa7a 100644 --- a/godot-macros/src/class/godot_api.rs +++ b/godot-macros/src/class/godot_api.rs @@ -393,6 +393,7 @@ fn transform_trait_impl(original_impl: Impl) -> Result { let mut register_fn = quote! { None }; let mut create_fn = quote! { None }; + let mut recreate_fn = quote! { None }; let mut to_string_fn = quote! { None }; let mut on_notification_fn = quote! { None }; @@ -435,6 +436,9 @@ fn transform_trait_impl(original_impl: Impl) -> Result { } }; create_fn = quote! { Some(#prv::callbacks::create::<#class_name>) }; + if cfg!(since_api = "4.2") { + recreate_fn = quote! { Some(#prv::callbacks::recreate::<#class_name>) }; + } } "to_string" => { @@ -525,6 +529,7 @@ fn transform_trait_impl(original_impl: Impl) -> Result { component: #prv::PluginComponent::UserVirtuals { user_register_fn: #register_fn, user_create_fn: #create_fn, + user_recreate_fn: #recreate_fn, user_to_string_fn: #to_string_fn, user_on_notification_fn: #on_notification_fn, get_virtual_fn: #prv::callbacks::get_virtual::<#class_name>,