diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index 14249778e..1796f73d7 100644 --- a/.github/workflows/full-ci.yml +++ b/.github/workflows/full-ci.yml @@ -8,6 +8,8 @@ name: Full CI # Runs before merging. Rebases on master to make sure CI passes for latest integration, not only for the PR at the time of creation. on: + # allow manually triggering the workflow (https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_dispatch) + workflow_dispatch: merge_group: # push: diff --git a/godot-ffi/src/plugins.rs b/godot-ffi/src/plugins.rs index f85dd9b39..5af69a417 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..3d019d25f 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,107 @@ 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 + + let storage = quote! { + + #[used] + #[allow(non_upper_case_globals)] + #[doc(hidden)] + static #method_storage_name: std::sync::Mutex> = std::sync::Mutex::new(Vec::new()); + + #[used] + #[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 { + // secondary + + 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..895e32dbc 100644 --- a/godot-macros/src/class/godot_api.rs +++ b/godot-macros/src/class/godot_api.rs @@ -8,10 +8,30 @@ use proc_macro2::TokenStream; use crate::class::{transform_inherent_impl, transform_trait_impl}; -use crate::util::bail; +use crate::util::{bail, KvParser}; use crate::ParseResult; -pub fn attribute_godot_api(input_decl: venial::Item) -> ParseResult { +use quote::quote; + +fn parse_inherent_impl_attr(meta: TokenStream) -> Result { + // Hack because venial doesn't support direct meta parsing yet. + let input = quote! { + #[godot_api(#meta)] + fn func(); + }; + + let item = venial::parse_item(input)?; + 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 +52,17 @@ pub fn attribute_godot_api(input_decl: venial::Item) -> ParseResult }; if decl.trait_ty.is_some() { + 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 05e88843e..dee439be1 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -679,9 +679,36 @@ 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, subsequent 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 { +/// # // Virtual functions require base object. +/// # base: Base, +/// # } +/// #[godot_api] +/// impl MyStruct { +/// #[func] +/// pub fn foo(&self) { } +/// } +/// +/// #[godot_api(secondary)] +/// impl MyStruct { +/// #[func] +/// pub fn bar(&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. diff --git a/itest/rust/src/object_tests/object_test.rs b/itest/rust/src/object_tests/object_test.rs index 46a8462fe..441a7fae9 100644 --- a/itest/rust/src/object_tests/object_test.rs +++ b/itest/rust/src/object_tests/object_test.rs @@ -1076,3 +1076,59 @@ fn double_use_reference() { double_use.free(); emitter.free(); } + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +#[derive(GodotClass)] +#[class(init, base=Object)] +struct MultipleImplBlocks {} + +#[godot_api] +impl MultipleImplBlocks { + #[func] + fn foo(&self) -> String { + "result of foo".to_string() + } +} + +#[godot_api(secondary)] +impl MultipleImplBlocks { + #[func] + fn bar(&self) -> String { + "result of bar".to_string() + } +} + +#[godot_api(secondary)] +impl MultipleImplBlocks { + #[func] + fn baz(&self) -> String { + "result of baz".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_pull_927() { + let mut obj: Gd = MultipleImplBlocks::new_alloc(); + + fn call_and_check_result( + gd: &mut Gd, + method_name: StringName, + expected_result: String, + ) { + 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!(expected_result, result_as_string.unwrap()); + } + + // just call all three methods, if that works, then they have all been correctly registered + call_and_check_result(&mut obj, "foo".into(), "result of foo".into()); + call_and_check_result(&mut obj, "bar".into(), "result of bar".into()); + call_and_check_result(&mut obj, "baz".into(), "result of baz".into()); + + obj.free(); +}