diff --git a/godot-bindings/src/godot_exe.rs b/godot-bindings/src/godot_exe.rs index b96021b69..6f0ef43bc 100644 --- a/godot-bindings/src/godot_exe.rs +++ b/godot-bindings/src/godot_exe.rs @@ -112,7 +112,7 @@ pub(crate) fn read_godot_version(godot_bin: &Path) -> GodotVersion { let output = execute(cmd, "read Godot version"); let stdout = std::str::from_utf8(&output.stdout).expect("convert Godot version to UTF-8"); - match parse_godot_version(&stdout) { + match parse_godot_version(stdout) { Ok(parsed) => { assert_eq!( parsed.major, diff --git a/godot-codegen/src/special_cases/codegen_special_cases.rs b/godot-codegen/src/special_cases/codegen_special_cases.rs index 414f9e546..d30e780e6 100644 --- a/godot-codegen/src/special_cases/codegen_special_cases.rs +++ b/godot-codegen/src/special_cases/codegen_special_cases.rs @@ -138,6 +138,7 @@ const SELECTED_CLASSES: &[&str] = &[ "EditorPlugin", "Engine", "FileAccess", + "GDScript", "HTTPRequest", "Image", "ImageTextureLayered", diff --git a/godot-core/src/builtin/meta/registration/method.rs b/godot-core/src/builtin/meta/registration/method.rs index 36547c186..b728b84bf 100644 --- a/godot-core/src/builtin/meta/registration/method.rs +++ b/godot-core/src/builtin/meta/registration/method.rs @@ -136,8 +136,16 @@ impl ClassMethodInfo { default_argument_count: self.default_argument_count(), default_arguments: default_arguments_sys.as_mut_ptr(), }; - // SAFETY: - // The lifetime of the data we use here is at least as long as this function's scope. So we can + + if self.method_flags.is_set(MethodFlags::VIRTUAL) { + self.register_virtual_class_method(method_info_sys, return_value_sys); + } else { + self.register_nonvirtual_class_method(method_info_sys); + } + } + + fn register_nonvirtual_class_method(&self, method_info_sys: sys::GDExtensionClassMethodInfo) { + // SAFETY: The lifetime of the data we use here is at least as long as this function's scope. So we can // safely call this function without issue. // // Null pointers will only be passed along if we indicate to Godot that they are unused. @@ -150,6 +158,42 @@ impl ClassMethodInfo { } } + #[cfg(since_api = "4.3")] + fn register_virtual_class_method( + &self, + normal_method_info: sys::GDExtensionClassMethodInfo, + return_value_sys: sys::GDExtensionPropertyInfo, // passed separately because value, not pointer. + ) { + // Copy everything possible from regular method info. + let method_info_sys = sys::GDExtensionClassVirtualMethodInfo { + name: normal_method_info.name, + method_flags: normal_method_info.method_flags, + return_value: return_value_sys, + return_value_metadata: normal_method_info.return_value_metadata, + argument_count: normal_method_info.argument_count, + arguments: normal_method_info.arguments_info, + arguments_metadata: normal_method_info.arguments_metadata, + }; + + // SAFETY: Godot only needs arguments to be alive during the method call. + unsafe { + interface_fn!(classdb_register_extension_class_virtual_method)( + sys::get_library(), + self.class_name.string_sys(), + std::ptr::addr_of!(method_info_sys), + ) + } + } + + // Polyfill doing nothing. + #[cfg(before_api = "4.3")] + fn register_virtual_class_method( + &self, + _normal_method_info: sys::GDExtensionClassMethodInfo, + _return_value_sys: sys::GDExtensionPropertyInfo, + ) { + } + fn argument_count(&self) -> u32 { self.arguments .len() diff --git a/godot-core/src/builtin/meta/signature.rs b/godot-core/src/builtin/meta/signature.rs index 5a78c4579..b01aa5c71 100644 --- a/godot-core/src/builtin/meta/signature.rs +++ b/godot-core/src/builtin/meta/signature.rs @@ -49,6 +49,19 @@ pub trait VarcallSignatureTuple: PtrcallSignatureTuple { varargs: &[Variant], ) -> Self::Ret; + /// Outbound virtual call to a method overridden by a script attached to the object. + /// + /// Returns `None` if the script does not override the method. + #[cfg(since_api = "4.3")] + unsafe fn out_script_virtual_call( + // Separate parameters to reduce tokens in macro-generated API. + class_name: &'static str, + method_name: &'static str, + method_sname_ptr: sys::GDExtensionConstStringNamePtr, + object_ptr: sys::GDExtensionObjectPtr, + args: Self::Params, + ) -> Self::Ret; + unsafe fn out_utility_ptrcall_varargs( utility_fn: UtilityFunctionBind, function_name: &'static str, @@ -229,6 +242,45 @@ macro_rules! impl_varcall_signature_for_tuple { result.unwrap_or_else(|err| return_error::(&call_ctx, err)) } + #[cfg(since_api = "4.3")] + unsafe fn out_script_virtual_call( + // Separate parameters to reduce tokens in macro-generated API. + class_name: &'static str, + method_name: &'static str, + method_sname_ptr: sys::GDExtensionConstStringNamePtr, + object_ptr: sys::GDExtensionObjectPtr, + ($($pn,)*): Self::Params, + ) -> Self::Ret { + // Assumes that caller has previously checked existence of a virtual method. + + let call_ctx = CallContext::outbound(class_name, method_name); + //$crate::out!("out_script_virtual_call: {call_ctx}"); + + let object_call_script_method = sys::interface_fn!(object_call_script_method); + let explicit_args = [ + $( + GodotFfiVariant::ffi_to_variant(&into_ffi($pn)), + )* + ]; + + let variant_ptrs = explicit_args.iter().map(Variant::var_sys_const).collect::>(); + + let variant = Variant::from_var_sys_init(|return_ptr| { + let mut err = sys::default_call_error(); + object_call_script_method( + object_ptr, + method_sname_ptr, + variant_ptrs.as_ptr(), + variant_ptrs.len() as i64, + return_ptr, + std::ptr::addr_of_mut!(err), + ); + }); + + let result = ::try_from_variant(&variant); + result.unwrap_or_else(|err| return_error::(&call_ctx, err)) + } + // Note: this is doing a ptrcall, but uses variant conversions for it #[inline] unsafe fn out_utility_ptrcall_varargs( @@ -257,6 +309,7 @@ macro_rules! impl_varcall_signature_for_tuple { result.unwrap_or_else(|err| return_error::(&call_ctx, err)) } + #[inline] fn format_args(args: &Self::Params) -> String { let mut string = String::new(); diff --git a/godot-core/src/builtin/string/string_name.rs b/godot-core/src/builtin/string/string_name.rs index 79de54b8b..4ce8508b4 100644 --- a/godot-core/src/builtin/string/string_name.rs +++ b/godot-core/src/builtin/string/string_name.rs @@ -120,6 +120,11 @@ impl StringName { fn string_sys = sys; } + #[doc(hidden)] + pub fn string_sys_const(&self) -> sys::GDExtensionConstStringNamePtr { + sys::to_const_ptr(self.string_sys()) + } + #[doc(hidden)] pub fn as_inner(&self) -> inner::InnerStringName { inner::InnerStringName::from_outer(self) diff --git a/godot-core/src/obj/base.rs b/godot-core/src/obj/base.rs index a08a5ddea..ada541559 100644 --- a/godot-core/src/obj/base.rs +++ b/godot-core/src/obj/base.rs @@ -79,6 +79,12 @@ impl Base { pub fn as_gd(&self) -> &Gd { &self.obj } + + // Currently only used in outbound virtual calls (for scripts). + #[doc(hidden)] + pub fn obj_sys(&self) -> sys::GDExtensionObjectPtr { + self.obj.obj_sys() + } } impl Debug for Base { diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 39f7f1984..aa43b1f4d 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -423,7 +423,8 @@ impl Gd { Self::from_obj_sys_weak_or_none(ptr).unwrap() } - pub(crate) fn obj_sys(&self) -> sys::GDExtensionObjectPtr { + #[doc(hidden)] + pub fn obj_sys(&self) -> sys::GDExtensionObjectPtr { self.raw.obj_sys() } diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index db7639dcc..e8fa296c0 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -183,6 +183,11 @@ pub trait EngineBitfield: Copy { Self::try_from_ord(ord) .unwrap_or_else(|| panic!("ordinal {ord} does not map to any valid bit flag")) } + + // TODO consolidate API: named methods vs. | & ! etc. + fn is_set(self, flag: Self) -> bool { + self.ord() & flag.ord() != 0 + } } /// Trait for enums that can be used as indices in arrays. diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index d0102e15c..019637a96 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -130,6 +130,14 @@ where } } +#[cfg(since_api = "4.3")] +pub unsafe fn has_virtual_script_method( + object_ptr: sys::GDExtensionObjectPtr, + method_sname: sys::GDExtensionConstStringNamePtr, +) -> bool { + sys::interface_fn!(object_has_script_method)(sys::to_const_ptr(object_ptr), method_sname) != 0 +} + pub fn flush_stdout() { use std::io::Write; std::io::stdout().flush().expect("flush stdout"); diff --git a/godot-ffi/src/extras.rs b/godot-ffi/src/extras.rs index 4b2a6ebee..78712fe0c 100644 --- a/godot-ffi/src/extras.rs +++ b/godot-ffi/src/extras.rs @@ -58,6 +58,9 @@ impl_as_uninit!(GDExtensionTypePtr, GDExtensionUninitializedTypePtr); // ---------------------------------------------------------------------------------------------------------------------------------------------- // Helper functions +/// Differentiate from `sys::GDEXTENSION_CALL_ERROR_*` codes. +pub const GODOT_RUST_CALL_ERROR: GDExtensionCallErrorType = 40; + #[doc(hidden)] #[inline] pub fn default_call_error() -> GDExtensionCallError { @@ -105,6 +108,7 @@ pub fn panic_call_error( } GDEXTENSION_CALL_ERROR_INSTANCE_IS_NULL => "instance is null".to_string(), GDEXTENSION_CALL_ERROR_METHOD_NOT_CONST => "method is not const".to_string(), // not handled in Godot + GODOT_RUST_CALL_ERROR => "godot-rust function call failed".to_string(), _ => format!("unknown reason (error code {error})"), }; diff --git a/godot-macros/src/class/data_models/field_var.rs b/godot-macros/src/class/data_models/field_var.rs index 7923514db..09c75c8d6 100644 --- a/godot-macros/src/class/data_models/field_var.rs +++ b/godot-macros/src/class/data_models/field_var.rs @@ -194,7 +194,7 @@ impl GetterSetterImpl { let export_token = make_method_registration( class_name, FuncDefinition { - func: signature, + signature, // Since we're analyzing a struct's field, we don't have access to the corresponding get/set function's // external (non-#[func]) attributes. We have to assume the function exists and has the name the user // gave us, with the expected signature. @@ -202,10 +202,13 @@ impl GetterSetterImpl { // #[cfg()] (for instance) placed on the getter/setter function, but that is not currently supported. external_attributes: Vec::new(), rename: None, + is_virtual: false, has_gd_self: false, }, ); + let export_token = export_token.expect("getter/setter generation should not fail"); + Self { function_name, function_impl, diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 0711d84f1..1aa701703 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -5,19 +5,20 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::util; -use crate::util::ident; +use crate::util::{bail_fn, ident}; +use crate::{util, ParseResult}; use proc_macro2::{Group, Ident, TokenStream, TokenTree}; use quote::{format_ident, quote}; /// Information used for registering a Rust function with Godot. pub struct FuncDefinition { /// Raw information about the Rust function. - pub func: venial::Function, + pub signature: venial::Function, /// The function's non-gdext attributes (all except #[func]). pub external_attributes: Vec, /// The name the function will be exposed as in Godot. If `None`, the Rust function name is used. pub rename: Option, + pub is_virtual: bool, pub has_gd_self: bool, } @@ -33,8 +34,7 @@ pub fn make_virtual_callback( let method_name = &signature_info.method_name; 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); + let sig_tuple = signature_info.tuple_type(); let call_ctx = make_call_context( class_name.to_string().as_str(), @@ -62,16 +62,19 @@ pub fn make_virtual_callback( pub fn make_method_registration( class_name: &Ident, func_definition: FuncDefinition, -) -> TokenStream { +) -> ParseResult { let signature_info = into_signature_info( - func_definition.func, + func_definition.signature, class_name, func_definition.has_gd_self, ); - let sig_tuple = - util::make_signature_tuple_type(&signature_info.ret_type, &signature_info.param_types); + let sig_tuple = signature_info.tuple_type(); - let method_flags = make_method_flags(signature_info.receiver_type); + let is_virtual = func_definition.is_virtual; + let method_flags = match make_method_flags(signature_info.receiver_type, is_virtual) { + Ok(mf) => mf, + Err(msg) => return bail_fn(msg, signature_info.method_name), + }; let forwarding_closure = make_forwarding_closure(class_name, &signature_info, BeforeKind::Without); @@ -101,7 +104,7 @@ pub fn make_method_registration( .into_iter() .collect::>(); - quote! { + let registration = quote! { #(#cfg_attrs)* { use ::godot::obj::GodotClass; @@ -121,15 +124,15 @@ pub fn make_method_registration( // `get_ptrcall_func` upholds all the requirements for `ptrcall_func` let method_info = unsafe { ClassMethodInfo::from_signature::( - #class_name::class_name(), - method_name, - Some(varcall_func), - Some(ptrcall_func), - #method_flags, - &[ - #( #param_ident_strs ),* - ], - Vec::new() + #class_name::class_name(), + method_name, + Some(varcall_func), + Some(ptrcall_func), + #method_flags, + &[ + #( #param_ident_strs ),* + ], + Vec::new() ) }; @@ -139,16 +142,18 @@ pub fn make_method_registration( #method_name_str ); - + // Note: information whether the method is virtual is stored in method method_info's flags. method_info.register_extension_class_method(); }; - } + }; + + Ok(registration) } // ---------------------------------------------------------------------------------------------------------------------------------------------- // Implementation -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum ReceiverType { Ref, Mut, @@ -156,6 +161,7 @@ pub enum ReceiverType { Static, } +#[derive(Debug)] pub struct SignatureInfo { pub method_name: Ident, pub receiver_type: ReceiverType, @@ -174,6 +180,11 @@ impl SignatureInfo { ret_type: quote! { () }, } } + + pub fn tuple_type(&self) -> TokenStream { + // Note: for GdSelf receivers, first parameter is not even part of SignatureInfo anymore. + util::make_signature_tuple_type(&self.ret_type, &self.param_types) + } } pub enum BeforeKind { @@ -298,6 +309,7 @@ pub(crate) fn into_signature_info( } else { ReceiverType::Static }; + let num_params = signature.params.inner.len(); let mut param_idents = Vec::with_capacity(num_params); let mut param_types = Vec::with_capacity(num_params); @@ -351,15 +363,35 @@ pub(crate) fn into_signature_info( } } -fn make_method_flags(method_type: ReceiverType) -> TokenStream { - match method_type { - ReceiverType::Ref | ReceiverType::Mut | ReceiverType::GdSelf => { - quote! { ::godot::engine::global::MethodFlags::DEFAULT } +fn make_method_flags( + method_type: ReceiverType, + is_rust_virtual: bool, +) -> Result { + let scope = quote! { ::godot::engine::global::MethodFlags }; + + let base_flags = match method_type { + ReceiverType::Ref => { + quote! { #scope::NORMAL | #scope::CONST } + } + // Conservatively assume Gd receivers to mutate the object, since user can call bind_mut(). + ReceiverType::Mut | ReceiverType::GdSelf => { + quote! { #scope::NORMAL } } ReceiverType::Static => { - quote! { ::godot::engine::global::MethodFlags::STATIC } + if is_rust_virtual { + return Err("Static methods cannot be virtual".to_string()); + } + quote! { #scope::STATIC } } - } + }; + + let flags = if is_rust_virtual { + quote! { #base_flags | #scope::VIRTUAL } + } else { + base_flags + }; + + Ok(flags) } /// Generate code for a C FFI function that performs a varcall. @@ -386,8 +418,9 @@ fn make_varcall_func( ); if success.is_none() { - // Signal error and set return type to Nil - (*err).error = sys::GDEXTENSION_CALL_ERROR_INVALID_METHOD; // no better fitting enum? + // Signal error and set return type to Nil. + // None of the sys::GDEXTENSION_CALL_ERROR enums fits; so we use our own outside Godot's official range. + (*err).error = sys::GODOT_RUST_CALL_ERROR; // TODO(uninit) sys::interface_fn!(variant_new_nil)(sys::AsUninit::as_uninit(ret)); diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index 321706b35..5745d5829 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -13,7 +13,7 @@ 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::{bail, ident, path_ends_with_complex, require_api_version, KvParser}; use crate::{util, ParseResult}; pub fn derive_godot_class(decl: Declaration) -> ParseResult { @@ -307,12 +307,7 @@ fn parse_struct_attributes(class: &Struct) -> ParseResult { // #[class(editor_plugin)] if let Some(attr_key) = parser.handle_alone_with_span("editor_plugin")? { - if cfg!(before_api = "4.1") { - return bail!( - attr_key, - "#[class(editor_plugin)] is not supported in Godot 4.0" - ); - } + require_api_version!("4.1", &attr_key, "#[class(editor_plugin)]")?; is_editor_plugin = true; @@ -331,8 +326,9 @@ fn parse_struct_attributes(class: &Struct) -> ParseResult { } } - // #[class(hide)] - if parser.handle_alone("hide")? { + // #[class(hidden)] + if let Some(span) = parser.handle_alone_with_span("hidden")? { + require_api_version!("4.2", span, "#[class(hidden)]")?; is_hidden = true; } diff --git a/godot-macros/src/class/godot_api.rs b/godot-macros/src/class/godot_api.rs index 1d01e1dc2..72504830e 100644 --- a/godot-macros/src/class/godot_api.rs +++ b/godot-macros/src/class/godot_api.rs @@ -5,24 +5,20 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use proc_macro2::{Ident, TokenStream}; -use quote::quote; +use proc_macro2::{Delimiter, Group, Ident, TokenStream}; use quote::spanned::Spanned; -use venial::{ - Attribute, AttributeValue, Constant, Declaration, Error, FnParam, Function, Impl, ImplMember, - TyExpr, -}; +use quote::{format_ident, quote}; use crate::class::{ into_signature_info, make_method_registration, make_virtual_callback, BeforeKind, FuncDefinition, SignatureInfo, }; -use crate::util::{bail, KvParser}; +use crate::util::{bail, require_api_version, KvParser}; use crate::{util, ParseResult}; -pub fn attribute_godot_api(input_decl: Declaration) -> Result { +pub fn attribute_godot_api(input_decl: venial::Declaration) -> ParseResult { let decl = match input_decl { - Declaration::Impl(decl) => decl, + venial::Declaration::Impl(decl) => decl, _ => bail!( input_decl, "#[godot_api] can only be applied on impl blocks", @@ -50,55 +46,62 @@ pub fn attribute_godot_api(input_decl: Declaration) -> Result, + is_virtual: bool, has_gd_self: bool, }, - Signal(AttributeValue), - Const(#[allow(dead_code)] AttributeValue), + Signal(venial::AttributeValue), + Const(#[allow(dead_code)] venial::AttributeValue), } -struct BoundAttr { +struct ItemAttr { attr_name: Ident, index: usize, - ty: BoundAttrType, + ty: ItemAttrType, } -impl BoundAttr { - fn bail(self, msg: &str, method: &Function) -> Result { +impl ItemAttr { + fn bail(self, msg: &str, method: &venial::Function) -> ParseResult { bail!(&method.name, "#[{}]: {}", self.attr_name, msg) } } +fn bail_attr(attr_name: Ident, msg: &str, method: &venial::Function) -> ParseResult { + bail!(&method.name, "#[{}]: {}", attr_name, msg) +} + /// Holds information known from a signal's definition struct SignalDefinition { /// The signal's function signature. - signature: Function, + signature: venial::Function, /// The signal's non-gdext attributes (all except #[signal]). - external_attributes: Vec, + external_attributes: Vec, } /// Codegen for `#[godot_api] impl MyType` -fn transform_inherent_impl(mut original_impl: Impl) -> ParseResult { +fn transform_inherent_impl(mut original_impl: venial::Impl) -> ParseResult { let class_name = util::validate_impl(&original_impl, None, "godot_api")?; let class_name_obj = util::class_name_obj(&class_name); let prv = quote! { ::godot::private }; - let (funcs, signals) = process_godot_fns(&mut original_impl)?; + let (funcs, signals, out_virtual_impl) = process_godot_fns(&class_name, &mut original_impl)?; let signal_registrations = make_signal_registrations(signals, &class_name_obj); - let method_registrations = funcs + let method_registrations: Vec = funcs .into_iter() - .map(|func_def| make_method_registration(&class_name, func_def)); + .map(|func_def| make_method_registration(&class_name, func_def)) + .collect::>>()?; // <- FIXME transpose this let constant_registration = make_constant_registration(&mut original_impl, &class_name, &class_name_obj)?; let result = quote! { #original_impl + #out_virtual_impl impl ::godot::obj::cap::ImplementsGodotApi for #class_name { fn __register_methods() { @@ -136,16 +139,16 @@ fn make_signal_registrations( signature, external_attributes, } = signal; - let mut param_types: Vec = Vec::new(); + let mut param_types: Vec = Vec::new(); let mut param_names: Vec = Vec::new(); for param in signature.params.inner.iter() { match ¶m.0 { - FnParam::Typed(param) => { + venial::FnParam::Typed(param) => { param_types.push(param.ty.clone()); param_names.push(param.name.to_string()); } - FnParam::Receiver(_) => {} + venial::FnParam::Receiver(_) => {} }; } @@ -163,9 +166,10 @@ fn make_signal_registrations( // Transport #[cfg] attributes to the FFI glue, to ensure signals which were conditionally // removed from compilation don't cause errors. - let signal_cfg_attrs: Vec<&Attribute> = util::extract_cfg_attrs(external_attributes) - .into_iter() - .collect(); + let signal_cfg_attrs: Vec<&venial::Attribute> = + util::extract_cfg_attrs(external_attributes) + .into_iter() + .collect(); let signal_name_str = signature.name.to_string(); let signal_parameters_count = param_names.len(); let signal_parameters = param_array_decl; @@ -197,7 +201,7 @@ fn make_signal_registrations( } fn make_constant_registration( - original_impl: &mut Impl, + original_impl: &mut venial::Impl, class_name: &Ident, class_name_obj: &TokenStream, ) -> ParseResult { @@ -253,63 +257,102 @@ fn make_constant_registration( } fn process_godot_fns( - impl_block: &mut Impl, -) -> Result<(Vec, Vec), Error> { + class_name: &Ident, + impl_block: &mut venial::Impl, +) -> ParseResult<(Vec, Vec, TokenStream)> { let mut func_definitions = vec![]; let mut signal_definitions = vec![]; + let mut virtual_functions = vec![]; let mut removed_indexes = vec![]; for (index, item) in impl_block.body_items.iter_mut().enumerate() { - let ImplMember::Method(method) = item else { + let venial::ImplMember::Method(function) = item else { continue; }; - let Some(attr) = extract_attributes(&method, &method.attributes)? else { + let Some(attr) = extract_attributes(&function, &function.attributes)? else { continue; }; // Remaining code no longer has attribute -- rest stays - method.attributes.remove(attr.index); - - if method.qualifiers.tk_default.is_some() - || method.qualifiers.tk_const.is_some() - || method.qualifiers.tk_async.is_some() - || method.qualifiers.tk_unsafe.is_some() - || method.qualifiers.tk_extern.is_some() - || method.qualifiers.extern_abi.is_some() + function.attributes.remove(attr.index); + + if function.qualifiers.tk_default.is_some() + || function.qualifiers.tk_const.is_some() + || function.qualifiers.tk_async.is_some() + || function.qualifiers.tk_unsafe.is_some() + || function.qualifiers.tk_extern.is_some() + || function.qualifiers.extern_abi.is_some() { - return attr.bail("fn qualifiers are not allowed", method); + return attr.bail("fn qualifiers are not allowed", function); + } + + if function.generic_params.is_some() { + return attr.bail("generic fn parameters are not supported", function); } - match &attr.ty { - BoundAttrType::Func { + match attr.ty { + ItemAttrType::Func { rename, + is_virtual, has_gd_self, } => { - let external_attributes = method.attributes.clone(); - // Signatures are the same thing without body - let mut sig = util::reduce_to_signature(method); - if *has_gd_self { - if sig.params.is_empty() { - return attr.bail("with attribute key `gd_self`, the method must have a first parameter of type Gd", method); + let external_attributes = function.attributes.clone(); + + // Signatures are the same thing without body. + let mut signature = util::reduce_to_signature(function); + let gd_self_parameter = if has_gd_self { + if signature.params.is_empty() { + return bail_attr( + attr.attr_name, + "with attribute key `gd_self`, the method must have a first parameter of type Gd", + function + ); } else { - sig.params.inner.remove(0); + let param = signature.params.inner.remove(0); + + let venial::FnParam::Typed(param) = param.0 else { + return bail_attr( + attr.attr_name, + "with attribute key `gd_self`, the first parameter must be Gd (not a `self` receiver)", + function + ); + }; + + Some(param.name) } - } + } else { + None + }; + + // For virtual methods, rename/mangle existing user method and create a new method with the original name, + // which performs a dynamic dispatch. + if is_virtual { + add_virtual_script_call( + &mut virtual_functions, + function, + &signature, + class_name, + &rename, + gd_self_parameter, + ); + }; + func_definitions.push(FuncDefinition { - func: sig, + signature, external_attributes, - rename: rename.clone(), - has_gd_self: *has_gd_self, + rename, + is_virtual, + has_gd_self, }); } - BoundAttrType::Signal(ref _attr_val) => { - if method.return_ty.is_some() { - return attr.bail("return types are not supported", method); + ItemAttrType::Signal(ref _attr_val) => { + if function.return_ty.is_some() { + return attr.bail("return types are not supported", function); } - let external_attributes = method.attributes.clone(); - let sig = util::reduce_to_signature(method); + let external_attributes = function.attributes.clone(); + let sig = util::reduce_to_signature(function); signal_definitions.push(SignalDefinition { signature: sig, @@ -318,10 +361,10 @@ fn process_godot_fns( removed_indexes.push(index); } - BoundAttrType::Const(_) => { + ItemAttrType::Const(_) => { return attr.bail( "#[constant] can only be used on associated constant", - method, + function, ) } } @@ -333,14 +376,94 @@ fn process_godot_fns( impl_block.body_items.remove(index); } - Ok((func_definitions, signal_definitions)) + let out_virtual_impl = if virtual_functions.is_empty() { + TokenStream::new() + } else { + quote! { + impl #class_name { + #(#virtual_functions)* + } + } + }; + + Ok((func_definitions, signal_definitions, out_virtual_impl)) +} + +fn add_virtual_script_call( + virtual_functions: &mut Vec, + function: &mut venial::Function, + reduced_signature: &venial::Function, + class_name: &Ident, + rename: &Option, + gd_self_parameter: Option, +) { + assert!(cfg!(since_api = "4.3")); + + let class_name_str = class_name.to_string(); + let early_bound_name = format_ident!("__earlybound_{}", &function.name); + let method_name_str = rename + .clone() + .unwrap_or_else(|| format!("_{}", function.name)); + + // Clone might not strictly be necessary, but the 2 other callers of into_signature_info() are better off with pass-by-value. + let signature_info = into_signature_info( + reduced_signature.clone(), + class_name, + gd_self_parameter.is_some(), + ); + + let sig_tuple = signature_info.tuple_type(); + let arg_names = &signature_info.param_idents; + + let (object_ptr, receiver); + if let Some(gd_self_parameter) = gd_self_parameter { + object_ptr = quote! { #gd_self_parameter.obj_sys() }; + receiver = gd_self_parameter; + } else { + object_ptr = quote! { ::base_field(self).obj_sys() }; + receiver = util::ident("self"); + }; + + let code = quote! { + let object_ptr = #object_ptr; + let method_sname = ::godot::builtin::StringName::from(#method_name_str); + let method_sname_ptr = method_sname.string_sys_const(); + let has_virtual_method = unsafe { ::godot::private::has_virtual_script_method(object_ptr, method_sname_ptr) }; + + if has_virtual_method { + // Dynamic dispatch. + type CallSig = #sig_tuple; + let args = (#( #arg_names, )*); + unsafe { + ::out_script_virtual_call( + #class_name_str, + #method_name_str, + method_sname_ptr, + object_ptr, + args, + ) + } + } else { + // Fall back to default implementation. + Self::#early_bound_name(#receiver, #(#arg_names),*) + } + }; + + let mut early_bound_function = venial::Function { + name: early_bound_name, + body: Some(Group::new(Delimiter::Brace, code)), + ..function.clone() + }; + + std::mem::swap(&mut function.body, &mut early_bound_function.body); + virtual_functions.push(early_bound_function); } -fn process_godot_constants(decl: &mut Impl) -> Result, Error> { +fn process_godot_constants(decl: &mut venial::Impl) -> ParseResult> { let mut constant_signatures = vec![]; for item in decl.body_items.iter_mut() { - let ImplMember::Constant(constant) = item else { + let venial::ImplMember::Constant(constant) = item else { continue; }; @@ -349,13 +472,13 @@ fn process_godot_constants(decl: &mut Impl) -> Result, Error> { constant.attributes.remove(attr.index); match attr.ty { - BoundAttrType::Func { .. } => { + ItemAttrType::Func { .. } => { return bail!(constant, "#[func] can only be used on functions") } - BoundAttrType::Signal(_) => { + ItemAttrType::Signal(_) => { return bail!(constant, "#[signal] can only be used on functions") } - BoundAttrType::Const(_) => { + ItemAttrType::Const(_) => { if constant.initializer.is_none() { return bail!(constant, "exported constant must have initializer"); } @@ -370,8 +493,8 @@ fn process_godot_constants(decl: &mut Impl) -> Result, Error> { fn extract_attributes( error_scope: T, - attributes: &[Attribute], -) -> Result, Error> + attributes: &[venial::Attribute], +) -> ParseResult> where for<'a> &'a T: Spanned, { @@ -383,48 +506,65 @@ where }; let new_found = match attr_name { + // #[func] name if name == "func" => { - // TODO you-win (August 8, 2023): handle default values here as well? - // Safe unwrap since #[func] must be present if we got to this point let mut parser = KvParser::parse(attributes, "func")?.unwrap(); + // #[func(rename = MyClass)] let rename = parser.handle_expr("rename")?.map(|ts| ts.to_string()); + + // #[func(virtual)] + let is_virtual = if let Some(span) = parser.handle_alone_with_span("virtual")? { + require_api_version!("4.3", span, "#[func(virtual)]")?; + true + } else { + false + }; + + // #[func(gd_self)] let has_gd_self = parser.handle_alone("gd_self")?; - BoundAttr { + parser.finish()?; + + ItemAttr { attr_name: attr_name.clone(), index, - ty: BoundAttrType::Func { + ty: ItemAttrType::Func { rename, + is_virtual, has_gd_self, }, } } + + // #[signal] name if name == "signal" => { // TODO once parameters are supported, this should probably be moved to the struct definition // E.g. a zero-sized type Signal<(i32, String)> with a provided emit(i32, String) method // This could even be made public (callable on the struct obj itself) - BoundAttr { + ItemAttr { attr_name: attr_name.clone(), index, - ty: BoundAttrType::Signal(attr.value.clone()), + ty: ItemAttrType::Signal(attr.value.clone()), } } - name if name == "constant" => BoundAttr { + + // #[constant] + name if name == "constant" => ItemAttr { attr_name: attr_name.clone(), index, - ty: BoundAttrType::Const(attr.value.clone()), + ty: ItemAttrType::Const(attr.value.clone()), }, - // Ignore unknown attributes + // Ignore unknown attributes. _ => continue, }; - // Validate at most 1 attribute + // Ensure at most 1 attribute. if found.is_some() { bail!( &error_scope, - "at most one #[func], #[signal], or #[constant] attribute per declaration allowed", + "at most one #[func], #[signal] or #[constant] attribute per declaration allowed", )?; } @@ -461,7 +601,7 @@ fn convert_to_match_expression_or_none(tokens: Option) -> TokenStre } /// Codegen for `#[godot_api] impl GodotExt for MyType` -fn transform_trait_impl(original_impl: Impl) -> Result { +fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult { let (class_name, trait_path) = util::validate_trait_impl_virtual(&original_impl, "godot_api")?; let class_name_obj = util::class_name_obj(&class_name); @@ -487,7 +627,7 @@ fn transform_trait_impl(original_impl: Impl) -> Result { let prv = quote! { ::godot::private }; for item in original_impl.body_items.iter() { - let method = if let ImplMember::Method(f) = item { + let method = if let venial::ImplMember::Method(f) = item { f } else { continue; diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index 4411e0ae3..b8a783484 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -394,12 +394,12 @@ use crate::util::ident; /// /// ## Class hiding /// -/// If you want to register a class with Godot, but not have it show up in the editor then you can use `#[class(hide)]`. +/// If you want to register a class with Godot, but not have it show up in the editor then you can use `#[class(hidden)]`. /// /// ``` /// # use godot::prelude::*; /// #[derive(GodotClass)] -/// #[class(base=Node, init, hide)] +/// #[class(base=Node, init, hidden)] /// pub struct Foo {} /// ``` /// @@ -447,8 +447,6 @@ pub fn derive_godot_class(input: TokenStream) -> TokenStream { /// Proc-macro attribute to be used with `impl` blocks of [`#[derive(GodotClass)]`][GodotClass] structs. /// -/// See also [book chapter _Registering functions_](https://godot-rust.github.io/book/register/functions.html) and following. -/// /// Can be used in two ways: /// ```no_run /// # use godot::prelude::*; @@ -465,37 +463,43 @@ pub fn derive_godot_class(input: TokenStream) -> TokenStream { /// impl INode for MyClass { /* ... */ } /// ``` /// -/// The second case works by implementing the corresponding trait `I` for the base class of your class +/// The second case works by implementing the corresponding trait `I*` for the base class of your class /// (for example `IRefCounted` or `INode3D`). Then, you can add functionality such as: /// * `init` constructors /// * lifecycle methods like `ready` or `process` /// * `on_notification` method /// * `to_string` method /// -/// Neither `#[godot_api]` attribute is required. For small data bundles inheriting `RefCounted`, you may be fine with +/// Neither of the two `#[godot_api]` blocks is required. For small data bundles inheriting `RefCounted`, you may be fine with /// accessing properties directly from GDScript. /// -/// # Examples +/// See also [book chapter _Registering functions_](https://godot-rust.github.io/book/register/functions.html) and following. /// -/// ## `RefCounted` as a base, overridden `init` +/// **Table of contents** +/// - [Constructors](#constructors) +/// - [User-defined `init`](#user-defined-init) +/// - [Generated `init`](#generated-init) +/// - [Lifecycle functions](#lifecycle-functions) +/// - [User-defined functions](#user-defined-functions) +/// - [Associated functions and methods](#associated-functions-and-methods) +/// - [Virtual methods](#virtual-methods) +/// - [Constants and signals](#signals) /// -/// ```no_run -///# use godot::prelude::*; +/// # Constructors +/// +/// Note that `init` (the Godot default constructor) can be either provided by overriding it, or generated with a `#[class(init)]` attribute +/// on the struct. Classes without `init` cannot be instantiated from GDScript. /// +/// ## User-defined `init` +/// +/// ```no_run +/// # use godot::prelude::*; /// #[derive(GodotClass)] /// // no #[class(init)] here, since init() is overridden below. /// // #[class(base=RefCounted)] is implied if no base is specified. /// struct MyStruct; /// /// #[godot_api] -/// impl MyStruct { -/// #[func] -/// pub fn hello_world(&mut self) { -/// godot_print!("Hello World!") -/// } -/// } -/// -/// #[godot_api] /// impl IRefCounted for MyStruct { /// fn init(_base: Base) -> Self { /// MyStruct @@ -503,19 +507,33 @@ pub fn derive_godot_class(input: TokenStream) -> TokenStream { /// } /// ``` /// -/// Note that `init` can be either provided by overriding it, or generated with a `#[class(init)]` attribute on the struct. -/// Classes without `init` cannot be instantiated from GDScript. +/// ## Generated `init` /// -/// ## `Node` as a base, generated `init` +/// This initializes the `Base` field, and every other field with either `Default::default()` or the value specified in `#[init(default = ...)]`. /// /// ```no_run -///# use godot::prelude::*; -/// +/// # use godot::prelude::*; /// #[derive(GodotClass)] /// #[class(init, base=Node)] /// pub struct MyNode { /// base: Base, +/// +/// #[init(default = 42)] +/// some_integer: i64, /// } +/// ``` +/// +/// +/// # Lifecycle functions +/// +/// You can override the lifecycle functions `ready`, `process`, `physics_process` and so on, by implementing the trait corresponding to the +/// base class. +/// +/// ```no_run +/// # use godot::prelude::*; +/// #[derive(GodotClass)] +/// #[class(init, base=Node)] +/// pub struct MyNode; /// /// #[godot_api] /// impl INode for MyNode { @@ -524,6 +542,93 @@ pub fn derive_godot_class(input: TokenStream) -> TokenStream { /// } /// } /// ``` +/// +/// +/// # User-defined functions +/// +/// You can use the `#[func]` attribute to declare your own functions. These are exposed to Godot and callable from GDScript. +/// +/// ## Associated functions and methods +/// +/// If `#[func]` functions are called from the engine, they implicitly bind the surrounding `Gd` pointer: `Gd::bind()` in case of `&self`, +/// `Gd::bind_mut()` in case of `&mut self`. To avoid that, use `#[func(gd_self)]`, which requires an explicit first argument of type `Gd`. +/// +/// Functions without a receiver become static functions in Godot. They can be called from GDScript using `MyStruct.static_function()`. +/// If they return `Gd`, they are effectively constructors that allow taking arguments. +/// +/// ```no_run +/// # use godot::prelude::*; +/// #[derive(GodotClass)] +/// #[class(init)] +/// struct MyStruct { +/// field: i64, +/// base: Base, +/// } +/// +/// #[godot_api] +/// impl MyStruct { +/// #[func] +/// pub fn hello_world(&mut self) { +/// godot_print!("Hello World!") +/// } +/// +/// #[func] +/// pub fn static_function(constructor_arg: i64) -> Gd { +/// Gd::from_init_fn(|base| { +/// MyStruct { field: constructor_arg, base } +/// }) +/// } +/// +/// #[func(gd_self)] +/// pub fn explicit_receiver(mut this: Gd, other_arg: bool) { +/// // Only bind Gd pointer if needed. +/// if other_arg { +/// this.bind_mut().field = 55; +/// } +/// } +/// } +/// ``` +/// +/// ## Virtual methods +/// +/// Functions with the `#[func(virtual)]` attribute are virtual functions, meaning attached scripts can override them. +/// +/// ```no_run +/// # #[cfg(since_api = "4.3")] +/// # mod conditional { +/// # use godot::prelude::*; +/// #[derive(GodotClass)] +/// #[class(init)] +/// struct MyStruct { +/// // Virtual functions require base object. +/// base: Base, +/// } +/// +/// #[godot_api] +/// impl MyStruct { +/// #[func(virtual)] +/// fn language(&self) -> GString { +/// "Rust".into() +/// } +/// } +/// # } +/// ``` +/// +/// In GDScript, your method is available with a `_` prefix, following Godot convention for virtual methods: +/// ```gdscript +/// extends MyStruct +/// +/// func _language(): +/// return "GDScript" +/// ``` +/// +/// Now, `obj.language()` from Rust will dynamically dispatch the call. +/// +/// Make sure you understand the limitations in the [tutorial](https://godot-rust.github.io/book/register/virtual-functions.html). +/// +/// # Constants and signals +/// +/// Please refer to [the book](https://godot-rust.github.io/book/register/constants.html). #[proc_macro_attribute] pub fn godot_api(_meta: TokenStream, input: TokenStream) -> TokenStream { translate(input, class::attribute_godot_api) diff --git a/godot-macros/src/util/mod.rs b/godot-macros/src/util/mod.rs index 9a1dd4efc..26c444a43 100644 --- a/godot-macros/src/util/mod.rs +++ b/godot-macros/src/util/mod.rs @@ -58,7 +58,20 @@ macro_rules! bail { } } -pub(crate) use bail; +macro_rules! require_api_version { + ($min_version:literal, $span:expr, $attribute:literal) => { + if !cfg!(since_api = $min_version) { + bail!( + $span, + "{} requires at least Godot API version {}", + $attribute, + $min_version + ) + } else { + Ok(()) + } + }; +} pub fn error_fn(msg: impl AsRef, tokens: T) -> Error where @@ -73,7 +86,9 @@ macro_rules! error { } } +pub(crate) use bail; pub(crate) use error; +pub(crate) use require_api_version; pub fn reduce_to_signature(function: &Function) -> Function { let mut reduced = function.clone(); diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 8e8c85158..a9ee6eca7 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -107,6 +107,8 @@ //! //! The following features can be enabled for this crate. All off them are off by default. //! +//! Avoid `default-features = false` unless you know exactly what you are doing; it will disable some required internal features. +//! //! * **`double-precision`** //! //! Use `f64` instead of `f32` for the floating-point type [`real`][type@builtin::real]. Requires Godot to be compiled with the diff --git a/itest/godot/ManualFfiTests.gd b/itest/godot/ManualFfiTests.gd index bf493e6d9..cf23410ac 100644 --- a/itest/godot/ManualFfiTests.gd +++ b/itest/godot/ManualFfiTests.gd @@ -293,7 +293,7 @@ func test_option_export(): test_node.free() func test_func_rename(): - var func_rename := FuncRename.new() + var func_rename := FuncObj.new() assert_eq(func_rename.has_method("long_function_name_for_is_true"), false) assert_eq(func_rename.has_method("is_true"), true) @@ -307,27 +307,25 @@ func test_func_rename(): assert_eq(func_rename.has_method("spell_static"), true) assert_eq(func_rename.spell_static(), "static") -var gd_self_reference: GdSelfReference +var gd_self_obj: GdSelfObj func update_self_reference(value): - gd_self_reference.update_internal(value) - -# Todo: Once there is a way to assert for a SCRIPT ERROR failure this can be reenabled. -""" -func test_gd_self_reference_fails(): - # Create the gd_self_reference and connect its signal to a gdscript method that calls back into it. - gd_self_reference = GdSelfReference.new() - gd_self_reference.update_internal_signal.connect(update_self_reference) - - # The returned value will still be 0 because update_internal can't be called in update_self_reference due to a borrowing issue. - assert_eq(gd_self_reference.fail_to_update_internal_value_due_to_conflicting_borrow(10), 0) -""" - -func test_gd_self_reference_succeeds(): - # Create the gd_self_reference and connect its signal to a gdscript method that calls back into it. - gd_self_reference = GdSelfReference.new() - gd_self_reference.update_internal_signal.connect(update_self_reference) - - assert_eq(gd_self_reference.succeed_at_updating_internal_value(10), 10) + gd_self_obj.update_internal(value) + +# TODO: Once there is a way to assert for a SCRIPT ERROR failure, this can be re-enabled. +#func test_gd_self_obj_fails(): +# # Create the gd_self_obj and connect its signal to a gdscript method that calls back into it. +# gd_self_obj = GdSelfObj.new() +# gd_self_obj.update_internal_signal.connect(update_self_reference) +# +# # The returned value will still be 0 because update_internal can't be called in update_self_reference due to a borrowing issue. +# assert_eq(gd_self_obj.fail_to_update_internal_value_due_to_conflicting_borrow(10), 0) + +func test_gd_self_obj_succeeds(): + # Create the gd_self_obj and connect its signal to a gdscript method that calls back into it. + gd_self_obj = GdSelfObj.new() + gd_self_obj.update_internal_signal.connect(update_self_reference) + + assert_eq(gd_self_obj.succeed_at_updating_internal_value(10), 10) func sample_func(): pass diff --git a/itest/godot/SpecialTests.gd b/itest/godot/SpecialTests.gd index 445024def..5642bc35d 100644 --- a/itest/godot/SpecialTests.gd +++ b/itest/godot/SpecialTests.gd @@ -33,18 +33,23 @@ func test_collision_object_2d_input_event(): window.add_child(collision_object) - assert_that(not collision_object.input_event_called()) - assert_eq(collision_object.get_viewport(), null) + assert_that(not collision_object.input_event_called(), "Input event should not be propagated") + assert_eq(collision_object.get_viewport(), null, "Collision viewport should be null") var event := InputEventMouseMotion.new() event.global_position = Vector2.ZERO - window.push_unhandled_input(event) + + # Godot 4.0 compat: behavior of `push_unhandled_input` was not consistent with `push_input`. + if Engine.get_version_info().minor == 0: + window.push_unhandled_input(event) + else: + window.push_input(event) # Ensure we run a full physics frame await root.get_tree().physics_frame - assert_that(collision_object.input_event_called()) - assert_eq(collision_object.get_viewport(), window) + assert_that(collision_object.input_event_called(), "Input event should be propagated") + assert_eq(collision_object.get_viewport(), window, "Collision viewport should be the (non-null) window") window.queue_free() diff --git a/itest/rust/src/register_tests/func_test.rs b/itest/rust/src/register_tests/func_test.rs index 2108059d7..4aa33ce1e 100644 --- a/itest/rust/src/register_tests/func_test.rs +++ b/itest/rust/src/register_tests/func_test.rs @@ -14,10 +14,10 @@ use godot::prelude::*; #[derive(GodotClass)] #[class(init, base=RefCounted)] -struct FuncRename; +struct FuncObj; #[godot_api] -impl FuncRename { +impl FuncObj { #[func(rename=is_true)] fn long_function_name_for_is_true(&self) -> bool { true @@ -54,7 +54,7 @@ impl FuncRename { } } -impl FuncRename { +impl FuncObj { /// Unused but present to demonstrate how `rename = ...` can be used to avoid name clashes. #[allow(dead_code)] fn is_true(&self) -> bool { @@ -68,14 +68,14 @@ impl FuncRename { #[derive(GodotClass)] #[class(base=RefCounted)] -struct GdSelfReference { +struct GdSelfObj { internal_value: i32, base: Base, } #[godot_api] -impl GdSelfReference { +impl GdSelfObj { // A signal that will be looped back to update_internal through gdscript. #[signal] fn update_internal_signal(new_internal: i32); @@ -198,19 +198,19 @@ impl GdSelfReference { } #[func(gd_self)] - fn takes_gd_as_equivalent(mut this: Gd) -> bool { + fn takes_gd_as_equivalent(mut this: Gd) -> bool { this.bind_mut(); true } #[func(gd_self)] - fn takes_gd_as_self_no_return_type(this: Gd) { + fn takes_gd_as_self_no_return_type(this: Gd) { this.bind(); } } #[godot_api] -impl IRefCounted for GdSelfReference { +impl IRefCounted for GdSelfObj { fn init(base: Base) -> Self { Self { internal_value: 0, @@ -249,23 +249,13 @@ impl IRefCounted for GdSelfReference { } } -/// Checks at runtime if a class has a given method through [ClassDb]. -fn class_has_method(name: &str) -> bool { - ClassDb::singleton() - .class_has_method_ex(T::class_name().to_string_name(), name.into()) - .no_inheritance(true) - .done() -} - -/// Checks at runtime if a class has a given signal through [ClassDb]. -fn class_has_signal(name: &str) -> bool { - ClassDb::singleton().class_has_signal(T::class_name().to_string_name(), name.into()) -} +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Tests #[itest] fn cfg_doesnt_interfere_with_valid_method_impls() { // If we re-implement this method but the re-implementation is removed, that should keep the non-removed implementation. - let object = Gd::from_object(FuncRename); + let object = Gd::from_object(FuncObj); assert_eq!( object.bind().returns_hello_world(), GString::from("Hello world!") @@ -278,28 +268,44 @@ fn cfg_doesnt_interfere_with_valid_method_impls() { #[itest] fn cfg_removes_or_keeps_methods() { - assert!(class_has_method::( + assert!(class_has_method::( "func_recognized_with_simple_path_attribute_above_func_attr" )); - assert!(class_has_method::( + assert!(class_has_method::( "func_recognized_with_simple_path_attribute_below_func_attr" )); - assert!(class_has_method::( + assert!(class_has_method::( "cfg_removes_duplicate_function_impl" )); - assert!(!class_has_method::("cfg_removes_function")); + assert!(!class_has_method::("cfg_removes_function")); } #[itest] fn cfg_removes_or_keeps_signals() { - assert!(class_has_signal::( + assert!(class_has_signal::( "signal_recognized_with_simple_path_attribute_above_signal_attr" )); - assert!(class_has_signal::( + assert!(class_has_signal::( "signal_recognized_with_simple_path_attribute_below_signal_attr" )); - assert!(class_has_signal::( + assert!(class_has_signal::( "cfg_removes_duplicate_signal" )); - assert!(!class_has_signal::("cfg_removes_signal")); + assert!(!class_has_signal::("cfg_removes_signal")); +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Helpers + +/// Checks at runtime if a class has a given method through [ClassDb]. +fn class_has_method(name: &str) -> bool { + ClassDb::singleton() + .class_has_method_ex(T::class_name().to_string_name(), name.into()) + .no_inheritance(true) + .done() +} + +/// Checks at runtime if a class has a given signal through [ClassDb]. +fn class_has_signal(name: &str) -> bool { + ClassDb::singleton().class_has_signal(T::class_name().to_string_name(), name.into()) } diff --git a/itest/rust/src/register_tests/func_virtual_test.rs b/itest/rust/src/register_tests/func_virtual_test.rs new file mode 100644 index 000000000..459874ecc --- /dev/null +++ b/itest/rust/src/register_tests/func_virtual_test.rs @@ -0,0 +1,167 @@ +/* + * 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/. + */ + +// Needed for Clippy to accept #[cfg(all())] +#![allow(clippy::non_minimal_cfg)] + +use crate::framework::itest; +use godot::engine::GDScript; +use godot::prelude::*; + +#[derive(GodotClass)] +#[class(init)] +struct VirtualScriptCalls { + _base: Base, +} + +#[godot_api] +impl VirtualScriptCalls { + #[func(virtual)] + fn greet_lang(&self, i: i32) -> GString { + GString::from(format!("Rust#{i}")) + } + + #[func(virtual, rename = greet_lang2)] + fn gl2(&self, s: GString) -> GString { + GString::from(format!("{s} Rust")) + } + + #[func(virtual, gd_self)] + fn greet_lang3(_this: Gd, s: GString) -> GString { + GString::from(format!("{s} Rust")) + } + + #[func(virtual)] + fn set_thing(&mut self, _input: Variant) { + panic!("set_thing() must be overridden") + } + + #[func(virtual)] + fn get_thing(&self) -> Variant { + panic!("get_thing() must be overridden") + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Tests + +#[itest] +fn func_virtual() { + // Without script: "Rust". + let mut object = VirtualScriptCalls::new_gd(); + assert_eq!(object.bind().greet_lang(72), GString::from("Rust#72")); + + // With script: "GDScript". + object.set_script(make_script().to_variant()); + assert_eq!(object.bind().greet_lang(72), GString::from("GDScript#72")); + + // Dynamic call: "GDScript". + let result = object.call("_greet_lang".into(), &[72.to_variant()]); + assert_eq!(result, "GDScript#72".to_variant()); +} + +#[itest] +fn func_virtual_renamed() { + // Without script: "Rust". + let mut object = VirtualScriptCalls::new_gd(); + assert_eq!( + object.bind().gl2("Hello".into()), + GString::from("Hello Rust") + ); + + // With script: "GDScript". + object.set_script(make_script().to_variant()); + assert_eq!( + object.bind().gl2("Hello".into()), + GString::from("Hello GDScript") + ); + + // Dynamic call: "GDScript". + let result = object.call("greet_lang2".into(), &["Hello".to_variant()]); + assert_eq!(result, "Hello GDScript".to_variant()); +} + +#[itest] +fn func_virtual_gd_self() { + // Without script: "Rust". + let mut object = VirtualScriptCalls::new_gd(); + assert_eq!( + VirtualScriptCalls::greet_lang3(object.clone(), "Hoi".into()), + GString::from("Hoi Rust") + ); + + // With script: "GDScript". + object.set_script(make_script().to_variant()); + assert_eq!( + VirtualScriptCalls::greet_lang3(object.clone(), "Hoi".into()), + GString::from("Hoi GDScript") + ); + + // Dynamic call: "GDScript". + let result = object.call("_greet_lang3".into(), &["Hoi".to_variant()]); + assert_eq!(result, "Hoi GDScript".to_variant()); +} + +#[itest] +fn func_virtual_stateful() { + let mut object = VirtualScriptCalls::new_gd(); + object.set_script(make_script().to_variant()); + + let variant = Vector3i::new(1, 2, 2).to_variant(); + object.bind_mut().set_thing(variant.clone()); + + let retrieved = object.bind().get_thing(); + assert_eq!(retrieved, variant); +} + +fn make_script() -> Gd { + let code = r#" +extends VirtualScriptCalls + +var thing + +func _greet_lang(i: int) -> String: + return str("GDScript#", i) + +func greet_lang2(s: String) -> String: + return str(s, " GDScript") + +func _greet_lang3(s: String) -> String: + return str(s, " GDScript") + +func _set_thing(anything): + thing = anything + +func _get_thing(): + return thing +"#; + + let mut script = GDScript::new_gd(); + script.set_source_code(code.into()); + script.reload(); // Necessary so compile is triggered. + + let methods = script + .get_script_method_list() + .iter_shared() + .map(|dict| dict.get("name").unwrap()) + .collect::(); + + // Ensure script has been parsed + compiled correctly. + assert_eq!(script.get_instance_base_type(), "VirtualScriptCalls".into()); + assert_eq!( + methods, + varray![ + "_greet_lang", + "greet_lang2", + "_greet_lang3", + "_set_thing", + "_get_thing" + ] + ); + + script +} diff --git a/itest/rust/src/register_tests/mod.rs b/itest/rust/src/register_tests/mod.rs index 2b2740bae..c5ab15555 100644 --- a/itest/rust/src/register_tests/mod.rs +++ b/itest/rust/src/register_tests/mod.rs @@ -12,4 +12,7 @@ mod gdscript_ffi_test; mod option_ffi_test; mod var_test; +#[cfg(since_api = "4.3")] +mod func_virtual_test; + pub use gdscript_ffi_test::gen_ffi;