Skip to content

Commit

Permalink
[macros] allow multiple inherent impl blocks.
Browse files Browse the repository at this point in the history
All but one blocks must have the key 'secondary'.
  • Loading branch information
0x53A committed Nov 27, 2024
1 parent 8a62088 commit ecda5ea
Show file tree
Hide file tree
Showing 6 changed files with 258 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
117 changes: 93 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,98 @@ 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 `impl` block.

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 {
// 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(
Expand Down
37 changes: 34 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,19 @@ pub fn attribute_godot_api(input_decl: venial::Item) -> ParseResult<TokenStream>
};

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),
}
}
}
31 changes: 29 additions & 2 deletions godot-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -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<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
1 change: 1 addition & 0 deletions itest/rust/src/register_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
77 changes: 77 additions & 0 deletions itest/rust/src/register_tests/multiple_impl_blocks_test.rs
Original file line number Diff line number Diff line change
@@ -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::Base>) -> Self {
Self {}
}
}

#[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() {
let mut obj: Gd<MultipleImplBlocks> = MultipleImplBlocks::new_alloc();

fn call_and_check_result(
gd: &mut Gd<MultipleImplBlocks>,
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::<String>();
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, "foo", "result of foo");
call_and_check_result(&mut obj, "bar", "result of bar");
call_and_check_result(&mut obj, "baz", "result of baz");

obj.free();
}

// ----------------------------------------------------------------------------------------------------------------------------------------------

0 comments on commit ecda5ea

Please sign in to comment.