From c4dd6b77f7b0324e641abfa3602666ef56fffded Mon Sep 17 00:00:00 2001 From: Houtamelo Date: Thu, 19 Sep 2024 10:20:37 -0300 Subject: [PATCH 1/2] #[rpc] annotation for functions in #[godot_api] inherent impl blocks. The style is similar to GDScript's @rpc annotation, the macro can be used as follows: #1 - Separate arguments: ```rust #[rpc(any_peer, reliable)] fn some_rpc(&mut self) { //.. } ``` Providing overlapping arguments generates a compile error. Any omitted arguments are set to their default values. #2 - Provide an expression: ```rust const CONFIG: RpcArgs = RpcArgs { mode: RpcMode::Authority, ..RpcArgs::default() }; #[rpc(config = CONFIG_EXPR)] fn some_rpc(&mut self) { //.. } ``` Number #2 is useful in case you want to reuse the configuration on multiple functions. Number #2 is mutually exclusive with number #1. --- The generated macro code works as follows: - Caches the configuration in a `ClassPlugin`. - On `__before_ready()`, searches for the configuration in the plugin, registering them with Node::rpc_config(). --- godot-core/src/docs.rs | 4 +- godot-core/src/meta/mod.rs | 6 + godot-core/src/meta/rpc_config.rs | 50 ++++ godot-core/src/obj/traits.rs | 3 + godot-core/src/private.rs | 22 +- godot-core/src/registry/callbacks.rs | 4 + godot-core/src/registry/class.rs | 21 +- godot-core/src/registry/plugin.rs | 35 ++- godot-macros/Cargo.toml | 3 +- .../src/class/data_models/field_var.rs | 1 + godot-macros/src/class/data_models/func.rs | 3 + .../src/class/data_models/inherent_impl.rs | 232 +++++++++++++----- godot-macros/src/class/data_models/rpc.rs | 162 ++++++++++++ godot-macros/src/class/derive_godot_class.rs | 25 +- godot-macros/src/class/mod.rs | 3 + godot-macros/src/lib.rs | 40 ++- godot/Cargo.toml | 2 +- godot/src/lib.rs | 2 + .../rust/src/register_tests/constant_test.rs | 5 +- itest/rust/src/register_tests/mod.rs | 2 + itest/rust/src/register_tests/rpc_test.rs | 90 +++++++ 21 files changed, 613 insertions(+), 102 deletions(-) create mode 100644 godot-core/src/meta/rpc_config.rs create mode 100644 godot-macros/src/class/data_models/rpc.rs create mode 100644 itest/rust/src/register_tests/rpc_test.rs diff --git a/godot-core/src/docs.rs b/godot-core/src/docs.rs index f6bcda41b..9e153f254 100644 --- a/godot-core/src/docs.rs +++ b/godot-core/src/docs.rs @@ -6,7 +6,7 @@ */ use crate::meta::ClassName; -use crate::registry::plugin::PluginItem; +use crate::registry::plugin::{InherentImpl, PluginItem}; use std::collections::HashMap; /// Created for documentation on @@ -77,7 +77,7 @@ pub fn gather_xml_docs() -> impl Iterator { let class_name = x.class_name; match x.item { - PluginItem::InherentImpl { docs, .. } => { + PluginItem::InherentImpl(InherentImpl { docs, .. }) => { map.entry(class_name).or_default().inherent = docs } diff --git a/godot-core/src/meta/mod.rs b/godot-core/src/meta/mod.rs index b5a2e4a6f..138ab2f68 100644 --- a/godot-core/src/meta/mod.rs +++ b/godot-core/src/meta/mod.rs @@ -40,6 +40,10 @@ mod godot_convert; mod method_info; mod property_info; mod ref_arg; +// RpcConfig uses `MultiplayerPeer::TransferMode` and `MultiplayerApi::RpcMode`, +// which are only available when `codegen-full` is enabled. +#[cfg(feature = "codegen-full")] +mod rpc_config; mod sealed; mod signature; mod traits; @@ -47,6 +51,8 @@ mod traits; pub mod error; pub use class_name::ClassName; pub use godot_convert::{FromGodot, GodotConvert, ToGodot}; +#[cfg(feature = "codegen-full")] +pub use rpc_config::RpcConfig; pub use traits::{ArrayElement, GodotType, PackedArrayElement}; pub(crate) use crate::impl_godot_as_self; diff --git a/godot-core/src/meta/rpc_config.rs b/godot-core/src/meta/rpc_config.rs new file mode 100644 index 000000000..5a5b6311c --- /dev/null +++ b/godot-core/src/meta/rpc_config.rs @@ -0,0 +1,50 @@ +/* + * 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 crate::builtin::{Dictionary, StringName}; +use crate::classes::multiplayer_api::RpcMode; +use crate::classes::multiplayer_peer::TransferMode; +use crate::classes::Node; +use crate::dict; +use crate::meta::ToGodot; + +/// See [Godot documentation](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html#remote-procedure-calls) +#[derive(Copy, Clone, Debug)] +pub struct RpcConfig { + pub rpc_mode: RpcMode, + pub transfer_mode: TransferMode, + pub call_local: bool, + pub channel: u32, +} + +impl Default for RpcConfig { + fn default() -> Self { + Self { + rpc_mode: RpcMode::AUTHORITY, + transfer_mode: TransferMode::UNRELIABLE, + call_local: false, + channel: 0, + } + } +} + +impl RpcConfig { + /// Register `method` as a remote procedure call on `node`. + pub fn register(self, node: &mut Node, method: impl Into) { + node.rpc_config(method.into(), &self.into_dictionary().to_variant()); + } + + /// Returns a [`Dictionary`] populated with the values required for a call to [`Node::rpc_config`]. + pub fn into_dictionary(self) -> Dictionary { + dict! { + "rpc_mode": self.rpc_mode, + "transfer_mode": self.transfer_mode, + "call_local": self.call_local, + "channel": self.channel, + } + } +} diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index d6541936b..41ddd1d9d 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -455,6 +455,7 @@ pub mod cap { use super::*; use crate::builtin::{StringName, Variant}; use crate::obj::{Base, Bounds, Gd}; + use std::any::Any; /// Trait for all classes that are default-constructible from the Godot engine. /// @@ -558,6 +559,8 @@ pub mod cap { fn __register_methods(); #[doc(hidden)] fn __register_constants(); + #[doc(hidden)] + fn __register_rpcs(_: &mut dyn Any) {} } pub trait ImplementsGodotExports: GodotClass { diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index bc70ce33b..704668cf9 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -8,7 +8,9 @@ pub use crate::gen::classes::class_macros; pub use crate::obj::rtti::ObjectRtti; pub use crate::registry::callbacks; -pub use crate::registry::plugin::{ClassPlugin, ErasedRegisterFn, PluginItem}; +pub use crate::registry::plugin::{ + ClassPlugin, ErasedRegisterFn, ErasedRegisterRpcsFn, InherentImpl, PluginItem, +}; pub use crate::storage::{as_storage, Storage}; pub use sys::out; @@ -17,11 +19,10 @@ pub use crate::meta::trace; use crate::global::godot_error; use crate::meta::error::CallError; -use crate::meta::CallContext; +use crate::meta::{CallContext, ClassName}; use crate::sys; use std::sync::{atomic, Arc, Mutex}; use sys::Global; - // ---------------------------------------------------------------------------------------------------------------------------------------------- // Global variables @@ -128,6 +129,21 @@ pub(crate) fn iterate_plugins(mut visitor: impl FnMut(&ClassPlugin)) { sys::plugin_foreach!(__GODOT_PLUGIN_REGISTRY; visitor); } +pub(crate) fn find_inherent_impl(class_name: ClassName) -> Option { + // We do this manually instead of using `iterate_plugins()` because we want to break as soon as we find a match. + let plugins = __godot_rust_plugin___GODOT_PLUGIN_REGISTRY.lock().unwrap(); + + plugins.iter().find_map(|elem| { + if elem.class_name == class_name { + if let PluginItem::InherentImpl(inherent_impl) = &elem.item { + return Some(inherent_impl.clone()); + } + } + + None + }) +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Traits and types diff --git a/godot-core/src/registry/callbacks.rs b/godot-core/src/registry/callbacks.rs index 657389df8..d8fcc0411 100644 --- a/godot-core/src/registry/callbacks.rs +++ b/godot-core/src/registry/callbacks.rs @@ -354,3 +354,7 @@ pub fn register_user_methods_constants(_class_builde T::__register_methods(); T::__register_constants(); } + +pub fn register_user_rpcs(object: &mut dyn Any) { + T::__register_rpcs(object); +} diff --git a/godot-core/src/registry/class.rs b/godot-core/src/registry/class.rs index 3dd60cb38..68da32c63 100644 --- a/godot-core/src/registry/class.rs +++ b/godot-core/src/registry/class.rs @@ -13,7 +13,7 @@ use crate::meta::ClassName; use crate::obj::{cap, GodotClass}; use crate::private::{ClassPlugin, PluginItem}; use crate::registry::callbacks; -use crate::registry::plugin::ErasedRegisterFn; +use crate::registry::plugin::{ErasedRegisterFn, InherentImpl}; use crate::{godot_error, sys}; use sys::{interface_fn, out, Global, GlobalGuard, GlobalLockError}; @@ -71,7 +71,7 @@ impl ClassRegistrationInfo { // Note: when changing this match, make sure the array has sufficient size. let index = match item { PluginItem::Struct { .. } => 0, - PluginItem::InherentImpl { .. } => 1, + PluginItem::InherentImpl(InherentImpl { .. }) => 1, PluginItem::ITraitImpl { .. } => 2, }; @@ -200,6 +200,18 @@ pub fn unregister_classes(init_level: InitLevel) { } } +#[cfg(feature = "codegen-full")] +pub fn auto_register_rpcs(object: &mut T) { + // Find the element that matches our class, and call the closure if it exists. + if let Some(InherentImpl { + register_rpcs_fn: Some(closure), + .. + }) = crate::private::find_inherent_impl(T::class_name()) + { + (closure.raw)(object); + } +} + fn global_loaded_classes() -> GlobalGuard<'static, HashMap>> { match LOADED_CLASSES.try_lock() { Ok(it) => it, @@ -281,11 +293,12 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { } } - PluginItem::InherentImpl { + PluginItem::InherentImpl(InherentImpl { register_methods_constants_fn, + register_rpcs_fn: _, #[cfg(all(since_api = "4.3", feature = "docs"))] docs: _, - } => { + }) => { c.register_methods_constants_fn = Some(register_methods_constants_fn); } diff --git a/godot-core/src/registry/plugin.rs b/godot-core/src/registry/plugin.rs index 0453dfc1a..55971fcbc 100644 --- a/godot-core/src/registry/plugin.rs +++ b/godot-core/src/registry/plugin.rs @@ -12,7 +12,6 @@ use crate::meta::ClassName; use crate::sys; use std::any::Any; use std::fmt; - // TODO(bromeon): some information coming from the proc-macro API is deferred through PluginItem, while others is directly // translated to code. Consider moving more code to the PluginItem, which allows for more dynamic registration and will // be easier for a future builder API. @@ -45,6 +44,31 @@ impl fmt::Debug for ErasedRegisterFn { } } +#[derive(Copy, Clone)] +pub struct ErasedRegisterRpcsFn { + pub raw: fn(&mut dyn Any), +} + +impl fmt::Debug for ErasedRegisterRpcsFn { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x{:0>16x}", self.raw as usize) + } +} + +#[derive(Clone, Debug)] +pub struct InherentImpl { + /// Callback to library-generated function which registers functions and constants in the `impl` block. + /// + /// Always present since that's the entire point of this `impl` block. + pub register_methods_constants_fn: ErasedRegisterFn, + /// Callback to library-generated function which calls [`Node::rpc_config`](crate::classes::Node::rpc_config) for each function annotated with `#[rpc]` on the `impl` block. + /// + /// This function is called in [`UserClass::__before_ready()`](crate::obj::UserClass::__before_ready) definitions generated by the `#[derive(GodotClass)]` macro. + pub register_rpcs_fn: Option, + #[cfg(all(since_api = "4.3", feature = "docs"))] + pub docs: InherentImplDocs, +} + /// Represents the data part of a [`ClassPlugin`] instance. /// /// Each enumerator represents a different item in Rust code, which is processed by an independent proc macro (for example, @@ -102,14 +126,7 @@ pub enum PluginItem { }, /// Collected from `#[godot_api] impl MyClass`. - InherentImpl { - /// Callback to library-generated function which registers functions and constants in the `impl` block. - /// - /// Always present since that's the entire point of this `impl` block. - register_methods_constants_fn: ErasedRegisterFn, - #[cfg(all(since_api = "4.3", feature = "docs"))] - docs: InherentImplDocs, - }, + InherentImpl(InherentImpl), /// Collected from `#[godot_api] impl I... for MyClass`. ITraitImpl { diff --git a/godot-macros/Cargo.toml b/godot-macros/Cargo.toml index 6a422ebe2..fb3e170e5 100644 --- a/godot-macros/Cargo.toml +++ b/godot-macros/Cargo.toml @@ -13,6 +13,7 @@ homepage = "https://godot-rust.github.io" [features] api-custom = ["godot-bindings/api-custom"] docs = ["dep:markdown"] +codegen-full = [] [lib] proc-macro = true @@ -31,7 +32,7 @@ godot-bindings = { path = "../godot-bindings", version = "=0.1.3" } # emit_godot # Reverse dev dependencies so doctests can use `godot::` prefix. [dev-dependencies] -godot = { path = "../godot", default-features = false } +godot = { path = "../godot", default-features = false, features = ["__codegen-full"] } # https://docs.rs/about/metadata [package.metadata.docs.rs] diff --git a/godot-macros/src/class/data_models/field_var.rs b/godot-macros/src/class/data_models/field_var.rs index 93ad2dfca..3066713e9 100644 --- a/godot-macros/src/class/data_models/field_var.rs +++ b/godot-macros/src/class/data_models/field_var.rs @@ -204,6 +204,7 @@ impl GetterSetterImpl { external_attributes: Vec::new(), rename: None, is_script_virtual: false, + rpc_info: None, }, ); diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 2bf9de228..e19763636 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -5,6 +5,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use crate::class::RpcAttr; use crate::util::{bail_fn, ident, safe_ident}; use crate::{util, ParseResult}; use proc_macro2::{Group, Ident, TokenStream, TokenTree}; @@ -19,6 +20,8 @@ pub struct FuncDefinition { /// The name the function will be exposed as in Godot. If `None`, the Rust function name is used. pub rename: Option, pub is_script_virtual: bool, + /// Information about the RPC configuration, if provided. + pub rpc_info: Option, } /// Returns a C function which acts as the callback when a virtual method of this instance is invoked. diff --git a/godot-macros/src/class/data_models/inherent_impl.rs b/godot-macros/src/class/data_models/inherent_impl.rs index 25d4e9d14..5bde682aa 100644 --- a/godot-macros/src/class/data_models/inherent_impl.rs +++ b/godot-macros/src/class/data_models/inherent_impl.rs @@ -4,13 +4,13 @@ * 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 crate::class::{ into_signature_info, make_constant_registration, make_method_registration, - make_signal_registrations, ConstDefinition, FuncDefinition, SignalDefinition, SignatureInfo, + make_signal_registrations, ConstDefinition, FuncDefinition, RpcAttr, RpcMode, SignalDefinition, + SignatureInfo, TransferMode, }; -use crate::util::{bail, require_api_version, KvParser}; -use crate::{util, ParseResult}; +use crate::util::{bail, ident, require_api_version, KvParser}; +use crate::{handle_mutually_exclusive_keys, util, ParseResult}; use proc_macro2::{Delimiter, Group, Ident, TokenStream}; use quote::spanned::Spanned; @@ -18,18 +18,13 @@ use quote::{format_ident, quote}; /// Attribute for user-declared function. enum ItemAttrType { - Func { - rename: Option, - is_virtual: bool, - has_gd_self: bool, - }, + Func(FuncAttr, Option), Signal(venial::AttributeValue), Const(#[allow(dead_code)] venial::AttributeValue), } struct ItemAttr { attr_name: Ident, - index: usize, ty: ItemAttrType, } @@ -39,6 +34,34 @@ impl ItemAttr { } } +enum AttrParseResult { + Func(FuncAttr), + Rpc(RpcAttr), + FuncRpc(FuncAttr, RpcAttr), + Signal(venial::AttributeValue), + Const(#[allow(dead_code)] venial::AttributeValue), +} + +impl AttrParseResult { + fn into_attr_ty(self) -> ItemAttrType { + match self { + AttrParseResult::Func(func) => ItemAttrType::Func(func, None), + // If only `#[rpc]` is present, we assume #[func] with default values. + AttrParseResult::Rpc(rpc) => ItemAttrType::Func(FuncAttr::default(), Some(rpc)), + AttrParseResult::FuncRpc(func, rpc) => ItemAttrType::Func(func, Some(rpc)), + AttrParseResult::Signal(signal) => ItemAttrType::Signal(signal), + AttrParseResult::Const(constant) => ItemAttrType::Const(constant), + } + } +} + +#[derive(Default)] +struct FuncAttr { + pub rename: Option, + pub is_virtual: bool, + pub has_gd_self: bool, +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- /// Codegen for `#[godot_api] impl MyType` @@ -58,6 +81,11 @@ pub fn transform_inherent_impl(mut impl_block: venial::Impl) -> ParseResult = funcs .into_iter() .map(|func_def| make_method_registration(&class_name, func_def)) @@ -77,16 +105,21 @@ 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, }); }; @@ -108,13 +141,10 @@ fn process_godot_fns( continue; }; - let Some(attr) = extract_attributes(&function, &function.attributes)? else { + let Some(attr) = extract_attributes(function)? else { continue; }; - // Remaining code no longer has attribute -- rest stays - function.attributes.remove(attr.index); - if function.qualifiers.tk_default.is_some() || function.qualifiers.tk_const.is_some() || function.qualifiers.tk_async.is_some() @@ -130,16 +160,12 @@ fn process_godot_fns( } match attr.ty { - ItemAttrType::Func { - rename, - is_virtual, - has_gd_self, - } => { + ItemAttrType::Func(func, rpc_info) => { let external_attributes = function.attributes.clone(); // Signatures are the same thing without body. let mut signature = util::reduce_to_signature(function); - let gd_self_parameter = if has_gd_self { + let gd_self_parameter = if func.has_gd_self { if signature.params.is_empty() { return bail_attr( attr.attr_name, @@ -170,13 +196,13 @@ fn process_godot_fns( // For virtual methods, rename/mangle existing user method and create a new method with the original name, // which performs a dynamic dispatch. - if is_virtual { + if func.is_virtual { add_virtual_script_call( &mut virtual_functions, function, &signature_info, class_name, - &rename, + &func.rename, gd_self_parameter, ); }; @@ -184,8 +210,9 @@ fn process_godot_fns( func_definitions.push(FuncDefinition { signature_info, external_attributes, - rename, - is_script_virtual: is_virtual, + rename: func.rename, + is_script_virtual: func.is_virtual, + rpc_info, }); } ItemAttrType::Signal(ref _attr_val) => { @@ -235,13 +262,10 @@ fn process_godot_constants(decl: &mut venial::Impl) -> ParseResult { - return bail!(constant, "#[func] can only be used on functions") + ItemAttrType::Func(_, _) => { + return bail!(constant, "#[func] and #[rpc] can only be used on functions") } ItemAttrType::Signal(_) => { return bail!(constant, "#[signal] can only be used on functions") @@ -300,7 +324,7 @@ fn add_virtual_script_call( receiver = gd_self_parameter; } else { object_ptr = quote! { ::base_field(self).obj_sys() }; - receiver = util::ident("self"); + receiver = ident("self"); }; let code = quote! { @@ -338,21 +362,26 @@ fn add_virtual_script_call( virtual_functions.push(early_bound_function); } -fn extract_attributes( - error_scope: T, - attributes: &[venial::Attribute], -) -> ParseResult> +fn extract_attributes(item: &mut T) -> ParseResult> where for<'a> &'a T: Spanned, + T: AttributesMut, { + // Option<(attr_name: Ident, attr: ParsedAttr)> let mut found = None; - for (index, attr) in attributes.iter().enumerate() { + let mut index = 0; + + let attributes = item.attributes_mut(); + + while let Some(attr) = attributes.get(index) { + index += 1; + let Some(attr_name) = attr.get_single_path_segment() else { // Attribute of the form #[segmented::path] can't be what we are looking for continue; }; - let new_found = match attr_name { + let parsed_attr = match attr_name { // #[func] name if name == "func" => { // Safe unwrap since #[func] must be present if we got to this point @@ -374,15 +403,60 @@ where parser.finish()?; - ItemAttr { - attr_name: attr_name.clone(), - index, - ty: ItemAttrType::Func { - rename, - is_virtual, - has_gd_self, - }, - } + AttrParseResult::Func(FuncAttr { + rename, + is_virtual, + has_gd_self, + }) + } + + // #[rpc] + name if name == "rpc" => { + // Safe unwrap since #[rpc] must be present if we got to this point + let mut parser = KvParser::parse(attributes, "rpc")?.unwrap(); + + let rpc_mode = handle_mutually_exclusive_keys( + &mut parser, + "#[rpc]", + &["any_peer", "authority"], + )? + .map(|idx| RpcMode::from_usize(idx).unwrap()); + + let transfer_mode = handle_mutually_exclusive_keys( + &mut parser, + "#[rpc]", + &["reliable", "unreliable", "unreliable_ordered"], + )? + .map(|idx| TransferMode::from_usize(idx).unwrap()); + + let call_local = handle_mutually_exclusive_keys( + &mut parser, + "#[rpc]", + &["call_local", "call_remote"], + )? + .map(|idx| idx == 0); + + let channel = parser.handle_usize("channel")?.map(|x| x as u32); + + let config_expr = parser.handle_expr("config")?; + + parser.finish()?; + + let rpc_attr = match (config_expr, (&rpc_mode, &transfer_mode, &call_local, &channel)) { + // Ok: Only `config = [expr]` is present. + (Some(expr), (None, None, None, None)) => RpcAttr::Expression(expr), + // Err: `config = [expr]` is present along other parameters, which is not allowed. + (Some(_), _) => return bail!(&*item, "`#[rpc(config = ...)]` is mutually exclusive with any other parameters(`any_peer`, `reliable`, `call_local`, `channel = 0`)"), + // Ok: `config` is not present, any combination of the other parameters is allowed.. + _ => RpcAttr::SeparatedArgs { + rpc_mode, + transfer_mode, + call_local, + channel, + } + }; + + AttrParseResult::Rpc(rpc_attr) } // #[signal] @@ -390,38 +464,62 @@ where // TODO once parameters are supported, this should probably be moved to the struct definition // E.g. a zero-sized type Signal<(i32, String)> with a provided emit(i32, String) method // This could even be made public (callable on the struct obj itself) - ItemAttr { - attr_name: attr_name.clone(), - index, - ty: ItemAttrType::Signal(attr.value.clone()), - } + AttrParseResult::Signal(attr.value.clone()) } // #[constant] - name if name == "constant" => ItemAttr { - attr_name: attr_name.clone(), - index, - ty: ItemAttrType::Const(attr.value.clone()), - }, + name if name == "constant" => AttrParseResult::Const(attr.value.clone()), // Ignore unknown attributes. _ => continue, }; - // Ensure at most 1 attribute. - if found.is_some() { - bail!( - &error_scope, - "at most one #[func], #[signal] or #[constant] attribute per declaration allowed", - )?; - } + let attr_name = attr_name.clone(); - found = Some(new_found); + // Remaining code no longer has attribute -- rest stays + attributes.remove(index - 1); // -1 because we bumped the index at the beginning of the loop + index -= 1; + + let (new_name, new_attr) = match (found, parsed_attr) { + // First attribute + (None, parsed) => (attr_name, parsed), + // Regardless of the order, if we found both `#[func]` and `#[rpc]`, we can just merge them. + (Some((found_name, AttrParseResult::Func(func))), AttrParseResult::Rpc(rpc)) + | (Some((found_name, AttrParseResult::Rpc(rpc))), AttrParseResult::Func(func)) => ( + ident(&format!("{found_name}_{attr_name}")), + AttrParseResult::FuncRpc(func, rpc), + ), + // We found two incompatible attributes. + (Some((found_name, _)), _) => { + return bail!(&*item, "The attributes `{found_name}` and `{attr_name}` cannot be used in the same declaration")?; + } + }; + + found = Some((new_name, new_attr)); } - Ok(found) + Ok(found.map(|(attr_name, attr)| ItemAttr { + attr_name, + ty: attr.into_attr_ty(), + })) } fn bail_attr(attr_name: Ident, msg: &str, method: &venial::Function) -> ParseResult { bail!(&method.name, "#[{}]: {}", attr_name, msg) } + +trait AttributesMut { + fn attributes_mut(&mut self) -> &mut Vec; +} + +impl AttributesMut for venial::Function { + fn attributes_mut(&mut self) -> &mut Vec { + &mut self.attributes + } +} + +impl AttributesMut for venial::Constant { + fn attributes_mut(&mut self) -> &mut Vec { + &mut self.attributes + } +} diff --git a/godot-macros/src/class/data_models/rpc.rs b/godot-macros/src/class/data_models/rpc.rs new file mode 100644 index 000000000..d38218867 --- /dev/null +++ b/godot-macros/src/class/data_models/rpc.rs @@ -0,0 +1,162 @@ +/* + * 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 crate::class::FuncDefinition; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; + +/// Possible ways the user can specify RPC configuration. +pub enum RpcAttr { + // Individual keys in the `rpc` attribute. + // Example: `#[rpc(any_peer, reliable, call_remote, channel = 3)]` + SeparatedArgs { + rpc_mode: Option, + transfer_mode: Option, + call_local: Option, + channel: Option, + }, + // `args` key in the `rpc` attribute. + // Example: + // const RPC_CFG: RpcConfig = RpcConfig { mode: RpcMode::Authority, ..RpcConfig::default() }; + // #[rpc(config = RPC_CFG)] + Expression(TokenStream), +} + +#[derive(Clone, Copy)] +pub enum RpcMode { + AnyPeer, + Authority, +} + +impl RpcMode { + pub fn from_usize(value: usize) -> Option { + match value { + 0 => Some(RpcMode::AnyPeer), + 1 => Some(RpcMode::Authority), + _ => None, + } + } +} + +#[derive(Clone, Copy)] +pub enum TransferMode { + Reliable, + Unreliable, + UnreliableOrdered, +} + +impl TransferMode { + pub fn from_usize(value: usize) -> Option { + match value { + 0 => Some(TransferMode::Reliable), + 1 => Some(TransferMode::Unreliable), + 2 => Some(TransferMode::UnreliableOrdered), + _ => None, + } + } +} + +pub fn make_rpc_registrations_fn(class_name: &Ident, funcs: &[FuncDefinition]) -> TokenStream { + let rpc_registrations = funcs + .iter() + .filter_map(make_rpc_registration) + .collect::>(); + + // This check is necessary because the class might not implement `WithBaseField` or `Inherits`, + // which means `to_gd` wouldn't exist or the trait bounds on `RpcConfig::register` wouldn't be satisfied. + if rpc_registrations.is_empty() { + return TokenStream::new(); + } + + quote! { + #[allow(clippy::needless_update)] // clippy complains about using `..RpcConfig::default()` if all fields are overridden + fn __register_rpcs(object: &mut dyn ::std::any::Any) { + use ::std::any::Any; + use ::godot::meta::RpcConfig; + use ::godot::classes::multiplayer_api::RpcMode; + use ::godot::classes::multiplayer_peer::TransferMode; + use ::godot::classes::Node; + use ::godot::obj::{WithBaseField, Gd}; + + let mut gd = object + .downcast_mut::<#class_name>() + .expect("bad type erasure when registering RPCs") + .to_gd(); + + let node = gd.upcast_mut::(); + #( #rpc_registrations )* + } + } +} + +fn make_rpc_registration(func_def: &FuncDefinition) -> Option { + let rpc_info = func_def.rpc_info.as_ref()?; + + let create_struct = match rpc_info { + RpcAttr::SeparatedArgs { + rpc_mode, + transfer_mode, + call_local, + channel, + } => { + let override_rpc_mode = rpc_mode.map(|mode| { + let token = match mode { + RpcMode::Authority => quote! { RpcMode::AUTHORITY }, + RpcMode::AnyPeer => quote! { RpcMode::ANY_PEER }, + }; + + quote! { rpc_mode: #token, } + }); + + let override_transfer_mode = transfer_mode.map(|mode| { + let token = match mode { + TransferMode::Reliable => quote! { TransferMode::RELIABLE }, + TransferMode::Unreliable => quote! { TransferMode::UNRELIABLE }, + TransferMode::UnreliableOrdered => quote! { TransferMode::UNRELIABLE_ORDERED }, + }; + + quote! { transfer_mode: #token, } + }); + + let override_call_local = call_local.map(|call_local| { + quote! { call_local: #call_local, } + }); + + let override_channel = channel.map(|channel| { + quote! { channel: #channel, } + }); + + quote! { + let args = RpcConfig { + #override_rpc_mode + #override_transfer_mode + #override_call_local + #override_channel + ..RpcConfig::default() + }; + } + } + RpcAttr::Expression(expr) => { + quote! { let args = #expr; } + } + }; + + let method_name_str = if let Some(rename) = &func_def.rename { + rename.to_string() + } else { + func_def.signature_info.method_name.to_string() + }; + + let registration = quote! { + { + #create_struct + args.register(node, #method_name_str) + } + }; + + Some(registration) +} diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index 32d66ccb8..55ef01a96 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -13,7 +13,7 @@ use crate::class::{ SignatureInfo, }; use crate::util::{bail, ident, path_ends_with_complex, require_api_version, KvParser}; -use crate::{util, ParseResult}; +use crate::{handle_mutually_exclusive_keys, util, ParseResult}; pub fn derive_godot_class(item: venial::Item) -> ParseResult { let class = item @@ -255,6 +255,12 @@ fn make_user_class_impl( is_tool: bool, all_fields: &[Field], ) -> (TokenStream, bool) { + #[cfg(feature = "codegen-full")] + let rpc_registrations = + quote! { ::godot::register::private::auto_register_rpcs::<#class_name>(self); }; + #[cfg(not(feature = "codegen-full"))] + let rpc_registrations = TokenStream::new(); + let onready_inits = { let mut onready_fields = all_fields .iter() @@ -310,6 +316,7 @@ fn make_user_class_impl( } fn __before_ready(&mut self) { + #rpc_registrations #onready_inits } @@ -555,18 +562,12 @@ fn handle_opposite_keys( attribute: &str, ) -> ParseResult> { let antikey = format!("no_{}", key); + let result = handle_mutually_exclusive_keys(parser, attribute, &[key, &antikey])?; - let is_key = parser.handle_alone(key)?; - let is_no_key = parser.handle_alone(&antikey)?; - - match (is_key, is_no_key) { - (true, false) => Ok(Some(true)), - (false, true) => Ok(Some(false)), - (false, false) => Ok(None), - (true, true) => bail!( - parser.span(), - "#[{attribute}] attribute keys `{key}` and `{antikey}` are mutually exclusive", - ), + if let Some(idx) = result { + Ok(Some(idx == 0)) + } else { + Ok(None) } } diff --git a/godot-macros/src/class/mod.rs b/godot-macros/src/class/mod.rs index 22603268f..7f800c137 100644 --- a/godot-macros/src/class/mod.rs +++ b/godot-macros/src/class/mod.rs @@ -16,6 +16,8 @@ mod data_models { pub mod inherent_impl; pub mod interface_trait_impl; pub mod property; + #[cfg_attr(not(feature = "codegen-full"), allow(dead_code))] + pub mod rpc; pub mod signal; } @@ -27,6 +29,7 @@ pub(crate) use data_models::func::*; pub(crate) use data_models::inherent_impl::*; pub(crate) use data_models::interface_trait_impl::*; pub(crate) use data_models::property::*; +pub(crate) use data_models::rpc::*; pub(crate) use data_models::signal::*; pub(crate) use derive_godot_class::*; pub(crate) use godot_api::*; diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index a9b5ae833..414b77176 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -23,7 +23,7 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; -use crate::util::ident; +use crate::util::{bail, ident, KvParser}; // Below intra-doc link to the trait only works as HTML, not as symbol link. /// Derive macro for [`GodotClass`](../obj/trait.GodotClass.html) on structs. @@ -903,3 +903,41 @@ where TokenStream::from(result2) } + +/// Returns the index of the key in `keys` (if any) that is present. +fn handle_mutually_exclusive_keys( + parser: &mut KvParser, + attribute: &str, + keys: &[&str], +) -> ParseResult> { + let (oks, errs) = keys + .iter() + .enumerate() + .map(|(idx, key)| Ok(parser.handle_alone(key)?.then_some(idx))) + .partition::, _>(|result: &ParseResult>| result.is_ok()); + + if !errs.is_empty() { + return bail!(parser.span(), "{errs:?}"); + } + + let found_idxs = oks + .into_iter() + .filter_map(|r| r.unwrap()) // `partition` guarantees that this is `Ok` + .collect::>(); + + match found_idxs.len() { + 0 => Ok(None), + 1 => Ok(Some(found_idxs[0])), + _ => { + let offending_keys = keys + .iter() + .enumerate() + .filter(|(idx, _)| found_idxs.contains(idx)); + + bail!( + parser.span(), + "{attribute} attribute keys {offending_keys:?} are mutually exclusive" + ) + } + } +} diff --git a/godot/Cargo.toml b/godot/Cargo.toml index 6796e6f6b..e3aa46a6a 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -43,7 +43,7 @@ api-4-3 = ["godot-core/api-4-3"] default = ["__codegen-full"] # Private features, they are under no stability guarantee -__codegen-full = ["godot-core/codegen-full"] +__codegen-full = ["godot-core/codegen-full", "godot-macros/codegen-full"] __debug-log = ["godot-core/debug-log"] __trace = ["godot-core/trace"] diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 8eada3e91..62bebfd08 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -178,6 +178,8 @@ pub mod register { /// Re-exports used by proc-macro API. #[doc(hidden)] pub mod private { + #[cfg(feature = "__codegen-full")] + pub use godot_core::registry::class::auto_register_rpcs; pub use godot_core::registry::godot_register_wrappers::*; pub use godot_core::registry::{constant, method}; } diff --git a/itest/rust/src/register_tests/constant_test.rs b/itest/rust/src/register_tests/constant_test.rs index d7b25f9c8..346ee3a32 100644 --- a/itest/rust/src/register_tests/constant_test.rs +++ b/itest/rust/src/register_tests/constant_test.rs @@ -171,13 +171,14 @@ godot::sys::plugin_add!( __GODOT_PLUGIN_REGISTRY in ::godot::private; ::godot::private::ClassPlugin { class_name: HasOtherConstants::class_name(), - item: ::godot::private::PluginItem::InherentImpl { + item: ::godot::private::PluginItem::InherentImpl(::godot::private::InherentImpl { register_methods_constants_fn: ::godot::private::ErasedRegisterFn { raw: ::godot::private::callbacks::register_user_methods_constants::, }, + register_rpcs_fn: None, #[cfg(all(since_api = "4.3", feature = "register-docs"))] docs: ::godot::docs::InherentImplDocs::default(), - }, + }), init_level: HasOtherConstants::INIT_LEVEL, } ); diff --git a/itest/rust/src/register_tests/mod.rs b/itest/rust/src/register_tests/mod.rs index 92b8a8f76..14c586622 100644 --- a/itest/rust/src/register_tests/mod.rs +++ b/itest/rust/src/register_tests/mod.rs @@ -12,6 +12,8 @@ mod func_test; mod gdscript_ffi_test; mod naming_tests; mod option_ffi_test; +#[cfg(feature = "codegen-full")] +mod rpc_test; mod var_test; #[cfg(since_api = "4.3")] diff --git a/itest/rust/src/register_tests/rpc_test.rs b/itest/rust/src/register_tests/rpc_test.rs new file mode 100644 index 000000000..e2e62b2cf --- /dev/null +++ b/itest/rust/src/register_tests/rpc_test.rs @@ -0,0 +1,90 @@ +/* + * 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::multiplayer_api::RpcMode; +use godot::classes::multiplayer_peer::TransferMode; +use godot::classes::{Engine, MultiplayerApi}; +use godot::meta::RpcConfig; +use godot::prelude::*; +use godot::test::itest; + +#[derive(GodotClass)] +#[class(init, base = Node2D)] +pub struct RpcTest { + base: Base, +} + +const CACHED_CFG: RpcConfig = RpcConfig { + rpc_mode: RpcMode::AUTHORITY, + transfer_mode: TransferMode::RELIABLE, + call_local: false, + channel: 1, +}; + +#[godot_api] +impl RpcTest { + #[rpc] + pub fn default_args(&mut self) {} + + #[rpc(any_peer)] + pub fn arg_any_peer(&mut self) {} + + #[rpc(authority)] + pub fn arg_authority(&mut self) {} + + #[rpc(reliable)] + pub fn arg_reliable(&mut self) {} + + #[rpc(unreliable)] + pub fn arg_unreliable(&mut self) {} + + #[rpc(unreliable_ordered)] + pub fn arg_unreliable_ordered(&mut self) {} + + #[rpc(call_local)] + pub fn arg_call_local(&mut self) {} + + #[rpc(call_remote)] + pub fn arg_call_remote(&mut self) {} + + #[rpc(channel = 2)] + pub fn arg_channel(&mut self) {} + + #[rpc(any_peer, reliable, call_remote, channel = 2)] + pub fn all_args(&mut self) {} + + #[rpc(reliable, any_peer)] + #[func] + pub fn args_func(&mut self) {} + + #[rpc(unreliable)] + #[func(gd_self)] + pub fn args_func_gd_self(_this: Gd) {} + + #[rpc(config = CACHED_CFG)] + pub fn arg_config(&mut self) {} +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Tests + +// There's no way to check if the method was registered as an RPC. +// We could set up a multiplayer environment to test this in practice, but that would be a lot of work. +#[itest] +fn node_enters_tree() { + let node = RpcTest::new_alloc(); + + // Registering is done in `UserClass::__before_ready()`, and it requires a multiplayer api to exist. + let mut scene_tree = Engine::singleton() + .get_main_loop() + .unwrap() + .cast::(); + scene_tree.set_multiplayer(MultiplayerApi::create_default_interface()); + let mut root = scene_tree.get_root().unwrap(); + root.add_child(&node); + root.remove_child(&node); + node.free(); +} From 2ae2995be7d765517a44f05758454f6a63e541c7 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 22 Sep 2024 10:00:48 +0200 Subject: [PATCH 2/2] Follow-up changes to RpcConfig + #[cfg] + clippy --- godot-core/src/meta/mod.rs | 3 +-- godot-core/src/meta/rpc_config.rs | 12 +++++++----- godot-core/src/private.rs | 5 +++-- godot-core/src/registry/class.rs | 2 +- godot-macros/Cargo.toml | 4 ++-- godot-macros/src/class/data_models/rpc.rs | 2 +- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/godot-core/src/meta/mod.rs b/godot-core/src/meta/mod.rs index 138ab2f68..4945d2f73 100644 --- a/godot-core/src/meta/mod.rs +++ b/godot-core/src/meta/mod.rs @@ -40,8 +40,7 @@ mod godot_convert; mod method_info; mod property_info; mod ref_arg; -// RpcConfig uses `MultiplayerPeer::TransferMode` and `MultiplayerApi::RpcMode`, -// which are only available when `codegen-full` is enabled. +// RpcConfig uses MultiplayerPeer::TransferMode and MultiplayerApi::RpcMode, which are only enabled in `codegen-full` feature. #[cfg(feature = "codegen-full")] mod rpc_config; mod sealed; diff --git a/godot-core/src/meta/rpc_config.rs b/godot-core/src/meta/rpc_config.rs index 5a5b6311c..5cb7d4b0c 100644 --- a/godot-core/src/meta/rpc_config.rs +++ b/godot-core/src/meta/rpc_config.rs @@ -12,7 +12,9 @@ use crate::classes::Node; use crate::dict; use crate::meta::ToGodot; -/// See [Godot documentation](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html#remote-procedure-calls) +/// Configuration for a remote procedure call, typically used with `#[rpc(config = ...)]`. +/// +/// See [Godot documentation](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html#remote-procedure-calls). #[derive(Copy, Clone, Debug)] pub struct RpcConfig { pub rpc_mode: RpcMode, @@ -34,12 +36,12 @@ impl Default for RpcConfig { impl RpcConfig { /// Register `method` as a remote procedure call on `node`. - pub fn register(self, node: &mut Node, method: impl Into) { - node.rpc_config(method.into(), &self.into_dictionary().to_variant()); + pub fn configure_node(self, node: &mut Node, method_name: impl Into) { + node.rpc_config(method_name.into(), &self.to_dictionary().to_variant()); } - /// Returns a [`Dictionary`] populated with the values required for a call to [`Node::rpc_config`]. - pub fn into_dictionary(self) -> Dictionary { + /// Returns a [`Dictionary`] populated with the values required for a call to [`Node::rpc_config()`]. + pub fn to_dictionary(&self) -> Dictionary { dict! { "rpc_mode": self.rpc_mode, "transfer_mode": self.transfer_mode, diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index 704668cf9..543c2b943 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -19,7 +19,7 @@ pub use crate::meta::trace; use crate::global::godot_error; use crate::meta::error::CallError; -use crate::meta::{CallContext, ClassName}; +use crate::meta::CallContext; use crate::sys; use std::sync::{atomic, Arc, Mutex}; use sys::Global; @@ -129,7 +129,8 @@ pub(crate) fn iterate_plugins(mut visitor: impl FnMut(&ClassPlugin)) { sys::plugin_foreach!(__GODOT_PLUGIN_REGISTRY; visitor); } -pub(crate) fn find_inherent_impl(class_name: ClassName) -> Option { +#[cfg(feature = "codegen-full")] // Remove if used in other scenarios. +pub(crate) fn find_inherent_impl(class_name: crate::meta::ClassName) -> Option { // We do this manually instead of using `iterate_plugins()` because we want to break as soon as we find a match. let plugins = __godot_rust_plugin___GODOT_PLUGIN_REGISTRY.lock().unwrap(); diff --git a/godot-core/src/registry/class.rs b/godot-core/src/registry/class.rs index 68da32c63..0a51785fb 100644 --- a/godot-core/src/registry/class.rs +++ b/godot-core/src/registry/class.rs @@ -71,7 +71,7 @@ impl ClassRegistrationInfo { // Note: when changing this match, make sure the array has sufficient size. let index = match item { PluginItem::Struct { .. } => 0, - PluginItem::InherentImpl(InherentImpl { .. }) => 1, + PluginItem::InherentImpl(_) => 1, PluginItem::ITraitImpl { .. } => 2, }; diff --git a/godot-macros/Cargo.toml b/godot-macros/Cargo.toml index fb3e170e5..bc0cd28d5 100644 --- a/godot-macros/Cargo.toml +++ b/godot-macros/Cargo.toml @@ -13,7 +13,7 @@ homepage = "https://godot-rust.github.io" [features] api-custom = ["godot-bindings/api-custom"] docs = ["dep:markdown"] -codegen-full = [] +codegen-full = ["godot/__codegen-full"] [lib] proc-macro = true @@ -32,7 +32,7 @@ godot-bindings = { path = "../godot-bindings", version = "=0.1.3" } # emit_godot # Reverse dev dependencies so doctests can use `godot::` prefix. [dev-dependencies] -godot = { path = "../godot", default-features = false, features = ["__codegen-full"] } +godot = { path = "../godot", default-features = false} # https://docs.rs/about/metadata [package.metadata.docs.rs] diff --git a/godot-macros/src/class/data_models/rpc.rs b/godot-macros/src/class/data_models/rpc.rs index d38218867..442b4c88b 100644 --- a/godot-macros/src/class/data_models/rpc.rs +++ b/godot-macros/src/class/data_models/rpc.rs @@ -154,7 +154,7 @@ fn make_rpc_registration(func_def: &FuncDefinition) -> Option { let registration = quote! { { #create_struct - args.register(node, #method_name_str) + args.configure_node(node, #method_name_str) } };