Skip to content

Commit

Permalink
[macros] allow multiple inherent impl blocks. subsequent blocks must
Browse files Browse the repository at this point in the history
have the key 'secondary'
  • Loading branch information
0x53A committed Nov 6, 2024
1 parent 43e214a commit 2a5660f
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 37 deletions.
32 changes: 24 additions & 8 deletions godot-ffi/src/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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]
Expand All @@ -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]
Expand Down
126 changes: 102 additions & 24 deletions godot-macros/src/class/data_models/inherent_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TokenStream> {
pub fn transform_inherent_impl(
meta: InherentImplAttr,
mut impl_block: venial::Impl,
) -> ParseResult<TokenStream> {
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 };
Expand Down Expand Up @@ -93,38 +102,107 @@ pub fn transform_inherent_impl(mut impl_block: venial::Impl) -> ParseResult<Toke

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

let result = quote! {
#impl_block
let method_storage_name = format_ident!("__registration_methods_{class_name}");
let constants_storage_name = format_ident!("__registration_constants_{class_name}");

let fill_storage = quote! {
::godot::sys::plugin_execute_pre_main!({
#method_storage_name.lock().unwrap().push(||{

impl ::godot::obj::cap::ImplementsGodotApi for #class_name {
fn __register_methods() {
#( #method_registrations )*
#( #signal_registrations )*
}

fn __register_constants() {
#constant_registration
}
});
#constants_storage_name.lock().unwrap().push(||{

#rpc_registrations
}
#constant_registration

::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,
});
});
};

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<Vec<fn()>> = std::sync::Mutex::new(Vec::new());

#[used]
#[allow(non_upper_case_globals)]
#[doc(hidden)]
static #constants_storage_name: std::sync::Mutex<Vec<fn()>> = 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(
Expand Down
35 changes: 32 additions & 3 deletions godot-macros/src/class/godot_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TokenStream> {
use quote::quote;

fn parse_inherent_impl_attr(meta: TokenStream) -> Result<super::InherentImplAttr, venial::Error> {
// 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<TokenStream> {
let decl = match input_decl {
venial::Item::Impl(decl) => decl,
_ => bail!(
Expand All @@ -32,8 +52,17 @@ pub fn attribute_godot_api(input_decl: venial::Item) -> ParseResult<TokenStream>
};

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),
}
}
}
31 changes: 29 additions & 2 deletions godot-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RefCounted>,
/// # }
/// #[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.
Expand Down
56 changes: 56 additions & 0 deletions itest/rust/src/object_tests/object_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> = MultipleImplBlocks::new_alloc();

fn call_and_check_result(
gd: &mut Gd<MultipleImplBlocks>,
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::<String>();
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();
}

0 comments on commit 2a5660f

Please sign in to comment.