Skip to content

Commit 41ce77f

Browse files
authored
Merge pull request #444 from PgBiel/allow-cfg-virtual-impl-godot-api
Allow #[cfg] to be used with #[godot_api] - Part 2: virtual impls
2 parents 8165f62 + 8c63cc0 commit 41ce77f

File tree

3 files changed

+141
-15
lines changed

3 files changed

+141
-15
lines changed

godot-macros/src/class/godot_api.rs

+106-15
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,30 @@ where
424424

425425
// ----------------------------------------------------------------------------------------------------------------------------------------------
426426

427+
/// Expects either Some(quote! { () => A, () => B, ... }) or None as the 'tokens' parameter.
428+
/// The idea is that the () => ... arms can be annotated by cfg attrs, so, if any of them compiles (and assuming the cfg
429+
/// attrs only allow one arm to 'survive' compilation), their return value (Some(...)) will be prioritized over the
430+
/// 'None' from the catch-all arm at the end. If, however, none of them compile, then None is returned from the last
431+
/// match arm.
432+
fn convert_to_match_expression_or_none(tokens: Option<TokenStream>) -> TokenStream {
433+
if let Some(tokens) = tokens {
434+
quote! {
435+
{
436+
// When one of the () => ... arms is present, the last arm intentionally won't ever match.
437+
#[allow(unreachable_patterns)]
438+
// Don't warn when only _ => None is present as all () => ... arms were removed from compilation.
439+
#[allow(clippy::match_single_binding)]
440+
match () {
441+
#tokens
442+
_ => None,
443+
}
444+
}
445+
}
446+
} else {
447+
quote! { None }
448+
}
449+
}
450+
427451
/// Codegen for `#[godot_api] impl GodotExt for MyType`
428452
fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
429453
let (class_name, trait_name) = util::validate_trait_impl_virtual(&original_impl, "godot_api")?;
@@ -434,13 +458,14 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
434458
let mut register_class_impl = TokenStream::new();
435459
let mut on_notification_impl = TokenStream::new();
436460

437-
let mut register_fn = quote! { None };
438-
let mut create_fn = quote! { None };
439-
let mut recreate_fn = quote! { None };
440-
let mut to_string_fn = quote! { None };
441-
let mut on_notification_fn = quote! { None };
461+
let mut register_fn = None;
462+
let mut create_fn = None;
463+
let mut recreate_fn = None;
464+
let mut to_string_fn = None;
465+
let mut on_notification_fn = None;
442466

443467
let mut virtual_methods = vec![];
468+
let mut virtual_method_cfg_attrs = vec![];
444469
let mut virtual_method_names = vec![];
445470

446471
let prv = quote! { ::godot::private };
@@ -452,52 +477,99 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
452477
continue;
453478
};
454479

480+
// Transport #[cfg] attributes to the virtual method's FFI glue, to ensure it won't be
481+
// registered in Godot if conditionally removed from compilation.
482+
let cfg_attrs = util::extract_cfg_attrs(&method.attributes)
483+
.into_iter()
484+
.collect::<Vec<_>>();
455485
let method_name = method.name.to_string();
456486
match method_name.as_str() {
457487
"register_class" => {
488+
// Implements the trait once for each implementation of this method, forwarding the cfg attrs of each
489+
// implementation to the generated trait impl. If the cfg attrs allow for multiple implementations of
490+
// this method to exist, then Rust will generate an error, so we don't have to worry about the multiple
491+
// trait implementations actually generating an error, since that can only happen if multiple
492+
// implementations of the same method are kept by #[cfg] (due to user error).
493+
// Thus, by implementing the trait once for each possible implementation of this method (depending on
494+
// what #[cfg] allows), forwarding the cfg attrs, we ensure this trait impl will remain in the code if
495+
// at least one of the method impls are kept.
458496
register_class_impl = quote! {
497+
#register_class_impl
498+
499+
#(#cfg_attrs)*
459500
impl ::godot::obj::cap::GodotRegisterClass for #class_name {
460501
fn __godot_register_class(builder: &mut ::godot::builder::GodotBuilder<Self>) {
461502
<Self as #trait_name>::register_class(builder)
462503
}
463504
}
464505
};
465506

466-
register_fn = quote! {
467-
Some(#prv::ErasedRegisterFn {
507+
// Adds a match arm for each implementation of this method, transferring its respective cfg attrs to
508+
// the corresponding match arm (see explanation for the match after this loop).
509+
// In principle, the cfg attrs will allow only either 0 or 1 of a function with this name to exist,
510+
// unless there are duplicate implementations for the same method, which should error anyway.
511+
// Thus, in any correct program, the match arms (which are, in principle, identical) will be reduced to
512+
// a single one at most, since we forward the cfg attrs. The idea here is precisely to keep this
513+
// specific match arm 'alive' if at least one implementation of the method is also kept (hence why all
514+
// the match arms are identical).
515+
register_fn = Some(quote! {
516+
#register_fn
517+
#(#cfg_attrs)*
518+
() => Some(#prv::ErasedRegisterFn {
468519
raw: #prv::callbacks::register_class_by_builder::<#class_name>
469-
})
470-
};
520+
}),
521+
});
471522
}
472523

473524
"init" => {
474525
godot_init_impl = quote! {
526+
#godot_init_impl
527+
528+
#(#cfg_attrs)*
475529
impl ::godot::obj::cap::GodotInit for #class_name {
476530
fn __godot_init(base: ::godot::obj::Base<Self::Base>) -> Self {
477531
<Self as #trait_name>::init(base)
478532
}
479533
}
480534
};
481-
create_fn = quote! { Some(#prv::callbacks::create::<#class_name>) };
535+
create_fn = Some(quote! {
536+
#create_fn
537+
#(#cfg_attrs)*
538+
() => Some(#prv::callbacks::create::<#class_name>),
539+
});
482540
if cfg!(since_api = "4.2") {
483-
recreate_fn = quote! { Some(#prv::callbacks::recreate::<#class_name>) };
541+
recreate_fn = Some(quote! {
542+
#recreate_fn
543+
#(#cfg_attrs)*
544+
() => Some(#prv::callbacks::recreate::<#class_name>),
545+
});
484546
}
485547
}
486548

487549
"to_string" => {
488550
to_string_impl = quote! {
551+
#to_string_impl
552+
553+
#(#cfg_attrs)*
489554
impl ::godot::obj::cap::GodotToString for #class_name {
490555
fn __godot_to_string(&self) -> ::godot::builtin::GodotString {
491556
<Self as #trait_name>::to_string(self)
492557
}
493558
}
494559
};
495560

496-
to_string_fn = quote! { Some(#prv::callbacks::to_string::<#class_name>) };
561+
to_string_fn = Some(quote! {
562+
#to_string_fn
563+
#(#cfg_attrs)*
564+
() => Some(#prv::callbacks::to_string::<#class_name>),
565+
});
497566
}
498567

499568
"on_notification" => {
500569
on_notification_impl = quote! {
570+
#on_notification_impl
571+
572+
#(#cfg_attrs)*
501573
impl ::godot::obj::cap::GodotNotification for #class_name {
502574
fn __godot_notification(&mut self, what: i32) {
503575
if ::godot::private::is_class_inactive(Self::__config().is_tool) {
@@ -509,9 +581,11 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
509581
}
510582
};
511583

512-
on_notification_fn = quote! {
513-
Some(#prv::callbacks::on_notification::<#class_name>)
514-
};
584+
on_notification_fn = Some(quote! {
585+
#on_notification_fn
586+
#(#cfg_attrs)*
587+
() => Some(#prv::callbacks::on_notification::<#class_name>),
588+
});
515589
}
516590

517591
// Other virtual methods, like ready, process etc.
@@ -530,6 +604,11 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
530604
} else {
531605
format!("_{method_name}")
532606
};
607+
// Note that, if the same method is implemented multiple times (with different cfg attr combinations),
608+
// then there will be multiple match arms annotated with the same cfg attr combinations, thus they will
609+
// be reduced to just one arm (at most, if the implementations aren't all removed from compilation) for
610+
// each distinct method.
611+
virtual_method_cfg_attrs.push(cfg_attrs);
533612
virtual_method_names.push(virtual_method_name);
534613
virtual_methods.push(method);
535614
}
@@ -541,6 +620,17 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
541620
.map(|method| make_virtual_method_callback(&class_name, method))
542621
.collect();
543622

623+
// Use 'match' as a way to only emit 'Some(...)' if the given cfg attrs allow.
624+
// This permits users to conditionally remove virtual method impls from compilation while also removing their FFI
625+
// glue which would otherwise make them visible to Godot even if not really implemented.
626+
// Needs '#[allow(unreachable_patterns)]' to avoid warnings about the last match arm.
627+
// Also requires '#[allow(clippy::match_single_binding)]' for similar reasons.
628+
let register_fn = convert_to_match_expression_or_none(register_fn);
629+
let create_fn = convert_to_match_expression_or_none(create_fn);
630+
let recreate_fn = convert_to_match_expression_or_none(recreate_fn);
631+
let to_string_fn = convert_to_match_expression_or_none(to_string_fn);
632+
let on_notification_fn = convert_to_match_expression_or_none(on_notification_fn);
633+
544634
let result = quote! {
545635
#original_impl
546636
#godot_init_impl
@@ -560,6 +650,7 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
560650

561651
match name {
562652
#(
653+
#(#virtual_method_cfg_attrs)*
563654
#virtual_method_names => #virtual_method_callbacks,
564655
)*
565656
_ => None,

itest/rust/src/object_tests/virtual_methods_test.rs

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

8186
// ----------------------------------------------------------------------------------------------------------------------------------------------

itest/rust/src/register_tests/func_test.rs

+30
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,36 @@ impl RefCountedVirtual for GdSelfReference {
217217
base,
218218
}
219219
}
220+
221+
#[cfg(any())]
222+
fn init(base: Base<Self::Base>) -> Self {
223+
compile_error!("Removed by #[cfg]")
224+
}
225+
226+
#[cfg(all())]
227+
fn to_string(&self) -> GodotString {
228+
GodotString::new()
229+
}
230+
231+
#[cfg(any())]
232+
fn register_class() {
233+
compile_error!("Removed by #[cfg]");
234+
}
235+
236+
#[cfg(all())]
237+
fn on_notification(&mut self, _: godot::engine::notify::ObjectNotification) {
238+
godot_print!("Hello!");
239+
}
240+
241+
#[cfg(any())]
242+
fn on_notification(&mut self, _: godot::engine::notify::ObjectNotification) {
243+
compile_error!("Removed by #[cfg]");
244+
}
245+
246+
#[cfg(any())]
247+
fn cfg_removes_this() {
248+
compile_error!("Removed by #[cfg]");
249+
}
220250
}
221251

222252
/// Checks at runtime if a class has a given method through [ClassDb].

0 commit comments

Comments
 (0)