-
-
Notifications
You must be signed in to change notification settings - Fork 196
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
Conversation
8ae84df
to
45779d0
Compare
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-902 |
45779d0
to
33fc9f0
Compare
There was a problem hiding this 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()
.
godot-core/src/meta/rpc_args.rs
Outdated
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)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#[derive(Debug, Clone, Copy)] | |
#[derive(Copy, Clone, Debug)] |
Just canonical order 🙂
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>>(); |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Context is helpful:
.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), |
There was a problem hiding this comment.
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`)"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(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`)"), |
ty: ItemAttrType::Func { | ||
rename: None, | ||
is_virtual: false, | ||
has_gd_self: false, | ||
rpc_info: Some(rpc_info), | ||
}, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
let token = if call_local { | ||
quote! { true } | ||
} else { | ||
quote! { false } | ||
}; | ||
|
||
quote! { let args = RpcArgs { call_local: #token, ..args }; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 }; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
facepalm moment 😆
godot-core/src/registry/class.rs
Outdated
// 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(); |
There was a problem hiding this comment.
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)
.
#[cfg(not(feature = "codegen-full"))] | ||
let rpc_registrations = Option::<TokenStream>::None; |
There was a problem hiding this comment.
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 }; } |
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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()
}
33fc9f0
to
7aabeaf
Compare
@Bromeon Should be all good now |
There was a problem hiding this 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! 👍
godot-macros/Cargo.toml
Outdated
@@ -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"] } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 }; } |
There was a problem hiding this comment.
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()
}
#[cfg(feature = "codegen-full")] | ||
mod rpc_config; |
There was a problem hiding this comment.
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).
godot-core/src/meta/rpc_config.rs
Outdated
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)] |
There was a problem hiding this comment.
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...)
godot-core/src/meta/rpc_config.rs
Outdated
/// 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, | ||
} | ||
} |
There was a problem hiding this comment.
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?
#[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) {} | ||
} |
There was a problem hiding this comment.
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]
.
#[rpc]
attribute to user-defined functions
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().
0669831
to
c4dd6b7
Compare
@Bromeon ready for another round Edit: Forgot to mention: I made a bunch of changes to the module |
6ad6756
to
2ae2995
Compare
Made a few minor changes:
Thanks a lot for this great addition! 💪 |
The style is similar to GDScript's @rpc annotation, the macro can be used as follows:
1 - Separate arguments:
Providing overlapping arguments generates a compile error.
Any omitted arguments are set to their default values.
2 - Provide an expression:
2
is useful in case you want to reuse the configuration on multiple functions.2
is mutually exclusive with number1
.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:
ClassPlugin
.__before_ready()
, searches for the configuration in the plugin, registering them with Node::rpc_config().Edit: the enums
RpcMode
andTransferMode
are only available if the feature __codegen_full is enabled, so I had to feature-gate everything related to the implementation behind that feature.