Skip to content

Commit

Permalink
Allow #[cfg] to be used in #[godot_api] virtual impls
Browse files Browse the repository at this point in the history
  • Loading branch information
PgBiel committed Oct 12, 2023
1 parent 3491d7b commit 5b99137
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 15 deletions.
121 changes: 106 additions & 15 deletions godot-macros/src/class/godot_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>) -> 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<TokenStream, Error> {
let (class_name, trait_name) = util::validate_trait_impl_virtual(&original_impl, "godot_api")?;
Expand All @@ -391,13 +415,14 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
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 };
Expand All @@ -409,52 +434,99 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
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::<Vec<_>>();
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<Self>) {
<Self as #trait_name>::register_class(builder)
}
}
};

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::Base>) -> Self {
<Self as #trait_name>::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 {
<Self as #trait_name>::to_string(self)
}
}
};

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) {
Expand All @@ -466,9 +538,11 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
}
};

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.
Expand All @@ -487,6 +561,11 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
} 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);
}
Expand All @@ -498,6 +577,17 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
.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
Expand All @@ -517,6 +607,7 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {

match name {
#(
#(#virtual_method_cfg_attrs)*
#virtual_method_names => #virtual_method_callbacks,
)*
_ => None,
Expand Down
9 changes: 9 additions & 0 deletions godot-macros/src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item = &venial::Attribute> {
attrs.iter().filter(|attr| {
attr.get_single_path_segment()
.map_or(false, |name| name == "cfg")
})
}

pub(crate) struct DeclInfo {
pub where_: Option<WhereClause>,
pub generic_params: Option<GenericParamList>,
Expand Down
5 changes: 5 additions & 0 deletions itest/rust/src/object_tests/virtual_methods_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,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]")
}
}

// ----------------------------------------------------------------------------------------------------------------------------------------------
Expand Down
30 changes: 30 additions & 0 deletions itest/rust/src/register_tests/func_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,34 @@ impl RefCountedVirtual for GdSelfReference {
base,
}
}

#[cfg(any())]
fn init(base: Base<Self::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]");
}
}

0 comments on commit 5b99137

Please sign in to comment.