diff --git a/godot-macros/src/class/data_models/field_var.rs b/godot-macros/src/class/data_models/field_var.rs index 114110304..f91482c23 100644 --- a/godot-macros/src/class/data_models/field_var.rs +++ b/godot-macros/src/class/data_models/field_var.rs @@ -194,6 +194,12 @@ impl GetterSetterImpl { class_name, FuncDefinition { func: 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. + // Ideally, we'd be able to place #[cfg_attr] on #[var(get)] and #[var(set)] to be able to match a + // #[cfg()] (for instance) placed on the getter/setter function, but that is not currently supported. + external_attributes: Vec::new(), rename: None, has_gd_self: false, }, diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 3bcbe3f41..6905e1407 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -12,6 +12,8 @@ use quote::{format_ident, quote}; pub struct FuncDefinition { /// Raw information about the Rust function. pub func: 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 has_gd_self: bool, @@ -78,7 +80,14 @@ pub fn make_method_registration( }; let param_ident_strs = param_idents.iter().map(|ident| ident.to_string()); + // Transport #[cfg] attrs to the FFI glue to ensure functions which were conditionally + // removed from compilation don't cause errors. + let cfg_attrs = util::extract_cfg_attrs(&func_definition.external_attributes) + .into_iter() + .collect::>(); + quote! { + #(#cfg_attrs)* { use ::godot::obj::GodotClass; use ::godot::builtin::meta::registration::method::MethodInfo; diff --git a/godot-macros/src/class/godot_api.rs b/godot-macros/src/class/godot_api.rs index 2aabdfa7a..54632a60b 100644 --- a/godot-macros/src/class/godot_api.rs +++ b/godot-macros/src/class/godot_api.rs @@ -67,21 +67,35 @@ impl BoundAttr { } } +/// Holds information known from a signal's definition +struct SignalDefinition { + /// The signal's function signature. + signature: Function, + + /// The signal's non-gdext attributes (all except #[signal]). + external_attributes: Vec, +} + /// Codegen for `#[godot_api] impl MyType` fn transform_inherent_impl(mut decl: Impl) -> Result { let class_name = util::validate_impl(&decl, None, "godot_api")?; let class_name_obj = util::class_name_obj(&class_name); let (funcs, signals) = process_godot_fns(&mut decl)?; + let mut signal_cfg_attrs: Vec> = Vec::new(); let mut signal_name_strs: Vec = Vec::new(); let mut signal_parameters_count: Vec = Vec::new(); let mut signal_parameters: Vec = Vec::new(); - for signature in signals { + for signal in signals.iter() { + let SignalDefinition { + signature, + external_attributes, + } = signal; let mut param_types: Vec = Vec::new(); let mut param_names: Vec = Vec::new(); - for param in signature.params.inner { + for param in signature.params.inner.iter() { match ¶m.0 { FnParam::Typed(param) => { param_types.push(param.ty.clone()); @@ -103,6 +117,13 @@ fn transform_inherent_impl(mut decl: Impl) -> Result { ] }; + // Transport #[cfg] attrs to the FFI glue to ensure signals which were conditionally + // removed from compilation don't cause errors. + signal_cfg_attrs.push( + util::extract_cfg_attrs(external_attributes) + .into_iter() + .collect(), + ); signal_name_strs.push(signature.name.to_string()); signal_parameters_count.push(param_names.len()); signal_parameters.push(param_array_decl); @@ -115,6 +136,7 @@ fn transform_inherent_impl(mut decl: Impl) -> Result { .map(|func_def| make_method_registration(&class_name, func_def)); let consts = process_godot_constants(&mut decl)?; + let mut integer_constant_cfg_attrs = Vec::new(); let mut integer_constant_names = Vec::new(); let mut integer_constant_values = Vec::new(); @@ -125,6 +147,15 @@ fn transform_inherent_impl(mut decl: Impl) -> Result { let name = &constant.name; + // Unlike with #[func] and #[signal], we don't remove the attributes from Constant + // signatures within 'process_godot_constants'. + let cfg_attrs = util::extract_cfg_attrs(&constant.attributes) + .into_iter() + .collect::>(); + + // Transport #[cfg] attrs to the FFI glue to ensure constants which were conditionally + // removed from compilation don't cause errors. + integer_constant_cfg_attrs.push(cfg_attrs); integer_constant_names.push(constant.name.to_string()); integer_constant_values.push(quote! { #class_name::#name }); } @@ -136,6 +167,7 @@ fn transform_inherent_impl(mut decl: Impl) -> Result { use ::godot::builtin::StringName; #( + #(#integer_constant_cfg_attrs)* ExportConstant::new( #class_name_obj, ConstantKind::Integer( @@ -164,20 +196,23 @@ fn transform_inherent_impl(mut decl: Impl) -> Result { use ::godot::sys; #( - let parameters_info: [::godot::builtin::meta::PropertyInfo; #signal_parameters_count] = #signal_parameters; - - let mut parameters_info_sys: [::godot::sys::GDExtensionPropertyInfo; #signal_parameters_count] = - std::array::from_fn(|i| parameters_info[i].property_sys()); - - let signal_name = ::godot::builtin::StringName::from(#signal_name_strs); - - sys::interface_fn!(classdb_register_extension_class_signal)( - sys::get_library(), - #class_name_obj.string_sys(), - signal_name.string_sys(), - parameters_info_sys.as_ptr(), - sys::GDExtensionInt::from(#signal_parameters_count as i64), - ); + #(#signal_cfg_attrs)* + { + let parameters_info: [::godot::builtin::meta::PropertyInfo; #signal_parameters_count] = #signal_parameters; + + let mut parameters_info_sys: [::godot::sys::GDExtensionPropertyInfo; #signal_parameters_count] = + std::array::from_fn(|i| parameters_info[i].property_sys()); + + let signal_name = ::godot::builtin::StringName::from(#signal_name_strs); + + sys::interface_fn!(classdb_register_extension_class_signal)( + sys::get_library(), + #class_name_obj.string_sys(), + signal_name.string_sys(), + parameters_info_sys.as_ptr(), + sys::GDExtensionInt::from(#signal_parameters_count as i64), + ); + }; )* } } @@ -203,9 +238,11 @@ fn transform_inherent_impl(mut decl: Impl) -> Result { Ok(result) } -fn process_godot_fns(decl: &mut Impl) -> Result<(Vec, Vec), Error> { +fn process_godot_fns( + decl: &mut Impl, +) -> Result<(Vec, Vec), Error> { let mut func_definitions = vec![]; - let mut signal_signatures = vec![]; + let mut signal_definitions = vec![]; let mut removed_indexes = vec![]; for (index, item) in decl.body_items.iter_mut().enumerate() { @@ -238,6 +275,7 @@ fn process_godot_fns(decl: &mut Impl) -> Result<(Vec, Vec { + 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 { @@ -249,6 +287,7 @@ fn process_godot_fns(decl: &mut Impl) -> Result<(Vec, Vec Result<(Vec, Vec { @@ -278,7 +321,7 @@ fn process_godot_fns(decl: &mut Impl) -> Result<(Vec, Vec Result, Error> { diff --git a/godot-macros/src/util/mod.rs b/godot-macros/src/util/mod.rs index a65511a80..509048458 100644 --- a/godot-macros/src/util/mod.rs +++ b/godot-macros/src/util/mod.rs @@ -227,6 +227,15 @@ pub(crate) fn path_ends_with(path: &[TokenTree], expected: &str) -> bool { .unwrap_or(false) } +pub(crate) fn extract_cfg_attrs( + attrs: &[venial::Attribute], +) -> impl IntoIterator { + attrs.iter().filter(|attr| { + attr.get_single_path_segment() + .map_or(false, |name| name == "cfg") + }) +} + pub(crate) struct DeclInfo { pub where_: Option, pub generic_params: Option, diff --git a/itest/rust/src/register_tests/constant_test.rs b/itest/rust/src/register_tests/constant_test.rs index c671de9c4..1874472ae 100644 --- a/itest/rust/src/register_tests/constant_test.rs +++ b/itest/rust/src/register_tests/constant_test.rs @@ -40,15 +40,45 @@ impl HasConstants { #[constant] #[cfg(all())] const CONSTANT_RECOGNIZED_WITH_SIMPLE_PATH_ATTRIBUTE_BELOW_CONST_ATTR: bool = true; + + // The three identically-named definitions below should be mutually exclusive thanks to #[cfg]. + #[constant] + const CFG_REMOVES_DUPLICATE_CONSTANT_DEF: i64 = 5; + + #[cfg(any())] + #[constant] + const CFG_REMOVES_DUPLICATE_CONSTANT_DEF: i64 = compile_error!("Removed by #[cfg]"); + + #[constant] + #[cfg(any())] + const CFG_REMOVES_DUPLICATE_CONSTANT_DEF: i64 = compile_error!("Removed by #[cfg]"); + + // The constant below should end up not being defined at all. + #[cfg(any())] + #[constant] + const CFG_REMOVES_CONSTANT: bool = compile_error!("Removed by #[cfg]"); + + #[constant] + #[cfg(any())] + const CFG_REMOVES_CONSTANT: bool = compile_error!("Removed by #[cfg]"); +} + +/// Checks at runtime if a class has a given integer constant through [ClassDb]. +fn class_has_integer_constant(name: &str) -> bool { + ClassDb::singleton().class_has_integer_constant(T::class_name().to_string_name(), name.into()) } #[itest] fn constants_correct_value() { - const CONSTANTS: [(&str, i64); 4] = [ + const CONSTANTS: [(&str, i64); 5] = [ ("A", HasConstants::A), ("B", HasConstants::B as i64), ("C", HasConstants::C as i64), ("D", HasConstants::D as i64), + ( + "CFG_REMOVES_DUPLICATE_CONSTANT_DEF", + HasConstants::CFG_REMOVES_DUPLICATE_CONSTANT_DEF, + ), ]; let constants = ClassDb::singleton() @@ -72,6 +102,16 @@ fn constants_correct_value() { static_assert!(HasConstants::CONSTANT_RECOGNIZED_WITH_SIMPLE_PATH_ATTRIBUTE_BELOW_CONST_ATTR); } +#[itest] +fn cfg_removes_or_keeps_constants() { + assert!(class_has_integer_constant::( + "CFG_REMOVES_DUPLICATE_CONSTANT_DEF" + )); + assert!(!class_has_integer_constant::( + "CFG_REMOVES_CONSTANT" + )); +} + #[derive(GodotClass)] struct HasOtherConstants {} diff --git a/itest/rust/src/register_tests/func_test.rs b/itest/rust/src/register_tests/func_test.rs index 4dd66bd1a..19f8a8dae 100644 --- a/itest/rust/src/register_tests/func_test.rs +++ b/itest/rust/src/register_tests/func_test.rs @@ -7,6 +7,8 @@ // Needed for Clippy to accept #[cfg(all())] #![allow(clippy::non_minimal_cfg)] +use crate::framework::itest; +use godot::engine::ClassDb; use godot::prelude::*; #[derive(GodotClass)] @@ -29,6 +31,26 @@ impl FuncRename { fn renamed_static() -> GodotString { GodotString::from("static") } + + #[cfg(all())] + fn returns_hello_world(&self) -> GodotString { + GodotString::from("Hello world!") + } + + #[cfg(any())] + fn returns_hello_world(&self) -> GodotString { + compile_error!("Removed by #[cfg]") + } + + #[cfg(any())] + fn returns_bye_world(&self) -> GodotString { + compile_error!("Removed by #[cfg]") + } + + #[cfg(all())] + fn returns_bye_world(&self) -> GodotString { + GodotString::from("Bye world!") + } } impl FuncRename { @@ -89,6 +111,35 @@ impl GdSelfReference { f1 && f2 } + #[func] + fn cfg_removes_duplicate_function_impl() -> bool { + true + } + + #[cfg(any())] + #[func] + fn cfg_removes_duplicate_function_impl() -> bool { + compile_error!("Removed by #[cfg]") + } + + #[func] + #[cfg(any())] + fn cfg_removes_duplicate_function_impl() -> bool { + compile_error!("Removed by #[cfg]") + } + + #[cfg(any())] + #[func] + fn cfg_removes_function() -> bool { + compile_error!("Removed by #[cfg]") + } + + #[func] + #[cfg(any())] + fn cfg_removes_function() -> bool { + compile_error!("Removed by #[cfg]") + } + #[signal] #[rustfmt::skip] fn signal_shouldnt_panic_with_segmented_path_attribute(); @@ -101,6 +152,25 @@ impl GdSelfReference { #[cfg(all())] fn signal_recognized_with_simple_path_attribute_below_signal_attr(); + #[signal] + fn cfg_removes_duplicate_signal(); + + #[cfg(any())] + #[signal] + fn cfg_removes_duplicate_signal(); + + #[signal] + #[cfg(any())] + fn cfg_removes_duplicate_signal(); + + #[cfg(any())] + #[signal] + fn cfg_removes_signal(); + + #[signal] + #[cfg(any())] + fn cfg_removes_signal(); + #[func] fn fail_to_update_internal_value_due_to_conflicting_borrow( &mut self, @@ -148,3 +218,58 @@ impl RefCountedVirtual 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()) +} + +#[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::new(FuncRename); + assert_eq!( + object.bind().returns_hello_world(), + GodotString::from("Hello world!") + ); + assert_eq!( + object.bind().returns_bye_world(), + GodotString::from("Bye world!") + ); +} + +#[itest] +fn cfg_removes_or_keeps_methods() { + assert!(class_has_method::( + "func_recognized_with_simple_path_attribute_above_func_attr" + )); + assert!(class_has_method::( + "func_recognized_with_simple_path_attribute_below_func_attr" + )); + assert!(class_has_method::( + "cfg_removes_duplicate_function_impl" + )); + assert!(!class_has_method::("cfg_removes_function")); +} + +#[itest] +fn cfg_removes_or_keeps_signals() { + assert!(class_has_signal::( + "signal_recognized_with_simple_path_attribute_above_signal_attr" + )); + assert!(class_has_signal::( + "signal_recognized_with_simple_path_attribute_below_signal_attr" + )); + assert!(class_has_signal::( + "cfg_removes_duplicate_signal" + )); + assert!(!class_has_signal::("cfg_removes_signal")); +}