Skip to content

Commit 296da8e

Browse files
committed
[macros] allow multiple inherent impl blocks. subsequent blocks must
have the key 'secondary'
1 parent 8a62088 commit 296da8e

File tree

6 files changed

+258
-37
lines changed

6 files changed

+258
-37
lines changed

godot-ffi/src/plugins.rs

+24-8
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ macro_rules! plugin_registry {
3333
#[cfg_attr(rustfmt, rustfmt::skip)]
3434
// ^ skip: paste's [< >] syntax chokes fmt
3535
// cfg_attr: workaround for https://github.com/rust-lang/rust/pull/52234#issuecomment-976702997
36-
macro_rules! plugin_add_inner_wasm {
36+
macro_rules! plugin_execute_pre_main_wasm {
3737
($gensym:ident,) => {
3838
// Rust presently requires that statics with a custom `#[link_section]` must be a simple
3939
// 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 {
4949
};
5050
}
5151

52+
/// Executes a block of code before main, by utilising platform specific linker instructions.
5253
#[doc(hidden)]
5354
#[macro_export]
5455
#[allow(clippy::deprecated_cfg_attr)]
5556
#[cfg_attr(rustfmt, rustfmt::skip)]
5657
// ^ skip: paste's [< >] syntax chokes fmt
5758
// cfg_attr: workaround for https://github.com/rust-lang/rust/pull/52234#issuecomment-976702997
58-
macro_rules! plugin_add_inner {
59-
($registry:ident; $plugin:expr; $( $path_tt:tt )* ) => {
59+
macro_rules! plugin_execute_pre_main {
60+
($body:expr) => {
6061
const _: () = {
6162
#[allow(non_upper_case_globals)]
6263
#[used]
@@ -76,20 +77,35 @@ macro_rules! plugin_add_inner {
7677
#[cfg_attr(target_os = "android", link_section = ".text.startup")]
7778
#[cfg_attr(target_os = "linux", link_section = ".text.startup")]
7879
extern "C" fn __inner_init() {
79-
let mut guard = $crate::paste::paste!( $( $path_tt )* [< __godot_rust_plugin_ $registry >] )
80-
.lock()
81-
.unwrap();
82-
guard.push($plugin);
80+
$body
8381
}
8482
__inner_init
8583
};
8684

8785
#[cfg(target_family = "wasm")]
88-
$crate::gensym! { $crate::plugin_add_inner_wasm!() }
86+
$crate::gensym! { $crate::plugin_execute_pre_main_wasm!() }
8987
};
9088
};
9189
}
9290

91+
/// register a plugin by executing code pre-main that adds the plugin to the plugin registry
92+
#[doc(hidden)]
93+
#[macro_export]
94+
#[allow(clippy::deprecated_cfg_attr)]
95+
#[cfg_attr(rustfmt, rustfmt::skip)]
96+
// ^ skip: paste's [< >] syntax chokes fmt
97+
// cfg_attr: workaround for https://github.com/rust-lang/rust/pull/52234#issuecomment-976702997
98+
macro_rules! plugin_add_inner {
99+
($registry:ident; $plugin:expr; $( $path_tt:tt )* ) => {
100+
$crate::plugin_execute_pre_main!({
101+
let mut guard = $crate::paste::paste!( $( $path_tt )* [< __godot_rust_plugin_ $registry >] )
102+
.lock()
103+
.unwrap();
104+
guard.push($plugin);
105+
});
106+
};
107+
}
108+
93109
/// Register a plugin to a registry
94110
#[doc(hidden)]
95111
#[macro_export]

godot-macros/src/class/data_models/inherent_impl.rs

+93-24
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,17 @@ struct FuncAttr {
6464

6565
// ----------------------------------------------------------------------------------------------------------------------------------------------
6666

67+
pub struct InherentImplAttr {
68+
/// For implementation reasons, there can be a single 'primary' impl block and 0 or more 'secondary' impl blocks.
69+
/// For now this is controlled by a key in the the 'godot_api' attribute
70+
pub secondary: bool,
71+
}
72+
6773
/// Codegen for `#[godot_api] impl MyType`
68-
pub fn transform_inherent_impl(mut impl_block: venial::Impl) -> ParseResult<TokenStream> {
74+
pub fn transform_inherent_impl(
75+
meta: InherentImplAttr,
76+
mut impl_block: venial::Impl,
77+
) -> ParseResult<TokenStream> {
6978
let class_name = util::validate_impl(&impl_block, None, "godot_api")?;
7079
let class_name_obj = util::class_name_obj(&class_name);
7180
let prv = quote! { ::godot::private };
@@ -93,38 +102,98 @@ pub fn transform_inherent_impl(mut impl_block: venial::Impl) -> ParseResult<Toke
93102

94103
let constant_registration = make_constant_registration(consts, &class_name, &class_name_obj)?;
95104

96-
let result = quote! {
97-
#impl_block
105+
let method_storage_name = format_ident!("__registration_methods_{class_name}");
106+
let constants_storage_name = format_ident!("__registration_constants_{class_name}");
107+
108+
let fill_storage = quote! {
109+
::godot::sys::plugin_execute_pre_main!({
110+
#method_storage_name.lock().unwrap().push(||{
98111

99-
impl ::godot::obj::cap::ImplementsGodotApi for #class_name {
100-
fn __register_methods() {
101112
#( #method_registrations )*
102113
#( #signal_registrations )*
103-
}
104114

105-
fn __register_constants() {
106-
#constant_registration
107-
}
115+
});
116+
#constants_storage_name.lock().unwrap().push(||{
108117

109-
#rpc_registrations
110-
}
118+
#constant_registration
111119

112-
::godot::sys::plugin_add!(__GODOT_PLUGIN_REGISTRY in #prv; #prv::ClassPlugin {
113-
class_name: #class_name_obj,
114-
item: #prv::PluginItem::InherentImpl(#prv::InherentImpl {
115-
register_methods_constants_fn: #prv::ErasedRegisterFn {
116-
raw: #prv::callbacks::register_user_methods_constants::<#class_name>,
117-
},
118-
register_rpcs_fn: Some(#prv::ErasedRegisterRpcsFn {
119-
raw: #prv::callbacks::register_user_rpcs::<#class_name>,
120-
}),
121-
#docs
122-
}),
123-
init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL,
120+
});
124121
});
125122
};
126123

127-
Ok(result)
124+
if !meta.secondary {
125+
// We are the primary `impl` block.
126+
127+
let storage = quote! {
128+
#[used]
129+
#[allow(non_upper_case_globals)]
130+
#[doc(hidden)]
131+
static #method_storage_name: std::sync::Mutex<Vec<fn()>> = std::sync::Mutex::new(Vec::new());
132+
133+
#[used]
134+
#[allow(non_upper_case_globals)]
135+
#[doc(hidden)]
136+
static #constants_storage_name: std::sync::Mutex<Vec<fn()>> = std::sync::Mutex::new(Vec::new());
137+
};
138+
139+
let trait_impl = quote! {
140+
impl ::godot::obj::cap::ImplementsGodotApi for #class_name {
141+
fn __register_methods() {
142+
let guard = #method_storage_name.lock().unwrap();
143+
for f in guard.iter() {
144+
f();
145+
}
146+
}
147+
148+
fn __register_constants() {
149+
let guard = #constants_storage_name.lock().unwrap();
150+
for f in guard.iter() {
151+
f();
152+
}
153+
}
154+
155+
#rpc_registrations
156+
}
157+
};
158+
159+
let class_registration = quote! {
160+
161+
::godot::sys::plugin_add!(__GODOT_PLUGIN_REGISTRY in #prv; #prv::ClassPlugin {
162+
class_name: #class_name_obj,
163+
item: #prv::PluginItem::InherentImpl(#prv::InherentImpl {
164+
register_methods_constants_fn: #prv::ErasedRegisterFn {
165+
raw: #prv::callbacks::register_user_methods_constants::<#class_name>,
166+
},
167+
register_rpcs_fn: Some(#prv::ErasedRegisterRpcsFn {
168+
raw: #prv::callbacks::register_user_rpcs::<#class_name>,
169+
}),
170+
#docs
171+
}),
172+
init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL,
173+
});
174+
175+
};
176+
177+
let result = quote! {
178+
#impl_block
179+
#storage
180+
#trait_impl
181+
#fill_storage
182+
#class_registration
183+
};
184+
185+
Ok(result)
186+
} else {
187+
// We are in a secondary `impl` block, so most of the work has already been done
188+
// and we just need to add out registration functions in the storage defined by the primary `impl` block.
189+
190+
let result = quote! {
191+
#impl_block
192+
#fill_storage
193+
};
194+
195+
Ok(result)
196+
}
128197
}
129198

130199
fn process_godot_fns(

godot-macros/src/class/godot_api.rs

+34-3
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,30 @@
88
use proc_macro2::TokenStream;
99

1010
use crate::class::{transform_inherent_impl, transform_trait_impl};
11-
use crate::util::bail;
11+
use crate::util::{bail, KvParser};
1212
use crate::ParseResult;
1313

14-
pub fn attribute_godot_api(input_decl: venial::Item) -> ParseResult<TokenStream> {
14+
use quote::quote;
15+
16+
fn parse_inherent_impl_attr(meta: TokenStream) -> Result<super::InherentImplAttr, venial::Error> {
17+
// Hack because venial doesn't support direct meta parsing yet.
18+
let input = quote! {
19+
#[godot_api(#meta)]
20+
fn func();
21+
};
22+
23+
let item = venial::parse_item(input)?;
24+
let mut attr = KvParser::parse_required(item.attributes(), "godot_api", &meta)?;
25+
let secondary = attr.handle_alone("secondary")?;
26+
attr.finish()?;
27+
28+
Ok(super::InherentImplAttr { secondary })
29+
}
30+
31+
pub fn attribute_godot_api(
32+
meta: TokenStream,
33+
input_decl: venial::Item,
34+
) -> ParseResult<TokenStream> {
1535
let decl = match input_decl {
1636
venial::Item::Impl(decl) => decl,
1737
_ => bail!(
@@ -32,8 +52,19 @@ pub fn attribute_godot_api(input_decl: venial::Item) -> ParseResult<TokenStream>
3252
};
3353

3454
if decl.trait_ty.is_some() {
55+
// 'meta' contains the parameters to the macro, that is, for `#[godot_api(a, b, x=y)]`, anything inside the braces.
56+
// We currently don't accept any parameters for a trait `impl`, so show an error to the user if they added something there.
57+
if meta.to_string() != "" {
58+
return bail!(
59+
meta,
60+
"#[godot_api] on a trait implementation currently does not support any parameters"
61+
);
62+
}
3563
transform_trait_impl(decl)
3664
} else {
37-
transform_inherent_impl(decl)
65+
match parse_inherent_impl_attr(meta) {
66+
Ok(meta) => transform_inherent_impl(meta, decl),
67+
Err(err) => Err(err),
68+
}
3869
}
3970
}

godot-macros/src/lib.rs

+29-2
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@ pub fn derive_godot_class(input: TokenStream) -> TokenStream {
520520
/// - [Virtual methods](#virtual-methods)
521521
/// - [RPC attributes](#rpc-attributes)
522522
/// - [Constants and signals](#signals)
523+
/// - [Multiple inherent `impl` blocks](#multiple-inherent-impl-blocks)
523524
///
524525
/// # Constructors
525526
///
@@ -749,9 +750,35 @@ pub fn derive_godot_class(input: TokenStream) -> TokenStream {
749750
/// # Constants and signals
750751
///
751752
/// Please refer to [the book](https://godot-rust.github.io/book/register/constants.html).
753+
///
754+
/// # Multiple inherent `impl` blocks
755+
///
756+
/// 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.
757+
/// 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.
758+
/// ```no_run
759+
/// # use godot::prelude::*;
760+
/// # #[derive(GodotClass)]
761+
/// # #[class(init)]
762+
/// # struct MyStruct {
763+
/// # base: Base<RefCounted>,
764+
/// # }
765+
/// #[godot_api]
766+
/// impl MyStruct {
767+
/// #[func]
768+
/// pub fn foo(&self) { }
769+
/// }
770+
///
771+
/// #[godot_api(secondary)]
772+
/// impl MyStruct {
773+
/// #[func]
774+
/// pub fn bar(&self) { }
775+
/// }
776+
/// ```
752777
#[proc_macro_attribute]
753-
pub fn godot_api(_meta: TokenStream, input: TokenStream) -> TokenStream {
754-
translate(input, class::attribute_godot_api)
778+
pub fn godot_api(meta: TokenStream, input: TokenStream) -> TokenStream {
779+
translate(input, |body| {
780+
class::attribute_godot_api(TokenStream2::from(meta), body)
781+
})
755782
}
756783

757784
/// Derive macro for [`GodotConvert`](../builtin/meta/trait.GodotConvert.html) on structs.

itest/rust/src/register_tests/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod conversion_test;
1010
mod derive_godotconvert_test;
1111
mod func_test;
1212
mod gdscript_ffi_test;
13+
mod multiple_impl_blocks_test;
1314
mod naming_tests;
1415
mod option_ffi_test;
1516
mod register_docs_test;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
use godot::classes::IObject;
9+
use godot::obj::{Base, Gd, NewAlloc};
10+
use godot::register::{godot_api, GodotClass};
11+
12+
use crate::framework::itest;
13+
14+
// ----------------------------------------------------------------------------------------------------------------------------------------------
15+
16+
#[derive(GodotClass)]
17+
#[class(base=Object)]
18+
struct MultipleImplBlocks {}
19+
20+
#[godot_api]
21+
impl IObject for MultipleImplBlocks {
22+
fn init(_base: Base<Self::Base>) -> Self {
23+
Self {}
24+
}
25+
}
26+
27+
#[godot_api]
28+
impl MultipleImplBlocks {
29+
#[func]
30+
fn foo(&self) -> String {
31+
"result of foo".to_string()
32+
}
33+
}
34+
35+
#[godot_api(secondary)]
36+
impl MultipleImplBlocks {
37+
#[func]
38+
fn bar(&self) -> String {
39+
"result of bar".to_string()
40+
}
41+
}
42+
43+
#[godot_api(secondary)]
44+
impl MultipleImplBlocks {
45+
#[func]
46+
fn baz(&self) -> String {
47+
"result of baz".to_string()
48+
}
49+
}
50+
51+
/// Test that multiple inherent '#[godot_api]' impl blocks can be registered.
52+
/// https://github.com/godot-rust/gdext/pull/927
53+
#[itest]
54+
fn godot_api_multiple_impl_blocks() {
55+
let mut obj: Gd<MultipleImplBlocks> = MultipleImplBlocks::new_alloc();
56+
57+
fn call_and_check_result(
58+
gd: &mut Gd<MultipleImplBlocks>,
59+
method_name: &str,
60+
expected_result: &str,
61+
) {
62+
assert!(gd.has_method(method_name));
63+
let result = gd.call(method_name, &[]);
64+
let result_as_string = result.try_to::<String>();
65+
assert!(result_as_string.is_ok());
66+
assert_eq!(result_as_string.unwrap(), expected_result);
67+
}
68+
69+
// Just call all three methods; if that works, then they have all been correctly registered.
70+
call_and_check_result(&mut obj, "foo", "result of foo");
71+
call_and_check_result(&mut obj, "bar", "result of bar");
72+
call_and_check_result(&mut obj, "baz", "result of baz");
73+
74+
obj.free();
75+
}
76+
77+
// ----------------------------------------------------------------------------------------------------------------------------------------------

0 commit comments

Comments
 (0)