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

Type-safe signals #1000

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft

Type-safe signals #1000

wants to merge 3 commits into from

Conversation

Bromeon
Copy link
Member

@Bromeon Bromeon commented Jan 6, 2025

Closes #8.
Closes #620.

Early draft for a type-safe signal system. Largely backwards compatible with the current syntax.

The following...

#[signal]
fn my_signal(i: i32, s: GString);

fn static_function(i: i32, s: GString);
fn method(&mut self, i: i32, s: GString);

...expands to code that can then be used like this (inside impl MyClass):

// Connect
let mut sig = self.signals().my_signal();
sig.connect(Self::static_function);
sig.connect_self(Self::method);

// Emit
sig.emit(1234, "hello".into());

Note that this is explicitly not limited to functions declared via #[func]. I had an earlier draft with a generated funcs() API which would route the calls via Godot reflection, but this seems more flexible, since you can keep signal recipients private in Rust. On the downside, it may be harder to disconnect such signals.

Compatibility

⚠️ Currently marked breaking-change because #[signal] now requires a Base<T> field.

As far as I can see, that's the only breakage. It might also be possible to opt in to the new type-safe signals via something like #[godot_api(signals)] or so.

This feature is only available on Godot 4.2+, since it requires custom callables.

Work to do

There's a ton that can still be added, in either this or future PRs:

  1. More connect options
    • Connect with other objects: sig.connect_obj(obj, Class::method)
    • Connect flags
    • Disconnection
  2. External API (outside the impl)
    • something like gd.signals().my_signal()
  3. Signals for Godot's own classes, not just user-defined #[signal]
  4. Visibility of signals and generated API (pub etc).
    • One problem in particular is when the user-defined class is private, but its signal provider is public...
  5. Testing re-entrancy
  6. AsArg in emit()
  7. Support for &self methods
  8. Documentation and examples

Adds three APIs to classes that contain at least one #[signal] decl:
- self.signals()
- self.funcs()
- Self::static_funcs()

The idea is to use generated symbols for type-safe connecting
(signal -> func) as well as emitting. This is still WIP.
@Bromeon Bromeon added feature Adds functionality to the library c: register Register classes, functions and other symbols to GDScript labels Jan 6, 2025
@GodotRust
Copy link

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

Copy link
Member

@lilizoey lilizoey left a comment

Choose a reason for hiding this comment

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

just looking through this a bit and it's really cool from what i can tell! as it's a draft im not really gonna do a proper review but it'll be cool to see the more finished product.

(also happy 1000th issue/pr :p)

Comment on lines 17 to 20
pub trait ParamTuple {
fn to_variant_array(&self) -> Vec<Variant>;
fn from_variant_array(array: &[&Variant]) -> Self;
}
Copy link
Member

Choose a reason for hiding this comment

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

i wonder if this is something we could reuse in Ptr/VarcallSignatureTuple?

Copy link
Member Author

@Bromeon Bromeon Jan 6, 2025

Choose a reason for hiding this comment

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

Yes, I was wondering the same, the similarities are hard to overlook 🙂 It could even be a foundation for a potential builder API one day, if we allow users to register their own methods....

One thing that would be interesting, but very hard to measure, is how compile-time is impacted by different approaches (e.g. more code in declarative macros vs. procedural macros vs. traits/generics) 🤔

Copy link
Member

Choose a reason for hiding this comment

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

One thing that would be interesting, but very hard to measure, is how compile-time is impacted by different approaches (e.g. more code in declarative macros vs. procedural macros vs. traits/generics) 🤔

I can imagine it would also vary a lot depending on exactly what lies where too

@Bromeon Bromeon added the breaking-change Requires SemVer bump label Jan 6, 2025
@Bromeon Bromeon force-pushed the feature/signals branch 2 times, most recently from 522dfbc to 73f520f Compare January 6, 2025 19:42

/// Extracted syntax info for a declared signal.
struct SignalDetails<'a> {
/// `fn my_signal(i: i32, s: GString)`
Copy link
Contributor

Choose a reason for hiding this comment

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

I would change these from docstrings /// to normal comments // - IDE highlights might be a bit too confusing in isolation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure how you mean this -- these types are only used during codegen of the macro and won't be visible to the user.

Using /// has the advantage that hovering over the fields (elsewhere) shows some example formats, plus there's syntax highlighting for the `...` backticked parts 🙂

@Arignir
Copy link

Arignir commented Jan 8, 2025

Seeing this MR gave me a smile for the whole day. Thanks. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking-change Requires SemVer bump 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.

Refer to #[func] names in type-safe ways (not just strings) Signals
5 participants