From 8fb70598b36b85132102ac9e3c80f8976a242a53 Mon Sep 17 00:00:00 2001 From: Lukas Rieger Date: Tue, 22 Oct 2024 23:08:08 +0200 Subject: [PATCH] [macros] allow multiple inherent impl blocks. All but one blocks must have the key 'secondary'. --- godot-ffi/src/plugins.rs | 32 +++-- .../src/class/data_models/inherent_impl.rs | 115 ++++++++++++++---- godot-macros/src/class/godot_api.rs | 31 ++++- godot-macros/src/lib.rs | 39 ++++-- godot-macros/src/util/mod.rs | 18 +++ itest/rust/src/register_tests/mod.rs | 1 + .../multiple_impl_blocks_test.rs | 77 ++++++++++++ 7 files changed, 269 insertions(+), 44 deletions(-) create mode 100644 itest/rust/src/register_tests/multiple_impl_blocks_test.rs diff --git a/godot-ffi/src/plugins.rs b/godot-ffi/src/plugins.rs index f85dd9b39..81a54b1c4 100644 --- a/godot-ffi/src/plugins.rs +++ b/godot-ffi/src/plugins.rs @@ -33,7 +33,7 @@ macro_rules! plugin_registry { #[cfg_attr(rustfmt, rustfmt::skip)] // ^ skip: paste's [< >] syntax chokes fmt // cfg_attr: workaround for https://github.com/rust-lang/rust/pull/52234#issuecomment-976702997 -macro_rules! plugin_add_inner_wasm { +macro_rules! plugin_execute_pre_main_wasm { ($gensym:ident,) => { // Rust presently requires that statics with a custom `#[link_section]` must be a simple // list of bytes on the wasm target (with no extra levels of indirection such as references). @@ -49,14 +49,15 @@ macro_rules! plugin_add_inner_wasm { }; } +/// Executes a block of code before main, by utilising platform specific linker instructions. #[doc(hidden)] #[macro_export] #[allow(clippy::deprecated_cfg_attr)] #[cfg_attr(rustfmt, rustfmt::skip)] // ^ skip: paste's [< >] syntax chokes fmt // cfg_attr: workaround for https://github.com/rust-lang/rust/pull/52234#issuecomment-976702997 -macro_rules! plugin_add_inner { - ($registry:ident; $plugin:expr; $( $path_tt:tt )* ) => { +macro_rules! plugin_execute_pre_main { + ($body:expr) => { const _: () = { #[allow(non_upper_case_globals)] #[used] @@ -76,20 +77,35 @@ macro_rules! plugin_add_inner { #[cfg_attr(target_os = "android", link_section = ".text.startup")] #[cfg_attr(target_os = "linux", link_section = ".text.startup")] extern "C" fn __inner_init() { - let mut guard = $crate::paste::paste!( $( $path_tt )* [< __godot_rust_plugin_ $registry >] ) - .lock() - .unwrap(); - guard.push($plugin); + $body } __inner_init }; #[cfg(target_family = "wasm")] - $crate::gensym! { $crate::plugin_add_inner_wasm!() } + $crate::gensym! { $crate::plugin_execute_pre_main_wasm!() } }; }; } +/// register a plugin by executing code pre-main that adds the plugin to the plugin registry +#[doc(hidden)] +#[macro_export] +#[allow(clippy::deprecated_cfg_attr)] +#[cfg_attr(rustfmt, rustfmt::skip)] +// ^ skip: paste's [< >] syntax chokes fmt +// cfg_attr: workaround for https://github.com/rust-lang/rust/pull/52234#issuecomment-976702997 +macro_rules! plugin_add_inner { + ($registry:ident; $plugin:expr; $( $path_tt:tt )* ) => { + $crate::plugin_execute_pre_main!({ + let mut guard = $crate::paste::paste!( $( $path_tt )* [< __godot_rust_plugin_ $registry >] ) + .lock() + .unwrap(); + guard.push($plugin); + }); + }; +} + /// Register a plugin to a registry #[doc(hidden)] #[macro_export] diff --git a/godot-macros/src/class/data_models/inherent_impl.rs b/godot-macros/src/class/data_models/inherent_impl.rs index 245f26ccc..c0638f8fc 100644 --- a/godot-macros/src/class/data_models/inherent_impl.rs +++ b/godot-macros/src/class/data_models/inherent_impl.rs @@ -64,8 +64,17 @@ struct FuncAttr { // ---------------------------------------------------------------------------------------------------------------------------------------------- +pub struct InherentImplAttr { + /// For implementation reasons, there can be a single 'primary' impl block and 0 or more 'secondary' impl blocks. + /// For now this is controlled by a key in the the 'godot_api' attribute + pub secondary: bool, +} + /// Codegen for `#[godot_api] impl MyType` -pub fn transform_inherent_impl(mut impl_block: venial::Impl) -> ParseResult { +pub fn transform_inherent_impl( + meta: InherentImplAttr, + mut impl_block: venial::Impl, +) -> ParseResult { let class_name = util::validate_impl(&impl_block, None, "godot_api")?; let class_name_obj = util::class_name_obj(&class_name); let prv = quote! { ::godot::private }; @@ -93,38 +102,96 @@ pub fn transform_inherent_impl(mut impl_block: venial::Impl) -> ParseResult, - }, - register_rpcs_fn: Some(#prv::ErasedRegisterRpcsFn { - raw: #prv::callbacks::register_user_rpcs::<#class_name>, - }), - #docs - }), - init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL, + }); }); }; - Ok(result) + if !meta.secondary { + // We are the primary `impl` block. + + let storage = quote! { + #[allow(non_upper_case_globals)] + #[doc(hidden)] + static #method_storage_name: std::sync::Mutex> = std::sync::Mutex::new(Vec::new()); + + #[allow(non_upper_case_globals)] + #[doc(hidden)] + static #constants_storage_name: std::sync::Mutex> = std::sync::Mutex::new(Vec::new()); + }; + + let trait_impl = quote! { + impl ::godot::obj::cap::ImplementsGodotApi for #class_name { + fn __register_methods() { + let guard = #method_storage_name.lock().unwrap(); + for f in guard.iter() { + f(); + } + } + + fn __register_constants() { + let guard = #constants_storage_name.lock().unwrap(); + for f in guard.iter() { + f(); + } + } + + #rpc_registrations + } + }; + + let class_registration = quote! { + + ::godot::sys::plugin_add!(__GODOT_PLUGIN_REGISTRY in #prv; #prv::ClassPlugin { + class_name: #class_name_obj, + item: #prv::PluginItem::InherentImpl(#prv::InherentImpl { + register_methods_constants_fn: #prv::ErasedRegisterFn { + raw: #prv::callbacks::register_user_methods_constants::<#class_name>, + }, + register_rpcs_fn: Some(#prv::ErasedRegisterRpcsFn { + raw: #prv::callbacks::register_user_rpcs::<#class_name>, + }), + #docs + }), + init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL, + }); + + }; + + let result = quote! { + #impl_block + #storage + #trait_impl + #fill_storage + #class_registration + }; + + Ok(result) + } else { + // We are in a secondary `impl` block, so most of the work has already been done + // and we just need to add our registration functions in the storage defined by the primary `impl` block. + + let result = quote! { + #impl_block + #fill_storage + }; + + Ok(result) + } } fn process_godot_fns( diff --git a/godot-macros/src/class/godot_api.rs b/godot-macros/src/class/godot_api.rs index bebadd06a..0e6b967e8 100644 --- a/godot-macros/src/class/godot_api.rs +++ b/godot-macros/src/class/godot_api.rs @@ -8,10 +8,24 @@ use proc_macro2::TokenStream; use crate::class::{transform_inherent_impl, transform_trait_impl}; -use crate::util::bail; +use crate::util::{bail, venial_parse_meta, KvParser}; use crate::ParseResult; -pub fn attribute_godot_api(input_decl: venial::Item) -> ParseResult { +use quote::{format_ident, quote}; + +fn parse_inherent_impl_attr(meta: TokenStream) -> Result { + let item = venial_parse_meta(&meta, format_ident!("godot_api"), "e! { fn func(); })?; + let mut attr = KvParser::parse_required(item.attributes(), "godot_api", &meta)?; + let secondary = attr.handle_alone("secondary")?; + attr.finish()?; + + Ok(super::InherentImplAttr { secondary }) +} + +pub fn attribute_godot_api( + meta: TokenStream, + input_decl: venial::Item, +) -> ParseResult { let decl = match input_decl { venial::Item::Impl(decl) => decl, _ => bail!( @@ -32,8 +46,19 @@ pub fn attribute_godot_api(input_decl: venial::Item) -> ParseResult }; if decl.trait_ty.is_some() { + // 'meta' contains the parameters to the macro, that is, for `#[godot_api(a, b, x=y)]`, anything inside the braces. + // We currently don't accept any parameters for a trait `impl`, so show an error to the user if they added something there. + if meta.to_string() != "" { + return bail!( + meta, + "#[godot_api] on a trait implementation currently does not support any parameters" + ); + } transform_trait_impl(decl) } else { - transform_inherent_impl(decl) + match parse_inherent_impl_attr(meta) { + Ok(meta) => transform_inherent_impl(meta, decl), + Err(err) => Err(err), + } } } diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index 8662f8faa..4fc0fd417 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -520,6 +520,7 @@ pub fn derive_godot_class(input: TokenStream) -> TokenStream { /// - [Virtual methods](#virtual-methods) /// - [RPC attributes](#rpc-attributes) /// - [Constants and signals](#signals) +/// - [Multiple inherent `impl` blocks](#multiple-inherent-impl-blocks) /// /// # Constructors /// @@ -749,9 +750,35 @@ pub fn derive_godot_class(input: TokenStream) -> TokenStream { /// # Constants and signals /// /// Please refer to [the book](https://godot-rust.github.io/book/register/constants.html). +/// +/// # Multiple inherent `impl` blocks +/// +/// Just like with regular structs, you can have multiple inherent `impl` blocks. This can be useful for code organization or when you want to generate code from a proc-macro. +/// For implementation reasons, all but one `impl` blocks must have the key `secondary`. There is no difference between implementing all functions in one block or splitting them up between multiple blocks. +/// ```no_run +/// # use godot::prelude::*; +/// # #[derive(GodotClass)] +/// # #[class(init)] +/// # struct MyStruct { +/// # base: Base, +/// # } +/// #[godot_api] +/// impl MyStruct { +/// #[func] +/// pub fn one(&self) { } +/// } +/// +/// #[godot_api(secondary)] +/// impl MyStruct { +/// #[func] +/// pub fn two(&self) { } +/// } +/// ``` #[proc_macro_attribute] -pub fn godot_api(_meta: TokenStream, input: TokenStream) -> TokenStream { - translate(input, class::attribute_godot_api) +pub fn godot_api(meta: TokenStream, input: TokenStream) -> TokenStream { + translate(input, |body| { + class::attribute_godot_api(TokenStream2::from(meta), body) + }) } /// Derive macro for [`GodotConvert`](../builtin/meta/trait.GodotConvert.html) on structs. @@ -961,13 +988,7 @@ where let input2 = TokenStream2::from(input); let meta2 = TokenStream2::from(meta); - // Hack because venial doesn't support direct meta parsing yet - let input = quote! { - #[#self_name(#meta2)] - #input2 - }; - - let result2 = venial::parse_item(input) + let result2 = util::venial_parse_meta(&meta2, self_name, &input2) .and_then(transform) .unwrap_or_else(|e| e.to_compile_error()); diff --git a/godot-macros/src/util/mod.rs b/godot-macros/src/util/mod.rs index be622420e..1ec2ffe79 100644 --- a/godot-macros/src/util/mod.rs +++ b/godot-macros/src/util/mod.rs @@ -288,3 +288,21 @@ pub fn safe_ident(s: &str) -> Ident { _ => ident(s) } } + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// Parses a `meta` TokenStream, that is, the tokens in parameter position of a proc-macro (between the braces). +/// Because venial can't actually parse a meta item directly, this is done by reconstructing the full macro attribute on top of some content and then parsing *that*. +pub fn venial_parse_meta( + meta: &TokenStream, + self_name: Ident, + content: &TokenStream, +) -> Result { + // Hack because venial doesn't support direct meta parsing yet + let input = quote! { + #[#self_name(#meta)] + #content + }; + + venial::parse_item(input) +} diff --git a/itest/rust/src/register_tests/mod.rs b/itest/rust/src/register_tests/mod.rs index 5b7d225c5..3af883be2 100644 --- a/itest/rust/src/register_tests/mod.rs +++ b/itest/rust/src/register_tests/mod.rs @@ -10,6 +10,7 @@ mod conversion_test; mod derive_godotconvert_test; mod func_test; mod gdscript_ffi_test; +mod multiple_impl_blocks_test; mod naming_tests; mod option_ffi_test; mod register_docs_test; diff --git a/itest/rust/src/register_tests/multiple_impl_blocks_test.rs b/itest/rust/src/register_tests/multiple_impl_blocks_test.rs new file mode 100644 index 000000000..c4c1f275a --- /dev/null +++ b/itest/rust/src/register_tests/multiple_impl_blocks_test.rs @@ -0,0 +1,77 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use godot::classes::IObject; +use godot::obj::{Base, Gd, NewAlloc}; +use godot::register::{godot_api, GodotClass}; + +use crate::framework::itest; + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +#[derive(GodotClass)] +#[class(base=Object)] +struct MultipleImplBlocks {} + +#[godot_api] +impl IObject for MultipleImplBlocks { + fn init(_base: Base) -> Self { + Self {} + } +} + +#[godot_api] +impl MultipleImplBlocks { + #[func] + fn first(&self) -> String { + "1st result".to_string() + } +} + +#[godot_api(secondary)] +impl MultipleImplBlocks { + #[func] + fn second(&self) -> String { + "2nd result".to_string() + } +} + +#[godot_api(secondary)] +impl MultipleImplBlocks { + #[func] + fn third(&self) -> String { + "3rd result".to_string() + } +} + +/// Test that multiple inherent '#[godot_api]' impl blocks can be registered. +/// https://github.com/godot-rust/gdext/pull/927 +#[itest] +fn godot_api_multiple_impl_blocks() { + let mut obj: Gd = MultipleImplBlocks::new_alloc(); + + fn call_and_check_result( + gd: &mut Gd, + method_name: &str, + expected_result: &str, + ) { + assert!(gd.has_method(method_name)); + let result = gd.call(method_name, &[]); + let result_as_string = result.try_to::(); + assert!(result_as_string.is_ok()); + assert_eq!(result_as_string.unwrap(), expected_result); + } + + // Just call all three methods; if that works, then they have all been correctly registered. + call_and_check_result(&mut obj, "first", "1st result"); + call_and_check_result(&mut obj, "second", "2nd result"); + call_and_check_result(&mut obj, "third", "3rd result"); + + obj.free(); +} + +// ----------------------------------------------------------------------------------------------------------------------------------------------