Skip to content

Type-safe signals #1000

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

Merged
merged 17 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/full-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ jobs:
RUSTDOCFLAGS: >
-D rustdoc::broken-intra-doc-links -D rustdoc::private-intra-doc-links -D rustdoc::invalid-codeblock-attributes
-D rustdoc::invalid-rust-codeblocks -D rustdoc::invalid-html-tags -D rustdoc::bare-urls -D rustdoc::unescaped-backticks
-D warnings
run: cargo doc -p godot --ignore-rust-version


Expand Down
2 changes: 1 addition & 1 deletion godot-core/src/builtin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,9 @@ pub use crate::{array, dict, real, reals, varray};

// Re-export generated enums.
pub use crate::gen::central::global_reexported_enums::{Corner, EulerOrder, Side, VariantOperator};
pub use crate::sys::VariantType;
// Not yet public.
pub(crate) use crate::gen::central::VariantDispatch;
pub use crate::sys::VariantType;

#[doc(hidden)]
pub mod __prelude_reexport {
Expand Down
2 changes: 1 addition & 1 deletion godot-core/src/classes/class_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub(crate) fn display_string<T: GodotClass>(
obj: &Gd<T>,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
let string: GString = obj.raw.as_object().to_string();
let string: GString = obj.raw.as_object_ref().to_string();
<GString as std::fmt::Display>::fmt(&string, f)
}

Expand Down
3 changes: 3 additions & 0 deletions godot-core/src/meta/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ pub use class_name::ClassName;
pub use godot_convert::{FromGodot, GodotConvert, ToGodot};
pub use traits::{ArrayElement, GodotType, PackedArrayElement};

#[cfg(since_api = "4.2")]
pub use crate::registry::signal::variadic::ParamTuple;

pub(crate) use array_type_info::ArrayTypeInfo;
pub(crate) use traits::{
element_godot_type_name, element_variant_type, GodotFfiVariant, GodotNullableFfi,
Expand Down
24 changes: 23 additions & 1 deletion godot-core/src/obj/gd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::meta::{
};
use crate::obj::{
bounds, cap, Bounds, DynGd, GdDerefTarget, GdMut, GdRef, GodotClass, Inherits, InstanceId,
RawGd,
RawGd, WithSignals,
};
use crate::private::callbacks;
use crate::registry::property::{object_export_element_type_string, Export, Var};
Expand Down Expand Up @@ -327,6 +327,11 @@ impl<T: GodotClass> Gd<T> {
.expect("Upcast to Object failed. This is a bug; please report it.")
}

/// Equivalent to [`upcast_mut::<Object>()`][Self::upcast_mut], but without bounds.
pub(crate) fn upcast_object_mut(&mut self) -> &mut classes::Object {
self.raw.as_object_mut()
}

/// **Upcast shared-ref:** access this object as a shared reference to a base class.
///
/// This is semantically equivalent to multiple applications of [`Self::deref()`]. Not really useful on its own, but combined with
Expand Down Expand Up @@ -700,6 +705,23 @@ where
}
}

impl<T> Gd<T>
where
T: WithSignals,
{
/// Access user-defined signals of this object.
///
/// For classes that have at least one `#[signal]` defined, returns a collection of signal names. Each returned signal has a specialized
/// API for connecting and emitting signals in a type-safe way. This method is the equivalent of [`WithSignals::signals()`], but when
/// called externally (not from `self`). If you are within the `impl` of a class, use `self.signals()` directly instead.
///
/// If you haven't already, read the [book chapter about signals](https://godot-rust.github.io/book/register/signals.html) for a
/// walkthrough.
pub fn signals(&self) -> T::SignalCollection<'_> {
T::__signals_from_external(self)
}
}

// ----------------------------------------------------------------------------------------------------------------------------------------------
// Trait impls

Expand Down
7 changes: 6 additions & 1 deletion godot-core/src/obj/raw_gd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,16 @@ impl<T: GodotClass> RawGd<T> {
// self.as_target_mut()
// }

pub(crate) fn as_object(&self) -> &classes::Object {
pub(crate) fn as_object_ref(&self) -> &classes::Object {
// SAFETY: Object is always a valid upcast target.
unsafe { self.as_upcast_ref() }
}

pub(crate) fn as_object_mut(&mut self) -> &mut classes::Object {
// SAFETY: Object is always a valid upcast target.
unsafe { self.as_upcast_mut() }
}

/// # Panics
/// If this `RawGd` is null. In Debug mode, sanity checks (valid upcast, ID comparisons) can also lead to panics.
///
Expand Down
12 changes: 6 additions & 6 deletions godot-core/src/obj/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,10 @@ pub trait ScriptInstance: Sized {
args: &[&Variant],
) -> Result<Variant, sys::GDExtensionCallErrorType>;

/// Identifies the script instance as a placeholder. If this function and
/// [IScriptExtension::is_placeholder_fallback_enabled](crate::classes::IScriptExtension::is_placeholder_fallback_enabled) return true,
/// Godot will call [`Self::property_set_fallback`] instead of [`Self::set_property`].
/// Identifies the script instance as a placeholder, routing property writes to a fallback if applicable.
///
/// If this function and [IScriptExtension::is_placeholder_fallback_enabled] return true, Godot will call [`Self::property_set_fallback`]
/// instead of [`Self::set_property`].
fn is_placeholder(&self) -> bool;

/// Validation function for the engine to verify if the script exposes a certain method.
Expand Down Expand Up @@ -157,8 +158,7 @@ pub trait ScriptInstance: Sized {
/// The engine may call this function if it failed to get a property value via [`ScriptInstance::get_property`] or the native type's getter.
fn property_get_fallback(&self, name: StringName) -> Option<Variant>;

/// The engine may call this function if
/// [`IScriptExtension::is_placeholder_fallback_enabled`](crate::classes::IScriptExtension::is_placeholder_fallback_enabled) is enabled.
/// The engine may call this function if [`IScriptExtension::is_placeholder_fallback_enabled`] is enabled.
fn property_set_fallback(this: SiMut<Self>, name: StringName, value: &Variant) -> bool;

/// This function will be called to handle calls to [`Object::get_method_argument_count`](crate::classes::Object::get_method_argument_count)
Expand Down Expand Up @@ -347,7 +347,7 @@ pub unsafe fn create_script_instance<T: ScriptInstance>(
/// This function both checks if the passed script matches the one currently assigned to the passed object, as well as verifies that
/// there is an instance for the script.
///
/// Use this function to implement [`IScriptExtension::instance_has`](crate::classes::IScriptExtension::instance_has).
/// Use this function to implement [`IScriptExtension::instance_has`].
#[cfg(since_api = "4.2")]
pub fn script_instance_exists<O, S>(object: &Gd<O>, script: &Gd<S>) -> bool
where
Expand Down
38 changes: 36 additions & 2 deletions godot-core/src/obj/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,8 @@ pub trait IndexEnum: EngineEnum {
// Possible alternative for builder APIs, although even less ergonomic: Base<T> could be Base<T, Self> and return Gd<Self>.
#[diagnostic::on_unimplemented(
message = "Class `{Self}` requires a `Base<T>` field",
label = "missing field `_base: Base<...>`",
note = "A base field is required to access the base from within `self`, for script-virtual functions or #[rpc] methods",
label = "missing field `_base: Base<...>` in struct declaration",
note = "A base field is required to access the base from within `self`, as well as for #[signal], #[rpc] and #[func(virtual)]",
note = "see also: https://godot-rust.github.io/book/register/classes.html#the-base-field"
)]
pub trait WithBaseField: GodotClass + Bounds<Declarer = bounds::DeclUser> {
Expand Down Expand Up @@ -428,6 +428,40 @@ pub trait WithBaseField: GodotClass + Bounds<Declarer = bounds::DeclUser> {
}
}

pub trait WithSignals: WithBaseField {
type SignalCollection<'a>;

/// Access user-defined signals of the current object `self`.
///
/// For classes that have at least one `#[signal]` defined, returns a collection of signal names. Each returned signal has a specialized
/// API for connecting and emitting signals in a type-safe way. If you need to access signals from outside (given a `Gd` pointer), use
/// [`Gd::signals()`] instead.
///
/// If you haven't already, read the [book chapter about signals](https://godot-rust.github.io/book/register/signals.html) for a
/// walkthrough.
///
/// # Provided API
///
/// The returned collection provides a method for each signal, with the same name as the corresponding `#[signal]`. \
/// For example, if you have...
/// ```ignore
/// #[signal]
/// fn damage_taken(&mut self, amount: i32);
/// ```
/// ...then you can access the signal as `self.signals().damage_taken()`, which returns an object with the following API:
///
/// | Method signature | Description |
/// |------------------|-------------|
/// | `connect(f: impl FnMut(i32))` | Connects global or associated function, or a closure. |
/// | `connect_self(f: impl FnMut(&mut Self, i32))` | Connects a `&mut self` method or closure. |
/// | `emit(amount: i32)` | Emits the signal with the given arguments. |
///
fn signals(&mut self) -> Self::SignalCollection<'_>;

#[doc(hidden)]
fn __signals_from_external(external: &Gd<Self>) -> Self::SignalCollection<'_>;
}

/// Extension trait for all reference-counted classes.
pub trait NewGd: GodotClass {
/// Return a new, ref-counted `Gd` containing a default-constructed instance.
Expand Down
1 change: 0 additions & 1 deletion godot-core/src/private.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ use std::sync::atomic;
#[cfg(debug_assertions)]
use std::sync::{Arc, Mutex};
use sys::Global;

// ----------------------------------------------------------------------------------------------------------------------------------------------
// Global variables

Expand Down
7 changes: 7 additions & 0 deletions godot-core/src/registry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ pub mod method;
pub mod plugin;
pub mod property;

#[cfg(since_api = "4.2")]
pub mod signal;

// Contents re-exported in `godot` crate; just keep empty.
#[cfg(before_api = "4.2")]
pub mod signal {}

// RpcConfig uses MultiplayerPeer::TransferMode and MultiplayerApi::RpcMode, which are only enabled in `codegen-full` feature.
#[cfg(feature = "codegen-full")]
mod rpc_config;
Expand Down
Loading
Loading