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

Add #[rpc] attribute to user-defined functions #902

Merged
merged 2 commits into from
Sep 22, 2024

Conversation

Houtamelo
Copy link
Contributor

@Houtamelo Houtamelo commented Sep 19, 2024

The style is similar to GDScript's @rpc annotation, the macro can be used as follows:

1 - Separate arguments:

#[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:

const CONFIG: RpcArgs = RpcArgs {
    mode: RpcMode::Authority,
    ..RpcArgs::default()
};

#[rpc(config = CONFIG)]
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.

You can use the #[func] attribute in conjunction with #[rpc]. If #[func] is not present (but #[rpc] is), then #[func] is implied with default arguments.

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().

Edit: the enums RpcMode and TransferMode are only available if the feature __codegen_full is enabled, so I had to feature-gate everything related to the implementation behind that feature.

@Bromeon Bromeon linked an issue Sep 19, 2024 that may be closed by this pull request
@Bromeon Bromeon added feature Adds functionality to the library c: register Register classes, functions and other symbols to GDScript labels Sep 19, 2024
@GodotRust
Copy link

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-902

Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot, this looks great! 😊

I would maybe rename RpcInfo to RpcConfig, just so it's in line with #[rpc(config = ...)] and Node::rpc_config().

use crate::meta::ToGodot;

/// See [Godot documentation](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html#remote-procedure-calls)
#[derive(Debug, Clone, Copy)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[derive(Debug, Clone, Copy)]
#[derive(Copy, Clone, Debug)]

Just canonical order 🙂

Comment on lines 43 to 67
pub fn make_rpc_registrations_fn(
class_name: &Ident,
funcs: &mut [FuncDefinition],
) -> Option<TokenStream> {
let rpc_registrations = funcs
.iter_mut()
.filter_map(make_rpc_registration)
.collect::<Vec<TokenStream>>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit unusual that filter_map modifies the element, even though not forbidden.

Is the only write operation you need this, or also something else?

let rpc_info = func_def.rpc_info.take()?;

Does this have to be a take()? What if you just read the element?

Keeping things immutable is nice because it allows multiple borrows (in future code refactors) and makes immediately obvious that the argument isn't changed. With the above mutable parameter funcs: &mut [FuncDefinition], the first impression is "this changes the function signature".


let mut gd = object
.downcast_mut::<#class_name>()
.expect("bad type erasure")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context is helpful:

Suggested change
.expect("bad type erasure")
.expect("bad type erasure when registering RPCs")

pub enum RpcInfo {
// Individual keys in the `rpc` attribute.
// Example: `#[rpc(any_peer, reliable, call_remote, channel = 3)]`
SeparatedArgs(RpcSeparatedArgs),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I'm missing something, it doesn't look like you benefit from having an actual struct here, so could you inline the 4 fields into the enum variant?

I'm not worried about the future, it's a 1-click IDE operation to extract to a struct, should we ever need the flexibility.

// Ok: Only `args = [expr]` is present.
(Some(expr), (None, None, None, None)) => RpcInfo::Expression(expr),
// Err: `args = [expr]` is present along other parameters, which is not allowed.
(Some(_), _) => return bail!(&error_scope, "`#[rpc(config = [expr])]` is mutually exclusive with any other parameters(`any_peer`, `reliable`, `call_local`, `channel = 0`)"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(Some(_), _) => return bail!(&error_scope, "`#[rpc(config = [expr])]` is mutually exclusive with any other parameters(`any_peer`, `reliable`, `call_local`, `channel = 0`)"),
(Some(_), _) => return bail!(&error_scope, "`#[rpc(config = ...)]` is mutually exclusive with any other parameters(`any_peer`, `reliable`, `call_local`, `channel = 0`)"),

Comment on lines 560 to 528
ty: ItemAttrType::Func {
rename: None,
is_virtual: false,
has_gd_self: false,
rpc_info: Some(rpc_info),
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would probably be good to implement default() here, which represents the #[func] attribute without keys.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would require extracting the enum variant into a struct, would you like me to do that?

Comment on lines 110 to 116
let token = if call_local {
quote! { true }
} else {
quote! { false }
};

quote! { let args = RpcArgs { call_local: #token, ..args }; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let token = if call_local {
quote! { true }
} else {
quote! { false }
};
quote! { let args = RpcArgs { call_local: #token, ..args }; }
quote! { let args = RpcArgs { call_local: #call_local, ..args }; }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

facepalm moment 😆

Comment on lines 207 to 210
// We do this manually instead of using `iterate_plugins()` because we want to break as soon as we find a match.
let plugins = crate::private::__godot_rust_plugin___GODOT_PLUGIN_REGISTRY
.lock()
.unwrap();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be kept private.

Maybe it's better to add an additional function find_plugin(class_name).

Comment on lines 66 to 67
#[cfg(not(feature = "codegen-full"))]
let rpc_registrations = Option::<TokenStream>::None;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically we use TokenStream::new() for "no tokens", unless there's an explicit is_some() / if let Some(..) check on it needed.

RpcMode::AnyPeer => quote! { RpcMode::ANY_PEER },
};

quote! { let args = RpcArgs { mode: #token, ..args }; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that it matters much, but why did you choose this over args.mode = #token;?
To avoid the "unused mut" warning when no field is changed?

One option is also to provide directly the field inits, as in:

mode: #token

and then have

let args = RpcConfig {
    #( #field_inits, )*
    ..Default::default()
}

But possibly the ..Default::default() must be disabled if all are set, not 100% sure right now 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it that way because of your comment on the other issue:

image

Generated code by the macro -- one call for each key:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, but the "Generated code by the macro" would be per-field init, no? In Rust, you can have a trailing ..Default::default() even if all fields have already been specified.

So this would be even simpler to understand, and also lead to less generated code:

let args = RpcConfig {
    #( #field_inits, )*
    ..Default::default()
}

@Houtamelo
Copy link
Contributor Author

@Bromeon Should be all good now

Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for the update! 👍

@@ -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"] }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A regular cargo test (or similar) command may now pull in the entire codegen, which increases local and CI cycle duration.

Why is it necessary? I'd argue for doc-links alone, it's not worth it. Also because godot-macros's internal docs are only useful for developers, not users.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CI fails without the change I made, because for some reason cargo test is compiling godot-macros with the feature, but godot without, so it fails to compile because godot-macros then generates code that's feature-gated in godot.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it worked before? So what changed that makes it necessary now? Doctests?

It's not like the macros access any library functionality from the other crates -- they just generate code.

Copy link
Contributor Author

@Houtamelo Houtamelo Sep 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it worked before? So what changed that makes it necessary now? Doctests?

Not with my additions, no, they require enums that are only present when code-gen_full is enabled

RpcMode::AnyPeer => quote! { RpcMode::ANY_PEER },
};

quote! { let args = RpcArgs { mode: #token, ..args }; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, but the "Generated code by the macro" would be per-field init, no? In Rust, you can have a trailing ..Default::default() even if all fields have already been specified.

So this would be even simpler to understand, and also lead to less generated code:

let args = RpcConfig {
    #( #field_inits, )*
    ..Default::default()
}

Comment on lines +43 to +46
#[cfg(feature = "codegen-full")]
mod rpc_config;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a brief comment that MultiplayerPeer::TransferMode and MultiplayerApi::RpcMode aren't part of the basic set of classes (also below).

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)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please apply my suggestion verbatim (unless I make a mistake, of course). You can just click "Commit" on the web UI and squash later into your own commit.

The order should be Copy, Clone, Debug. I know it's bikeshedding, but it's documented, and with my readily committable suggestion, the effort on your side to change should be minimal 😉

(Yes, I know I should automate this at some point...)

Comment on lines 41 to 49
/// 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,
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Godot docs, the key names here are incorrect.

Could you then also rename the struct fields to match the dict keys?

itest/rust/src/register_tests/rpc_test.rs Outdated Show resolved Hide resolved
Comment on lines 27 to 69
#[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(config = CACHED_CFG)]
pub fn arg_config(&mut self) {}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add 1 case where you combine it with #[func].

godot-macros/src/class/data_models/rpc.rs Outdated Show resolved Hide resolved
godot-macros/src/class/data_models/inherent_impl.rs Outdated Show resolved Hide resolved
@Bromeon Bromeon changed the title #[rpc] annotation for functions in #[godot_api] inherent impl blocks. Add #[rpc] attribute to user-defined functions Sep 20, 2024
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().
@Houtamelo
Copy link
Contributor Author

Houtamelo commented Sep 20, 2024

@Bromeon ready for another round

Edit: Forgot to mention: I made a bunch of changes to the module inherent_impl, those were made with the goal of fixing a bug where the attributes #[func] and #[rpc] caused a compilation error when used together.
The bug was because, after parsing, the macro would only remove the first of the arguments found.

@Bromeon
Copy link
Member

Bromeon commented Sep 22, 2024

Made a few minor changes:

  • Reverted feature dependency in godot-macro/Cargo.toml, since that meant ./check.sh test would have pulled in full codegen.
  • Renamed RpcConfig methods register -> configure_node and into_dictionary -> to_dictionary (the latter can take by-ref, there's no advantage in consuming the argument).
  • Added a #[cfg], without which there was a local Clippy error.

Thanks a lot for this great addition! 💪

@Bromeon Bromeon added this pull request to the merge queue Sep 22, 2024
Merged via the queue into godot-rust:master with commit b26e114 Sep 22, 2024
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: register Register classes, functions and other symbols to GDScript feature Adds functionality to the library
Projects
None yet
Development

Successfully merging this pull request may close these issues.

@rpc support and examples
3 participants