diff --git a/godot-core/src/obj/script.rs b/godot-core/src/obj/script.rs index 43c87b335..cc3a6bd75 100644 --- a/godot-core/src/obj/script.rs +++ b/godot-core/src/obj/script.rs @@ -30,6 +30,7 @@ use crate::meta::{MethodInfo, PropertyInfo}; use crate::obj::{Base, Gd, GodotClass}; use crate::sys; +#[cfg(before_api = "4.3")] use self::bounded_ptr_list::BoundedPtrList; /// Implement custom scripts that can be attached to objects in Godot. @@ -153,17 +154,28 @@ pub trait ScriptInstance: Sized { /// The engine may call this function if /// [`IScriptExtension::is_placeholder_fallback_enabled`](crate::classes::IScriptExtension::is_placeholder_fallback_enabled) is enabled. fn property_set_fallback(this: SiMut, name: StringName, value: &Variant) -> bool; + + /// This function will be called to handle calls to [`Object::get_method_argument_count`](crate::classes::Object::get_method_argument_count) + /// and `Callable::get_argument_count`. + /// + /// If `None` is returned the public methods will return `0`. + #[cfg(since_api = "4.3")] + fn get_method_argument_count(&self, _method: StringName) -> Option; } #[cfg(before_api = "4.2")] type ScriptInstanceInfo = sys::GDExtensionScriptInstanceInfo; -#[cfg(since_api = "4.2")] +#[cfg(all(since_api = "4.2", before_api = "4.3"))] type ScriptInstanceInfo = sys::GDExtensionScriptInstanceInfo2; +#[cfg(since_api = "4.3")] +type ScriptInstanceInfo = sys::GDExtensionScriptInstanceInfo3; struct ScriptInstanceData { inner: GdCell, script_instance_ptr: *mut ScriptInstanceInfo, + #[cfg(before_api = "4.3")] property_lists: BoundedPtrList, + #[cfg(before_api = "4.3")] method_lists: BoundedPtrList, base: Base, } @@ -236,7 +248,10 @@ pub unsafe fn create_script_instance( set_func: Some(script_instance_info::set_property_func::), get_func: Some(script_instance_info::get_property_func::), get_property_list_func: Some(script_instance_info::get_property_list_func::), + #[cfg(before_api = "4.3")] free_property_list_func: Some(script_instance_info::free_property_list_func::), + #[cfg(since_api = "4.3")] + free_property_list_func: Some(script_instance_info::free_property_list_func), #[cfg(since_api = "4.2")] get_class_category_func: None, // not yet implemented. @@ -249,7 +264,10 @@ pub unsafe fn create_script_instance( get_property_state_func: Some(script_instance_info::get_property_state_func::), get_method_list_func: Some(script_instance_info::get_method_list_func::), + #[cfg(before_api = "4.3")] free_method_list_func: Some(script_instance_info::free_method_list_func::), + #[cfg(since_api = "4.3")] + free_method_list_func: Some(script_instance_info::free_method_list_func), get_property_type_func: Some(script_instance_info::get_property_type_func::), #[cfg(since_api = "4.2")] validate_property_func: None, // not yet implemented. @@ -274,6 +292,11 @@ pub unsafe fn create_script_instance( get_language_func: Some(script_instance_info::get_language_func::), free_func: Some(script_instance_info::free_func::), + + #[cfg(since_api = "4.3")] + get_method_argument_count_func: Some( + script_instance_info::get_method_argument_count_func::, + ), }; let instance_ptr = Box::into_raw(Box::new(gd_instance)); @@ -281,7 +304,9 @@ pub unsafe fn create_script_instance( let data = ScriptInstanceData { inner: GdCell::new(rust_instance), script_instance_ptr: instance_ptr, + #[cfg(before_api = "4.3")] property_lists: BoundedPtrList::new(), + #[cfg(before_api = "4.3")] method_lists: BoundedPtrList::new(), // SAFETY: The script instance is always freed before the base object is destroyed. The weak reference should therefore never be // accessed after it has been freed. @@ -298,9 +323,12 @@ pub unsafe fn create_script_instance( #[cfg(before_api = "4.2")] let create_fn = sys::interface_fn!(script_instance_create); - #[cfg(since_api = "4.2")] + #[cfg(all(since_api = "4.2", before_api = "4.3"))] let create_fn = sys::interface_fn!(script_instance_create2); + #[cfg(since_api = "4.3")] + let create_fn = sys::interface_fn!(script_instance_create3); + create_fn( instance_ptr, data_ptr as sys::GDExtensionScriptInstanceDataPtr, @@ -458,6 +486,7 @@ impl<'a, T: ScriptInstance> DerefMut for SiMut<'a, T> { } // Encapsulate BoundedPtrList to help ensure safety. +#[cfg(before_api = "4.3")] mod bounded_ptr_list { use std::collections::HashMap; use std::sync::Mutex; @@ -537,6 +566,8 @@ mod script_instance_info { use super::{ScriptInstance, ScriptInstanceData, SiMut}; use crate::meta::{MethodInfo, PropertyInfo}; use sys::conv::{bool_to_sys, SYS_FALSE, SYS_TRUE}; + #[cfg(since_api = "4.3")] + use sys::conv::{ptr_list_from_sys, ptr_list_into_sys}; /// # Safety /// @@ -628,10 +659,14 @@ mod script_instance_info { }) .unwrap_or_default(); + #[cfg(before_api = "4.3")] let (list_ptr, list_length) = borrow_instance() .property_lists .list_into_sys(property_list); + #[cfg(since_api = "4.3")] + let (list_ptr, list_length) = ptr_list_into_sys(property_list); + // SAFETY: It is safe to assign a `u32` to `r_count`. unsafe { *r_count = list_length; @@ -666,8 +701,11 @@ mod script_instance_info { }) .unwrap_or_default(); + #[cfg(before_api = "4.3")] let (return_pointer, list_length) = borrow_instance().method_lists.list_into_sys(method_list); + #[cfg(since_api = "4.3")] + let (return_pointer, list_length) = ptr_list_into_sys(method_list); unsafe { *r_count = list_length; @@ -676,11 +714,12 @@ mod script_instance_info { return_pointer } + /// Provides the same functionality as the function below, but for Godot 4.2 and lower. + /// /// # Safety /// - /// - `p_instance` must point to a live immutable [`ScriptInstanceData`] for the duration of this function call - /// - `p_prop_info` must have been returned from a call to [`get_property_list_func`] called with the same `p_instance` pointer. - /// - `p_prop_info` must not have been mutated since the call to `get_property_list_func`. + /// See latest version below. + #[cfg(before_api = "4.3")] pub(super) unsafe extern "C" fn free_property_list_func( p_instance: sys::GDExtensionScriptInstanceDataPtr, p_prop_info: *const sys::GDExtensionPropertyInfo, @@ -699,6 +738,27 @@ mod script_instance_info { } } + /// # Safety + /// - `p_instance` must point to a live immutable [`ScriptInstanceData`] for the duration of this function call + /// - `p_prop_info` must have been returned from a call to [`get_property_list_func`] called with the same `p_instance` pointer. + /// - `p_prop_info` must not have been mutated since the call to `get_property_list_func`. + #[cfg(since_api = "4.3")] + pub(super) unsafe extern "C" fn free_property_list_func( + _p_instance: sys::GDExtensionScriptInstanceDataPtr, + p_prop_info: *const sys::GDExtensionPropertyInfo, + p_len: u32, + ) { + // SAFETY: `p_prop_info` was returned from a call to `list_into_sys`, and has not been mutated since. This is also the first call + // to `list_from_sys` with this pointer. + let property_infos = unsafe { ptr_list_from_sys(p_prop_info, p_len) }; + + for info in property_infos.iter() { + // SAFETY: `info` was returned from a call to `into_owned_property_sys` and this is the first and only time this function is called + // on it. + unsafe { PropertyInfo::free_owned_property_sys(*info) }; + } + } + /// # Safety /// /// - `p_self` must point to a live immutable [`ScriptInstanceData`] for the duration of this function call @@ -822,11 +882,12 @@ mod script_instance_info { bool_to_sys(has_method) } + /// Provides the same functionality as the function below, but for Godot 4.2 and lower. + /// /// # Safety /// - /// - `p_instance` must point to a live immutable [`ScriptInstanceData`] for the duration of this function call - /// - `p_method_info` must have been returned from a call to [`get_method_list_func`] called with the same `p_instance` pointer. - /// - `p_method_info` must not have been mutated since the call to `get_method_list_func`. + /// See latest version below. + #[cfg(before_api = "4.3")] pub(super) unsafe extern "C" fn free_method_list_func( p_instance: sys::GDExtensionScriptInstanceDataPtr, p_method_info: *const sys::GDExtensionMethodInfo, @@ -845,6 +906,28 @@ mod script_instance_info { } } + /// # Safety + /// + /// - `p_instance` must point to a live immutable [`ScriptInstanceData`] for the duration of this function call + /// - `p_method_info` must have been returned from a call to [`get_method_list_func`] called with the same `p_instance` pointer. + /// - `p_method_info` must not have been mutated since the call to `get_method_list_func`. + #[cfg(since_api = "4.3")] + pub(super) unsafe extern "C" fn free_method_list_func( + _p_instance: sys::GDExtensionScriptInstanceDataPtr, + p_method_info: *const sys::GDExtensionMethodInfo, + p_len: u32, + ) { + // SAFETY: `p_method_info` was returned from a call to `list_into_sys`, and has not been mutated since. This is also the first call + // to `list_from_sys` with this pointer. + let method_infos = unsafe { ptr_list_from_sys(p_method_info, p_len) }; + + for info in method_infos.iter() { + // SAFETY: `info` was returned from a call to `into_owned_method_sys`, and this is the first and only time we call this method on + // it. + unsafe { MethodInfo::free_owned_method_sys(*info) }; + } + } + /// # Safety /// /// - `p_instance` must point to a live immutable [`ScriptInstanceData`] for the duration of this function call @@ -1108,4 +1191,46 @@ mod script_instance_info { bool_to_sys(result) } + + /// # Safety + /// + /// - `p_instance` must point to a live immutable [`ScriptInstanceData`] for the duration of this function call + /// - `p_method` has to point to a valid [`StringName`]. + /// - `p_value` must be a valid [`sys::GDExtensionBool`] pointer. + #[cfg(since_api = "4.3")] + pub(super) unsafe extern "C" fn get_method_argument_count_func( + p_instance: sys::GDExtensionScriptInstanceDataPtr, + p_method: sys::GDExtensionConstStringNamePtr, + r_is_valid: *mut sys::GDExtensionBool, + ) -> sys::GDExtensionInt { + // SAFETY: `p_method` is a valid [`StringName`] pointer. + let method = unsafe { StringName::new_from_string_sys(p_method) }; + let ctx = || { + format!( + "error when calling {}::get_method_argument_count_func", + type_name::() + ) + }; + + let method_argument_count = handle_panic(ctx, || { + // SAFETY: `p_instance` points to a live immutable `ScriptInstanceData` for the duration of this call. + unsafe { ScriptInstanceData::::borrow_script_sys(p_instance) } + // Can panic if the GdCell is currently mutably bound. + .borrow() + // This is user code and could cause a panic. + .get_method_argument_count(method) + }) + // In case of a panic, handle_panic will print an error message. We will recover from the panic by falling back to the default value None. + .unwrap_or_default(); + + let (result, is_valid) = match method_argument_count { + Some(count) => (count, SYS_TRUE), + None => (0, SYS_FALSE), + }; + + // SAFETY: `r_is_valid` is assignable. + unsafe { *r_is_valid = is_valid }; + + result.into() + } } diff --git a/godot-ffi/src/conv.rs b/godot-ffi/src/conv.rs index dd0e957a8..882a7d34e 100644 --- a/godot-ffi/src/conv.rs +++ b/godot-ffi/src/conv.rs @@ -30,6 +30,39 @@ pub const fn bool_to_sys(value: bool) -> sys::GDExtensionBool { value as sys::GDExtensionBool } +/// Convert a list into a pointer + length pair. Should be used together with [`ptr_list_from_sys`]. +/// +/// If `list_from_sys` is not called on this list then that will cause a memory leak. +#[cfg(since_api = "4.3")] +pub fn ptr_list_into_sys(list: Vec) -> (*const T, u32) { + let len: u32 = list + .len() + .try_into() + .expect("list must have length that fits in u32"); + let ptr = Box::leak(list.into_boxed_slice()).as_ptr(); + + (ptr, len) +} + +/// Get a list back from a previous call to [`ptr_list_into_sys`]. +/// +/// # Safety +/// - `ptr` must have been returned from a call to `list_into_sys`. +/// - `ptr` must be passed to this function exactly once and not used in any other context. +#[cfg(since_api = "4.3")] +#[deny(unsafe_op_in_unsafe_fn)] +pub unsafe fn ptr_list_from_sys(ptr: *const T, len: u32) -> Box<[T]> { + let ptr: *mut T = ptr.cast_mut(); + let len: usize = sys::conv::u32_to_usize(len); + + // SAFETY: `ptr` was created in `list_into_sys` from a slice of length `len`. + // And has not been mutated since. It was mutable in the first place, but GDExtension API requires const ptr. + let slice = unsafe { std::slice::from_raw_parts_mut(ptr, len) }; + + // SAFETY: This is the first call to this function, and the list will not be accessed again after this function call. + unsafe { Box::from_raw(slice) } +} + pub const SYS_TRUE: sys::GDExtensionBool = bool_to_sys(true); pub const SYS_FALSE: sys::GDExtensionBool = bool_to_sys(false); diff --git a/itest/rust/src/builtin_tests/script/script_instance_tests.rs b/itest/rust/src/builtin_tests/script/script_instance_tests.rs index cbb5f35a5..5e141ce41 100644 --- a/itest/rust/src/builtin_tests/script/script_instance_tests.rs +++ b/itest/rust/src/builtin_tests/script/script_instance_tests.rs @@ -213,4 +213,9 @@ impl ScriptInstance for TestScriptInstance { fn property_set_fallback(_this: SiMut, _name: StringName, _value: &Variant) -> bool { false } + + #[cfg(since_api = "4.3")] + fn get_method_argument_count(&self, _method: StringName) -> Option { + None + } }