Skip to content

Allow #[cfg] to be used with #[godot_api] - Part 2: virtual impls #444

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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]")
}
}

// ----------------------------------------------------------------------------------------------------------------------------------------------
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]");
}
}