Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple #[godot_api] impl blocks #927

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/full-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
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();
}
Loading