diff --git a/godot-core/src/classes/class_runtime.rs b/godot-core/src/classes/class_runtime.rs index ba487b03d..0f77323f5 100644 --- a/godot-core/src/classes/class_runtime.rs +++ b/godot-core/src/classes/class_runtime.rs @@ -102,6 +102,30 @@ pub(crate) fn ensure_object_inherits(derived: ClassName, base: ClassName, instan ) } +#[cfg(debug_assertions)] +pub(crate) fn ensure_binding_not_null(binding: sys::GDExtensionClassInstancePtr) +where + T: GodotClass + Bounds, +{ + if !binding.is_null() { + return; + } + + // Non-tool classes can't be instantiated in the editor. + if crate::classes::Engine::singleton().is_editor_hint() { + panic!( + "Class {} -- null instance; does the class have a Godot creator function? \ + Ensure that the given class is a tool class with #[class(tool)], if it is being accessed in the editor.", + std::any::type_name::() + ) + } else { + panic!( + "Class {} -- null instance; does the class have a Godot creator function?", + std::any::type_name::() + ); + } +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Implementation of this file diff --git a/godot-core/src/obj/raw_gd.rs b/godot-core/src/obj/raw_gd.rs index 6befef0c7..8a7e8930b 100644 --- a/godot-core/src/obj/raw_gd.rs +++ b/godot-core/src/obj/raw_gd.rs @@ -425,8 +425,19 @@ where } } - // TODO: document unsafety in this function, and double check that it actually needs to be unsafe. - unsafe fn resolve_instance_ptr(&self) -> sys::GDExtensionClassInstancePtr { + /// Retrieves and caches pointer to this class instance if `self.obj` is non-null. + /// Returns a null pointer otherwise. + /// + /// Note: The returned pointer to the GDExtensionClass instance (even when `self.obj` is non-null) + /// might still be null when: + /// - The class isn't instantiable in the current context. + /// - The instance is a placeholder (e.g., non-`tool` classes in the editor). + /// + /// However, null pointers might also occur in other, undocumented contexts. + /// + /// # Panics + /// In Debug mode, if binding is null. + fn resolve_instance_ptr(&self) -> sys::GDExtensionClassInstancePtr { if self.is_null() { return ptr::null_mut(); } @@ -437,16 +448,21 @@ where } let callbacks = crate::storage::nop_instance_callbacks(); - let token = sys::get_library() as *mut std::ffi::c_void; - let binding = interface_fn!(object_get_instance_binding)(self.obj_sys(), token, &callbacks); - debug_assert!( - !binding.is_null(), - "Class {} -- null instance; does the class have a Godot creator function?", - std::any::type_name::() - ); + // SAFETY: library is already initialized. + let token = unsafe { sys::get_library() }; + let token = token.cast::(); + + // SAFETY: ensured that `self.obj` is non-null and valid. + let binding = unsafe { + interface_fn!(object_get_instance_binding)(self.obj_sys(), token, &callbacks) + }; + + let ptr: sys::GDExtensionClassInstancePtr = binding.cast(); + + #[cfg(debug_assertions)] + crate::classes::ensure_binding_not_null::(ptr); - let ptr = binding as sys::GDExtensionClassInstancePtr; self.cached_storage_ptr.set(ptr); ptr } diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index 2ac093d52..3f23d122b 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -325,7 +325,9 @@ use crate::util::{bail, ident, KvParser}; /// See [`ExtensionLibrary::editor_run_behavior()`](../init/trait.ExtensionLibrary.html#method.editor_run_behavior) /// for more information and further customization. /// -/// This is very similar to [GDScript's `@tool` feature](https://docs.godotengine.org/en/stable/tutorials/plugins/running_code_in_the_editor.html). +/// This behaves similarly to [GDScript's `@tool` feature](https://docs.godotengine.org/en/stable/tutorials/plugins/running_code_in_the_editor.html). +/// +/// **Note**: As in GDScript, the class must be marked as a `tool` to be accessible in the editor (e.g., for use by editor plugins and inspectors). /// /// ## Editor plugins ///