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

Proc-macro interface for async-await export #975

Merged
merged 2 commits into from
Dec 1, 2022

Conversation

chitoyuu
Copy link
Contributor

This is a two-part PR that accomplishes the following:

Fully proceduralize method exports

Converted the remaining macro_rules! used by the #[methods] macro into procedural macros, increasing flexibility for future expansion and reducing reliance on #[doc(hidden)] macro items.

This makes it easier to support more combinations of method signatures, without having to manually write out n^2 patterns.

Proc-macro interface for async-await export

Added support for exporting async fns and fns that return futures through the proc-macro interface.

  • Added #[method(async)], for methods that return futures but aren't async themselves. This is especially useful for working around lifetime elision (Rust putting the lifetime of &self into the return values of async fns).
  • Added the #[ctx] attribute that denotes the async context. Special arguments (#[base], #[ctx], and possibly #[self] in the future) can now be declared in any order, so long as they precede all regular arguments.
  • Methods without receivers can now be exported. However, they still currently use Map semantics.

Utterly double close #284.

Error messages

As a side effect to this refactoring, the macros are now capable of generating some fairly precisely spanned error messages:

derive_fail_methods_special_args.stderr
error: the method receiver cannot be optional (instead, remove the argument entirely)
  --> tests/ui/derive_fail_methods_special_args.rs:14:25
   |
14 |     async fn optional(#[opt] self, #[base] #[opt] _owner: &Node, #[ctx] #[opt] ctx: ()) {}
   |                         ^^^

error: the base/owner object cannot be optional (instead, remove the argument entirely)
  --> tests/ui/derive_fail_methods_special_args.rs:14:46
   |
14 |     async fn optional(#[opt] self, #[base] #[opt] _owner: &Node, #[ctx] #[opt] ctx: ()) {}
   |                                              ^^^

error: the async context cannot be optional (instead, remove the argument entirely)
  --> tests/ui/derive_fail_methods_special_args.rs:14:75
   |
14 |     async fn optional(#[opt] self, #[base] #[opt] _owner: &Node, #[ctx] #[opt] ctx: ()) {}
   |                                                                           ^^^

error: the method receiver cannot also be the base/owner object
  --> tests/ui/derive_fail_methods_special_args.rs:17:16
   |
17 |     fn based(#[base] self, #[base] _base: &Node, #[base] #[base] _basil: &Node, #[base] #[base] #[base] _basin: &Node) {}
   |                ^^^^

error: duplicate attribute
  --> tests/ui/derive_fail_methods_special_args.rs:17:60
   |
17 |     fn based(#[base] self, #[base] _base: &Node, #[base] #[base] _basil: &Node, #[base] #[base] #[base] _basin: &Node) {}
   |                                                            ^^^^

error: the special parameter base/owner object must only be declared once (the same parameter is already defined at #1)
  --> tests/ui/derive_fail_methods_special_args.rs:17:66
   |
17 |     fn based(#[base] self, #[base] _base: &Node, #[base] #[base] _basil: &Node, #[base] #[base] #[base] _basin: &Node) {}
   |                                                                  ^^^^^^

error: duplicate attribute
  --> tests/ui/derive_fail_methods_special_args.rs:17:91
   |
17 |     fn based(#[base] self, #[base] _base: &Node, #[base] #[base] _basil: &Node, #[base] #[base] #[base] _basin: &Node) {}
   |                                                                                           ^^^^

error: duplicate attribute
  --> tests/ui/derive_fail_methods_special_args.rs:17:99
   |
17 |     fn based(#[base] self, #[base] _base: &Node, #[base] #[base] _basil: &Node, #[base] #[base] #[base] _basin: &Node) {}
   |                                                                                                   ^^^^

error: the special parameter base/owner object must only be declared once (the same parameter is already defined at #1)
  --> tests/ui/derive_fail_methods_special_args.rs:17:105
   |
17 |     fn based(#[base] self, #[base] _base: &Node, #[base] #[base] _basil: &Node, #[base] #[base] #[base] _basin: &Node) {}
   |                                                                                                         ^^^^^^

error: the async context is only available to async methods
  --> tests/ui/derive_fail_methods_special_args.rs:20:26
   |
20 |     fn sync(self, #[ctx] ctx: ()) {}
   |                          ^^^

Close #439.

Compatibility

There aren't any breaking changes that I'm aware of. A shim for the legacy godot_wrap_method macro is kept for compatibility. It can be removed in the next breaking release. Async exports are unsupported by this shim.

Converted the remaining macro_rules! used by the `#[methods]` macro into
procedural macros, increasing flexibility for future expansion and
reducing reliance on `#[doc(hidden)]` macro items.

This makes it easier to support more combinations of method signatures,
without having to manually write out n^2 patterns.

A shim for the legacy `godot_wrap_method` macro is kept for
compatibility. It can be removed in the next breaking release.
@chitoyuu
Copy link
Contributor Author

bors try

bors bot added a commit that referenced this pull request Nov 24, 2022
@bors
Copy link
Contributor

bors bot commented Nov 24, 2022

try

Build failed:

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.

Just realized I didn't submit my review I started a few days ago. Not yet exhaustive, just a first few thoughts. Thanks a lot for this great addition! 🙂

Comment on lines +485 to +490
/// Convenience macro to wrap an object's method into a `Method` implementor
/// that can be passed to the engine when registering a class.
#[proc_macro]
#[deprecated = "The legacy manual export macro is deprecated and will be removed in a future godot-rust version. \
Either use the `#[methods]` attribute macro, or implement the `Method` trait manually instead."]
pub fn godot_wrap_method(input: TokenStream) -> TokenStream {
Copy link
Member

Choose a reason for hiding this comment

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

Clever! I think for all practical purposes, turning a declarative macro into a procedural function-like one should not constitute a breaking change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It shouldn't. There used to be a limitation where procedural function-like macros couldn't be used in expressions, but that was lifted quite a while ago (below our MSRV).

Comment on lines 518 to 531
/// Returns the (possibly renamed or imported as `gdnative`) identifier of the `gdnative_core` crate.
fn crate_gdnative_core() -> proc_macro2::TokenStream {
let found_crate = proc_macro_crate::crate_name("gdnative-core")
.or_else(|_| proc_macro_crate::crate_name("gdnative"))
.expect("crate not found");

match found_crate {
proc_macro_crate::FoundCrate::Itself => quote!(crate),
proc_macro_crate::FoundCrate::Name(name) => {
let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
quote!( #ident )
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

This is a really nice change, hardcoding the crate name was something that bothered me a bit in the past.

gdnative-derive/src/lib.rs Outdated Show resolved Hide resolved
gdnative-derive/src/methods.rs Outdated Show resolved Hide resolved
gdnative-derive/src/methods.rs Outdated Show resolved Hide resolved
gdnative/tests/ui/derive_fail_methods_special_args.rs Outdated Show resolved Hide resolved
Comment on lines +310 to +320
/// // This will NOT compile: Rust assumes that any futures returned by an `async fn` may only live as long as each of its
/// // arguments, and there is no way to tell it otherwise. As a result, it will emit some cryptic complaints about lifetime.
/// #[method]
/// async fn answer(&self) -> i32 {
/// 42
/// }
Copy link
Member

Choose a reason for hiding this comment

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

Is this not contradicting the code examples further above, where you have async fns that are apparently? Or is the difference the lack of return value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Technically it isn't, but I see how it can be confusing. As far as the macro is concerned, this declaration is valid. The only thing preventing it from compiling is Rust's lifetime elision rules, and those rules can be relaxed down the line such that code like this compile.

I'll try to think of a way to word the examples better.

Copy link
Member

Choose a reason for hiding this comment

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

Yep, I think it would help to have a rule for users to know when they can use async fn and when not.

Maybe some of these rules could also be detected by the proc-macro itself, to give a better compiler message.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's a problem with macro-based detection though: we still can't emit proper warnings. I don't think we want async fn (&self) to be a hard error here, for the reasons above, but any warnings we emit are still through #[deprecated], and as such a user cannot #[allow] them precisely without also allowing all the actually deprecated stuff.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Re-worded the examples.

Comment on lines 228 to 230
/// - `#[ctx]` - The [async context](gdnative::tasks::Context), for async methods. See the `async` argument
/// below.
Copy link
Member

Choose a reason for hiding this comment

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

Typical parameter declaration is

#[ctx] ctx: Arc<Context>

Is that clear enough? Or should the type perhaps be named AsyncContext? The latter would make sure that it doesn't make sense in a non-async function signature.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renaming the type would constitute a breaking change though, and I'm not exactly sure how much it's going to matter: currently the user have to import the Context type from gdnative::tasks::Context, which already indicates that it's related to async. The API is a dead giveaway too. I guess the attribute can be renamed #[async_ctx] if the extra clarity is worth the verbosity.

Note that the macro emits a rather informative error (imo) when it sees a #[ctx] in a non-async function:

error: the async context is only available to async methods
--> tests/ui/derive_fail_methods_special_args.rs:20:26
|
20 | fn sync(self, #[ctx] ctx: ()) {}
| ^^^

Copy link
Member

Choose a reason for hiding this comment

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

Ah true, this type was already there. #[async_ctx] was actually my first idea, but I thought in the type it might be clearer.

#[async_ctx] might be a bit clearer, but maybe we should ask some users. On the other hand, long signatures might be broken into multiple lines by rustfmt, and then having the same length as #[opt] would be a tiny readability bonus 😉probably not that much difference in practice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to #[async_ctx] for now.

@Bromeon Bromeon added the feature Adds functionality to the library label Nov 25, 2022
@chitoyuu
Copy link
Contributor Author

Thanks for the review! I'll implement most of the suggested changes tomorrow.

Added support for exporting `async fn`s and `fn`s that return futures
through the proc-macro interface.

- Added `#[method(async)]`, for methods that return futures but aren't
  async themselves. This is especially useful for working around
  lifetime elision (Rust putting the lifetime of `&self` into the return
  values of `async fn`s).
- Added the `#[ctx]` attribute that denotes the async context. Special
  arguments (`#[base]`, `#[ctx]`, and possibly `#[self]` in the future)
  can now be declared in any order, so long as they precede all regular
  arguments.
- Methods without receivers can now be exported. However, they still
  currently use `Map` semantics.

Async exports are unsupported by the legacy `godot_wrap_method` shim.
@chitoyuu
Copy link
Contributor Author

bors try

bors bot added a commit that referenced this pull request Nov 26, 2022
@bors
Copy link
Contributor

bors bot commented Nov 26, 2022

try

Build succeeded:

@Bromeon Bromeon added the c: export Component: export (mod export, derive) label Nov 27, 2022
@chitoyuu
Copy link
Contributor Author

chitoyuu commented Nov 30, 2022

Will merge a day later so related development can continue.

@chitoyuu
Copy link
Contributor Author

chitoyuu commented Dec 1, 2022

bors r+

@bors
Copy link
Contributor

bors bot commented Dec 1, 2022

Build succeeded:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: export Component: export (mod export, derive) feature Adds functionality to the library
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Better errors when the signature of exported methods are invalid [discussion] Async-await support
2 participants