Skip to content

Commit a9b9b67

Browse files
committed
Allow #[cfg] to be used in #[godot_api] virtual impls
1 parent 3491d7b commit a9b9b67

File tree

4 files changed

+146
-15
lines changed

4 files changed

+146
-15
lines changed

godot-macros/src/class/godot_api.rs

+102-15
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,24 @@ where
381381

382382
// ----------------------------------------------------------------------------------------------------------------------------------------------
383383

384+
/// Expects either Some(quote! { () => A, () => B, ... }) or None as the 'tokens' parameter.
385+
/// The idea is that the () => ... arms can be annotated by cfg attrs, so, if any of them compiles (and assuming the cfg
386+
/// attrs only allow one arm to 'survive' compilation), their return value (Some(...)) will be prioritized over the
387+
/// 'None' from the catch-all arm at the end. If, however, none of them compile, then None is returned from the last
388+
/// match arm.
389+
fn convert_to_match_expression_or_none(tokens: Option<TokenStream>) -> TokenStream {
390+
if let Some(tokens) = tokens {
391+
quote! {
392+
match () {
393+
#tokens
394+
_ => None,
395+
}
396+
}
397+
} else {
398+
quote! { None }
399+
}
400+
}
401+
384402
/// Codegen for `#[godot_api] impl GodotExt for MyType`
385403
fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
386404
let (class_name, trait_name) = util::validate_trait_impl_virtual(&original_impl, "godot_api")?;
@@ -391,13 +409,14 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
391409
let mut register_class_impl = TokenStream::new();
392410
let mut on_notification_impl = TokenStream::new();
393411

394-
let mut register_fn = quote! { None };
395-
let mut create_fn = quote! { None };
396-
let mut recreate_fn = quote! { None };
397-
let mut to_string_fn = quote! { None };
398-
let mut on_notification_fn = quote! { None };
412+
let mut register_fn = None;
413+
let mut create_fn = None;
414+
let mut recreate_fn = None;
415+
let mut to_string_fn = None;
416+
let mut on_notification_fn = None;
399417

400418
let mut virtual_methods = vec![];
419+
let mut virtual_method_cfg_attrs = vec![];
401420
let mut virtual_method_names = vec![];
402421

403422
let prv = quote! { ::godot::private };
@@ -409,52 +428,99 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
409428
continue;
410429
};
411430

431+
// Transport #[cfg] attributes to the virtual method's FFI glue, to ensure it won't be
432+
// registered in Godot if conditionally removed from compilation.
433+
let cfg_attrs = util::extract_cfg_attrs(&method.attributes)
434+
.into_iter()
435+
.collect::<Vec<_>>();
412436
let method_name = method.name.to_string();
413437
match method_name.as_str() {
414438
"register_class" => {
439+
// Implements the trait once for each implementation of this method, forwarding the cfg attrs of each
440+
// implementation to the generated trait impl. If the cfg attrs allow for multiple implementations of
441+
// this method to exist, then Rust will generate an error, so we don't have to worry about the multiple
442+
// trait implementations actually generating an error, since that can only happen if multiple
443+
// implementations of the same method are kept by #[cfg] (due to user error).
444+
// Thus, by implementing the trait once for each possible implementation of this method (depending on
445+
// what #[cfg] allows), forwarding the cfg attrs, we ensure this trait impl will remain in the code if
446+
// at least one of the method impls are kept.
415447
register_class_impl = quote! {
448+
#register_class_impl
449+
450+
#(#cfg_attrs)*
416451
impl ::godot::obj::cap::GodotRegisterClass for #class_name {
417452
fn __godot_register_class(builder: &mut ::godot::builder::GodotBuilder<Self>) {
418453
<Self as #trait_name>::register_class(builder)
419454
}
420455
}
421456
};
422457

423-
register_fn = quote! {
424-
Some(#prv::ErasedRegisterFn {
458+
// Adds a match arm for each implementation of this method, transferring its respective cfg attrs to
459+
// the corresponding match arm (see explanation for the match after this loop).
460+
// In principle, the cfg attrs will allow only either 0 or 1 of a function with this name to exist,
461+
// unless there are duplicate implementations for the same method, which should error anyway.
462+
// Thus, in any correct program, the match arms (which are, in principle, identical) will be reduced to
463+
// a single one at most, since we forward the cfg attrs. The idea here is precisely to keep this
464+
// specific match arm 'alive' if at least one implementation of the method is also kept (hence why all
465+
// the match arms are identical).
466+
register_fn = Some(quote! {
467+
#register_fn
468+
#(#cfg_attrs)*
469+
() => Some(#prv::ErasedRegisterFn {
425470
raw: #prv::callbacks::register_class_by_builder::<#class_name>
426-
})
427-
};
471+
}),
472+
});
428473
}
429474

430475
"init" => {
431476
godot_init_impl = quote! {
477+
#godot_init_impl
478+
479+
#(#cfg_attrs)*
432480
impl ::godot::obj::cap::GodotInit for #class_name {
433481
fn __godot_init(base: ::godot::obj::Base<Self::Base>) -> Self {
434482
<Self as #trait_name>::init(base)
435483
}
436484
}
437485
};
438-
create_fn = quote! { Some(#prv::callbacks::create::<#class_name>) };
486+
create_fn = Some(quote! {
487+
#create_fn
488+
#(#cfg_attrs)*
489+
() => Some(#prv::callbacks::create::<#class_name>),
490+
});
439491
if cfg!(since_api = "4.2") {
440-
recreate_fn = quote! { Some(#prv::callbacks::recreate::<#class_name>) };
492+
recreate_fn = Some(quote! {
493+
#recreate_fn
494+
#(#cfg_attrs)*
495+
() => Some(#prv::callbacks::recreate::<#class_name>),
496+
});
441497
}
442498
}
443499

444500
"to_string" => {
445501
to_string_impl = quote! {
502+
#to_string_impl
503+
504+
#(#cfg_attrs)*
446505
impl ::godot::obj::cap::GodotToString for #class_name {
447506
fn __godot_to_string(&self) -> ::godot::builtin::GodotString {
448507
<Self as #trait_name>::to_string(self)
449508
}
450509
}
451510
};
452511

453-
to_string_fn = quote! { Some(#prv::callbacks::to_string::<#class_name>) };
512+
to_string_fn = Some(quote! {
513+
#to_string_fn
514+
#(#cfg_attrs)*
515+
() => Some(#prv::callbacks::to_string::<#class_name>),
516+
});
454517
}
455518

456519
"on_notification" => {
457520
on_notification_impl = quote! {
521+
#on_notification_impl
522+
523+
#(#cfg_attrs)*
458524
impl ::godot::obj::cap::GodotNotification for #class_name {
459525
fn __godot_notification(&mut self, what: i32) {
460526
if ::godot::private::is_class_inactive(Self::__config().is_tool) {
@@ -466,9 +532,11 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
466532
}
467533
};
468534

469-
on_notification_fn = quote! {
470-
Some(#prv::callbacks::on_notification::<#class_name>)
471-
};
535+
on_notification_fn = Some(quote! {
536+
#on_notification_fn
537+
#(#cfg_attrs)*
538+
() => Some(#prv::callbacks::on_notification::<#class_name>),
539+
});
472540
}
473541

474542
// Other virtual methods, like ready, process etc.
@@ -487,6 +555,11 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
487555
} else {
488556
format!("_{method_name}")
489557
};
558+
// Note that, if the same method is implemented multiple times (with different cfg attr combinations),
559+
// then there will be multiple match arms annotated with the same cfg attr combinations, thus they will
560+
// be reduced to just one arm (at most, if the implementations aren't all removed from compilation) for
561+
// each distinct method.
562+
virtual_method_cfg_attrs.push(cfg_attrs);
490563
virtual_method_names.push(virtual_method_name);
491564
virtual_methods.push(method);
492565
}
@@ -498,6 +571,17 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
498571
.map(|method| make_virtual_method_callback(&class_name, method))
499572
.collect();
500573

574+
// Use 'match' as a way to only emit 'Some(...)' if the given cfg attrs allow.
575+
// This permits users to conditionally remove virtual method impls from compilation while also removing their FFI
576+
// glue which would otherwise make them visible to Godot even if not really implemented.
577+
// Needs '#[allow(unreachable_patterns)]' to avoid warnings about the last match arm.
578+
// Also requires '#[allow(clippy::match_single_binding)]' for similar reasons.
579+
let register_fn = convert_to_match_expression_or_none(register_fn);
580+
let create_fn = convert_to_match_expression_or_none(create_fn);
581+
let recreate_fn = convert_to_match_expression_or_none(recreate_fn);
582+
let to_string_fn = convert_to_match_expression_or_none(to_string_fn);
583+
let on_notification_fn = convert_to_match_expression_or_none(on_notification_fn);
584+
501585
let result = quote! {
502586
#original_impl
503587
#godot_init_impl
@@ -517,6 +601,7 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
517601

518602
match name {
519603
#(
604+
#(#virtual_method_cfg_attrs)*
520605
#virtual_method_names => #virtual_method_callbacks,
521606
)*
522607
_ => None,
@@ -526,6 +611,8 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
526611

527612
::godot::sys::plugin_add!(__GODOT_PLUGIN_REGISTRY in #prv; #prv::ClassPlugin {
528613
class_name: #class_name_obj,
614+
#[allow(unreachable_patterns)] // due to the cfg-based match statements
615+
#[allow(clippy::match_single_binding)] // avoid warning on single-arm matches
529616
component: #prv::PluginComponent::UserVirtuals {
530617
user_register_fn: #register_fn,
531618
user_create_fn: #create_fn,

godot-macros/src/util/mod.rs

+9
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,15 @@ pub(crate) fn path_ends_with(path: &[TokenTree], expected: &str) -> bool {
227227
.unwrap_or(false)
228228
}
229229

230+
pub(crate) fn extract_cfg_attrs(
231+
attrs: &[venial::Attribute],
232+
) -> impl IntoIterator<Item = &venial::Attribute> {
233+
attrs.iter().filter(|attr| {
234+
attr.get_single_path_segment()
235+
.map_or(false, |name| name == "cfg")
236+
})
237+
}
238+
230239
pub(crate) struct DeclInfo {
231240
pub where_: Option<WhereClause>,
232241
pub generic_params: Option<GenericParamList>,

itest/rust/src/object_tests/virtual_methods_test.rs

+5
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ impl Node2DVirtual for ReadyVirtualTest {
7575
fn ready(&mut self) {
7676
self.implementation_value += 1;
7777
}
78+
79+
#[cfg(any())]
80+
fn to_string(&self) -> GodotString {
81+
compile_error!("Removed by #[cfg]")
82+
}
7883
}
7984

8085
// ----------------------------------------------------------------------------------------------------------------------------------------------

itest/rust/src/register_tests/func_test.rs

+30
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,34 @@ impl RefCountedVirtual for GdSelfReference {
147147
base,
148148
}
149149
}
150+
151+
#[cfg(any())]
152+
fn init(base: Base<Self::Base>) -> Self {
153+
compile_error!("Removed by #[cfg]")
154+
}
155+
156+
#[cfg(all())]
157+
fn to_string(&self) -> GodotString {
158+
GodotString::new()
159+
}
160+
161+
#[cfg(any())]
162+
fn register_class() {
163+
compile_error!("Removed by #[cfg]");
164+
}
165+
166+
#[cfg(all())]
167+
fn on_notification(&mut self, _: godot::engine::notify::ObjectNotification) {
168+
godot_print!("Hello!");
169+
}
170+
171+
#[cfg(any())]
172+
fn on_notification(&mut self, _: godot::engine::notify::ObjectNotification) {
173+
compile_error!("Removed by #[cfg]");
174+
}
175+
176+
#[cfg(any())]
177+
fn cfg_removes_this() {
178+
compile_error!("Removed by #[cfg]");
179+
}
150180
}

0 commit comments

Comments
 (0)