Skip to content

Commit

Permalink
#[rpc] annotation for functions in #[godot_api] inherent impl blocks.
Browse files Browse the repository at this point in the history
The style is similar to GDScript's @rpc annotation, the macro can be used as follows:

godot-rust#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.

godot-rust#2 - Provide an expression:
```rust
const CONFIG: RpcArgs = RpcArgs {
    mode: RpcMode::Authority,
    ..RpcArgs::default()
};

#[rpc(config = CONFIG_EXPR)]
fn some_rpc(&mut self) {
    //..
}
```

Number godot-rust#2 is useful in case you want to reuse the configuration on multiple functions.

Number godot-rust#2 is mutually exclusive with number godot-rust#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().
  • Loading branch information
Houtamelo committed Sep 19, 2024
1 parent f33fe1f commit 7aabeaf
Show file tree
Hide file tree
Showing 21 changed files with 550 additions and 41 deletions.
4 changes: 2 additions & 2 deletions godot-core/src/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -77,7 +77,7 @@ pub fn gather_xml_docs() -> impl Iterator<Item = String> {
let class_name = x.class_name;

match x.item {
PluginItem::InherentImpl { docs, .. } => {
PluginItem::InherentImpl(InherentImpl { docs, .. }) => {
map.entry(class_name).or_default().inherent = docs
}

Expand Down
4 changes: 4 additions & 0 deletions godot-core/src/meta/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,17 @@ mod godot_convert;
mod method_info;
mod property_info;
mod ref_arg;
#[cfg(feature = "codegen-full")]
mod rpc_config;
mod sealed;
mod signature;
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;
Expand Down
50 changes: 50 additions & 0 deletions godot-core/src/meta/rpc_config.rs
Original file line number Diff line number Diff line change
@@ -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(Clone, Copy, Debug)]
pub struct RpcConfig {
pub mode: RpcMode,
pub transfer_mode: TransferMode,
pub call_local: bool,
pub transfer_channel: u32,
}

impl Default for RpcConfig {
fn default() -> Self {
Self {
mode: RpcMode::AUTHORITY,
transfer_mode: TransferMode::UNRELIABLE,
call_local: false,
transfer_channel: 0,
}
}
}

impl RpcConfig {
/// Register `method` as a remote procedure call on `node`.
pub fn register(self, node: &mut Node, method: impl Into<StringName>) {
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! {
"mode": self.mode,
"transfer_mode": self.transfer_mode,
"call_local": self.call_local,
"transfer_channel": self.transfer_channel,
}
}
}
3 changes: 3 additions & 0 deletions godot-core/src/obj/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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 {
Expand Down
22 changes: 19 additions & 3 deletions godot-core/src/private.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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

Expand Down Expand Up @@ -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<InherentImpl> {
// 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

Expand Down
4 changes: 4 additions & 0 deletions godot-core/src/registry/callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,7 @@ pub fn register_user_methods_constants<T: cap::ImplementsGodotApi>(_class_builde
T::__register_methods();
T::__register_constants();
}

pub fn register_user_rpcs<T: cap::ImplementsGodotApi>(object: &mut dyn Any) {
T::__register_rpcs(object);
}
21 changes: 17 additions & 4 deletions godot-core/src/registry/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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,
};

Expand Down Expand Up @@ -200,6 +200,18 @@ pub fn unregister_classes(init_level: InitLevel) {
}
}

#[cfg(feature = "codegen-full")]
pub fn auto_register_rpcs<T: GodotClass>(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<InitLevel, Vec<LoadedClass>>> {
match LOADED_CLASSES.try_lock() {
Ok(it) => it,
Expand Down Expand Up @@ -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);
}

Expand Down
35 changes: 26 additions & 9 deletions godot-core/src/registry/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<ErasedRegisterRpcsFn>,
#[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,
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion godot-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down
1 change: 1 addition & 0 deletions godot-macros/src/class/data_models/field_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ impl GetterSetterImpl {
external_attributes: Vec::new(),
rename: None,
is_script_virtual: false,
rpc_info: None,
},
);

Expand Down
3 changes: 3 additions & 0 deletions godot-macros/src/class/data_models/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use crate::class::RpcInfo;
use crate::util::{bail_fn, ident, safe_ident};
use crate::{util, ParseResult};
use proc_macro2::{Group, Ident, TokenStream, TokenTree};
Expand All @@ -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<String>,
pub is_script_virtual: bool,
/// Information about the RPC configuration, if provided.
pub rpc_info: Option<RpcInfo>,
}

/// Returns a C function which acts as the callback when a virtual method of this instance is invoked.
Expand Down
Loading

0 comments on commit 7aabeaf

Please sign in to comment.