diff --git a/godot-macros/src/class/godot_api.rs b/godot-macros/src/class/godot_api.rs index 2aabdfa7a..42655ff54 100644 --- a/godot-macros/src/class/godot_api.rs +++ b/godot-macros/src/class/godot_api.rs @@ -381,6 +381,30 @@ where // ---------------------------------------------------------------------------------------------------------------------------------------------- +/// Expects either Some(quote! { () => A, () => B, ... }) or None as the 'tokens' parameter. +/// The idea is that the () => ... arms can be annotated by cfg attrs, so, if any of them compiles (and assuming the cfg +/// attrs only allow one arm to 'survive' compilation), their return value (Some(...)) will be prioritized over the +/// 'None' from the catch-all arm at the end. If, however, none of them compile, then None is returned from the last +/// match arm. +fn convert_to_match_expression_or_none(tokens: Option) -> TokenStream { + if let Some(tokens) = tokens { + quote! { + { + // When one of the () => ... arms is present, the last arm intentionally won't ever match. + #[allow(unreachable_patterns)] + // Don't warn when only _ => None is present as all () => ... arms were removed from compilation. + #[allow(clippy::match_single_binding)] + match () { + #tokens + _ => None, + } + } + } + } else { + quote! { None } + } +} + /// Codegen for `#[godot_api] impl GodotExt for MyType` fn transform_trait_impl(original_impl: Impl) -> Result { let (class_name, trait_name) = util::validate_trait_impl_virtual(&original_impl, "godot_api")?; @@ -391,13 +415,14 @@ fn transform_trait_impl(original_impl: Impl) -> Result { let mut register_class_impl = TokenStream::new(); let mut on_notification_impl = TokenStream::new(); - 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 }; + let mut register_fn = None; + let mut create_fn = None; + let mut recreate_fn = None; + let mut to_string_fn = None; + let mut on_notification_fn = None; let mut virtual_methods = vec![]; + let mut virtual_method_cfg_attrs = vec![]; let mut virtual_method_names = vec![]; let prv = quote! { ::godot::private }; @@ -409,10 +434,26 @@ fn transform_trait_impl(original_impl: Impl) -> Result { continue; }; + // Transport #[cfg] attributes to the virtual method's FFI glue, to ensure it won't be + // registered in Godot if conditionally removed from compilation. + let cfg_attrs = util::extract_cfg_attrs(&method.attributes) + .into_iter() + .collect::>(); let method_name = method.name.to_string(); match method_name.as_str() { "register_class" => { + // Implements the trait once for each implementation of this method, forwarding the cfg attrs of each + // implementation to the generated trait impl. If the cfg attrs allow for multiple implementations of + // this method to exist, then Rust will generate an error, so we don't have to worry about the multiple + // trait implementations actually generating an error, since that can only happen if multiple + // implementations of the same method are kept by #[cfg] (due to user error). + // Thus, by implementing the trait once for each possible implementation of this method (depending on + // what #[cfg] allows), forwarding the cfg attrs, we ensure this trait impl will remain in the code if + // at least one of the method impls are kept. register_class_impl = quote! { + #register_class_impl + + #(#cfg_attrs)* impl ::godot::obj::cap::GodotRegisterClass for #class_name { fn __godot_register_class(builder: &mut ::godot::builder::GodotBuilder) { ::register_class(builder) @@ -420,29 +461,53 @@ fn transform_trait_impl(original_impl: Impl) -> Result { } }; - register_fn = quote! { - Some(#prv::ErasedRegisterFn { + // Adds a match arm for each implementation of this method, transferring its respective cfg attrs to + // the corresponding match arm (see explanation for the match after this loop). + // In principle, the cfg attrs will allow only either 0 or 1 of a function with this name to exist, + // unless there are duplicate implementations for the same method, which should error anyway. + // Thus, in any correct program, the match arms (which are, in principle, identical) will be reduced to + // a single one at most, since we forward the cfg attrs. The idea here is precisely to keep this + // specific match arm 'alive' if at least one implementation of the method is also kept (hence why all + // the match arms are identical). + register_fn = Some(quote! { + #register_fn + #(#cfg_attrs)* + () => Some(#prv::ErasedRegisterFn { raw: #prv::callbacks::register_class_by_builder::<#class_name> - }) - }; + }), + }); } "init" => { godot_init_impl = quote! { + #godot_init_impl + + #(#cfg_attrs)* impl ::godot::obj::cap::GodotInit for #class_name { fn __godot_init(base: ::godot::obj::Base) -> Self { ::init(base) } } }; - create_fn = quote! { Some(#prv::callbacks::create::<#class_name>) }; + create_fn = Some(quote! { + #create_fn + #(#cfg_attrs)* + () => Some(#prv::callbacks::create::<#class_name>), + }); if cfg!(since_api = "4.2") { - recreate_fn = quote! { Some(#prv::callbacks::recreate::<#class_name>) }; + recreate_fn = Some(quote! { + #recreate_fn + #(#cfg_attrs)* + () => Some(#prv::callbacks::recreate::<#class_name>), + }); } } "to_string" => { to_string_impl = quote! { + #to_string_impl + + #(#cfg_attrs)* impl ::godot::obj::cap::GodotToString for #class_name { fn __godot_to_string(&self) -> ::godot::builtin::GodotString { ::to_string(self) @@ -450,11 +515,18 @@ fn transform_trait_impl(original_impl: Impl) -> Result { } }; - to_string_fn = quote! { Some(#prv::callbacks::to_string::<#class_name>) }; + to_string_fn = Some(quote! { + #to_string_fn + #(#cfg_attrs)* + () => Some(#prv::callbacks::to_string::<#class_name>), + }); } "on_notification" => { on_notification_impl = quote! { + #on_notification_impl + + #(#cfg_attrs)* impl ::godot::obj::cap::GodotNotification for #class_name { fn __godot_notification(&mut self, what: i32) { if ::godot::private::is_class_inactive(Self::__config().is_tool) { @@ -466,9 +538,11 @@ fn transform_trait_impl(original_impl: Impl) -> Result { } }; - on_notification_fn = quote! { - Some(#prv::callbacks::on_notification::<#class_name>) - }; + on_notification_fn = Some(quote! { + #on_notification_fn + #(#cfg_attrs)* + () => Some(#prv::callbacks::on_notification::<#class_name>), + }); } // Other virtual methods, like ready, process etc. @@ -487,6 +561,11 @@ fn transform_trait_impl(original_impl: Impl) -> Result { } else { format!("_{method_name}") }; + // Note that, if the same method is implemented multiple times (with different cfg attr combinations), + // then there will be multiple match arms annotated with the same cfg attr combinations, thus they will + // be reduced to just one arm (at most, if the implementations aren't all removed from compilation) for + // each distinct method. + virtual_method_cfg_attrs.push(cfg_attrs); virtual_method_names.push(virtual_method_name); virtual_methods.push(method); } @@ -498,6 +577,17 @@ fn transform_trait_impl(original_impl: Impl) -> Result { .map(|method| make_virtual_method_callback(&class_name, method)) .collect(); + // Use 'match' as a way to only emit 'Some(...)' if the given cfg attrs allow. + // This permits users to conditionally remove virtual method impls from compilation while also removing their FFI + // glue which would otherwise make them visible to Godot even if not really implemented. + // Needs '#[allow(unreachable_patterns)]' to avoid warnings about the last match arm. + // Also requires '#[allow(clippy::match_single_binding)]' for similar reasons. + let register_fn = convert_to_match_expression_or_none(register_fn); + let create_fn = convert_to_match_expression_or_none(create_fn); + let recreate_fn = convert_to_match_expression_or_none(recreate_fn); + let to_string_fn = convert_to_match_expression_or_none(to_string_fn); + let on_notification_fn = convert_to_match_expression_or_none(on_notification_fn); + let result = quote! { #original_impl #godot_init_impl @@ -517,6 +607,7 @@ fn transform_trait_impl(original_impl: Impl) -> Result { match name { #( + #(#virtual_method_cfg_attrs)* #virtual_method_names => #virtual_method_callbacks, )* _ => None, 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/object_tests/virtual_methods_test.rs b/itest/rust/src/object_tests/virtual_methods_test.rs index c3d40e4d3..93825e862 100644 --- a/itest/rust/src/object_tests/virtual_methods_test.rs +++ b/itest/rust/src/object_tests/virtual_methods_test.rs @@ -76,6 +76,11 @@ impl Node2DVirtual for ReadyVirtualTest { fn ready(&mut self) { self.implementation_value += 1; } + + #[cfg(any())] + fn to_string(&self) -> GodotString { + compile_error!("Removed by #[cfg]") + } } // ---------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/itest/rust/src/register_tests/func_test.rs b/itest/rust/src/register_tests/func_test.rs index 4dd66bd1a..3b5ec7e30 100644 --- a/itest/rust/src/register_tests/func_test.rs +++ b/itest/rust/src/register_tests/func_test.rs @@ -147,4 +147,34 @@ impl RefCountedVirtual for GdSelfReference { base, } } + + #[cfg(any())] + fn init(base: Base) -> Self { + compile_error!("Removed by #[cfg]") + } + + #[cfg(all())] + fn to_string(&self) -> GodotString { + GodotString::new() + } + + #[cfg(any())] + fn register_class() { + compile_error!("Removed by #[cfg]"); + } + + #[cfg(all())] + fn on_notification(&mut self, _: godot::engine::notify::ObjectNotification) { + godot_print!("Hello!"); + } + + #[cfg(any())] + fn on_notification(&mut self, _: godot::engine::notify::ObjectNotification) { + compile_error!("Removed by #[cfg]"); + } + + #[cfg(any())] + fn cfg_removes_this() { + compile_error!("Removed by #[cfg]"); + } }