Skip to content

Inherited typed signals #1134

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 11 commits into from
Apr 21, 2025
Merged
23 changes: 23 additions & 0 deletions godot-codegen/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub struct Context<'a> {
inheritance_tree: InheritanceTree,
cached_rust_types: HashMap<GodotTy, RustTy>,
notifications_by_class: HashMap<TyName, Vec<(Ident, i32)>>,
classes_with_signals: HashSet<TyName>,
notification_enum_names_by_class: HashMap<TyName, NotificationEnum>,
method_table_indices: HashMap<MethodTableKey, usize>,
method_table_next_index: HashMap<String, usize>,
Expand Down Expand Up @@ -66,6 +67,10 @@ impl<'a> Context<'a> {
// Populate class lookup by name
engine_classes.insert(class_name.clone(), class);

if !option_as_slice(&class.signals).is_empty() {
ctx.classes_with_signals.insert(class_name.clone());
}

// Populate derived-to-base relations
if let Some(base) = class.inherits.as_ref() {
let base_name = TyName::from_godot(base);
Expand Down Expand Up @@ -278,6 +283,24 @@ impl<'a> Context<'a> {
self.cached_rust_types.get(ty)
}

/// Walks up in the hierarchy, and returns the first (nearest) base class which declares at least 1 signal.
///
/// Always returns a result, as `Object` (the root) itself declares signals.
pub fn find_nearest_base_with_signals(&self, class_name: &TyName) -> TyName {
let tree = self.inheritance_tree();

let mut class = class_name.clone();
while let Some(base) = tree.direct_base(&class) {
if self.classes_with_signals.contains(&base) {
return base;
} else {
class = base;
}
}

panic!("Object (root) should always have signals")
}

pub fn notification_constants(&'a self, class_name: &TyName) -> Option<&'a Vec<(Ident, i32)>> {
self.notifications_by_class.get(class_name)
}
Expand Down
11 changes: 7 additions & 4 deletions godot-codegen/src/generator/classes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
builders,
} = make_class_methods(class, &class.methods, &cfg_attributes, ctx);

let signal_types = signals::make_class_signals(class, &class.signals, ctx);
let signals::SignalCodegen {
signal_code,
has_own_signals,
} = signals::make_class_signals(class, &class.signals, ctx);

let enums = enums::make_enums(&class.enums, &cfg_attributes);
let constants = constants::make_constants(&class.constants);
Expand All @@ -137,14 +140,14 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
// Associated "sidecar" module is made public if there are other symbols related to the class, which are not
// in top-level godot::classes module (notification enums are not in the sidecar, but in godot::classes::notify).
// This checks if token streams (i.e. code) is empty.
let has_sidecar_module = !enums.is_empty() || !builders.is_empty() || signal_types.is_some();
let has_sidecar_module = !enums.is_empty() || !builders.is_empty() || has_own_signals;

let class_doc = docs::make_class_doc(
class_name,
base_ident_opt,
notification_enum.is_some(),
has_sidecar_module,
signal_types.is_some(),
has_own_signals,
);

let module_doc = docs::make_module_doc(class_name);
Expand Down Expand Up @@ -262,7 +265,7 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas

#builders
#enums
#signal_types
#signal_code
};
// note: TypePtr -> ObjectPtr conversion OK?

Expand Down
167 changes: 132 additions & 35 deletions godot-codegen/src/generator/signals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,48 +12,121 @@

use crate::context::Context;
use crate::conv;
use crate::models::domain::{Class, ClassLike, ClassSignal, FnParam, RustTy, TyName};
use crate::models::domain::{Class, ClassLike, ClassSignal, FnParam, ModName, RustTy, TyName};
use crate::util::{ident, safe_ident};
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};

pub struct SignalCodegen {
pub signal_code: TokenStream,
pub has_own_signals: bool,
}

pub fn make_class_signals(
class: &Class,
signals: &[ClassSignal],
_ctx: &mut Context,
) -> Option<TokenStream> {
if signals.is_empty() {
return None;
}

ctx: &mut Context,
) -> SignalCodegen {
let all_params: Vec<SignalParams> = signals
.iter()
.map(|s| SignalParams::new(&s.parameters))
.collect();

let signal_collection_struct = make_signal_collection(class, signals, &all_params);
let class_name = class.name();

// If no signals are defined in current class, walk up until we find some.
let (own_collection_struct, nearest_collection_name, nearest_class, has_own_signals);
if signals.is_empty() {
// Use the nearest base class that *has* signals, and store its collection name.
let nearest = ctx.find_nearest_base_with_signals(class_name);

// Doesn't define own collection struct if no signals are present (note that WithSignals is still implemented).
own_collection_struct = TokenStream::new();
nearest_collection_name = make_collection_name(&nearest);
nearest_class = Some(nearest);
has_own_signals = false;
} else {
let (code, name) = make_signal_collection(class, signals, &all_params);

own_collection_struct = code;
nearest_collection_name = name;
nearest_class = None;
has_own_signals = true;
};

let signal_types = signals
.iter()
.zip(all_params.iter())
.map(|(signal, params)| make_signal_individual_struct(signal, params));

let class_name = class.name();
let with_signals_impl =
make_with_signals_impl(class_name, &nearest_collection_name, nearest_class.as_ref());

Some(quote! {
let deref_impl =
has_own_signals.then(|| make_upcast_deref_impl(class_name, &nearest_collection_name));

let code = quote! {
#[cfg(since_api = "4.2")]
pub use signals::*;

#[cfg(since_api = "4.2")]
mod signals {
use crate::obj::Gd;
use crate::obj::{Gd, GodotClass};
use super::re_export::#class_name;
use crate::registry::signal::TypedSignal;
use super::*;

#signal_collection_struct
// These may be empty if the class doesn't define any signals itself.
#own_collection_struct
#( #signal_types )*

// These are always present.
#with_signals_impl
#deref_impl
}
})
};

SignalCodegen {
signal_code: code,
has_own_signals,
}
}

/// Creates `impl WithSignals`.
///
/// Present for every single class, as every class has at least inherited signals (since `Object` has some).
fn make_with_signals_impl(
class_name: &TyName,
collection_struct_name: &Ident,
nearest_class: Option<&TyName>, // None if own class has signals.
) -> TokenStream {
let base_use_statement = quote! { use crate::obj::WithSignals; };
let use_statement = if let Some(nearest_class) = nearest_class {
let module_name = ModName::from_godot(&nearest_class.godot_ty);
quote! {
#base_use_statement
use crate::classes::#module_name::#collection_struct_name;
}
} else {
base_use_statement
};

quote! {
#use_statement
impl WithSignals for #class_name {
type SignalCollection<'c, C: WithSignals> = #collection_struct_name<'c, C>;
type __SignalObj<'c> = Gd<Self>;
// type __SignalObj<'c, C: WithSignals> = Gd<Self>;

// During construction, C = Self.
#[doc(hidden)]
fn __signals_from_external(gd_mut: &mut Gd<Self>) -> Self::SignalCollection<'_, Self> {
Self::SignalCollection {
__internal_obj: Some(gd_mut.clone()),
}
}
}
}
}

// Used outside, to document class with links to this type.
Expand All @@ -70,7 +143,9 @@ fn make_signal_collection(
class: &Class,
signals: &[ClassSignal],
params: &[SignalParams],
) -> TokenStream {
) -> (TokenStream, Ident) {
debug_assert!(!signals.is_empty()); // checked outside

let class_name = class.name();
let collection_struct_name = make_collection_name(class_name);

Expand All @@ -83,9 +158,9 @@ fn make_signal_collection(
quote! {
// Important to return lifetime 'c here, not '_.
#[doc = #provider_docs]
pub fn #signal_name(&mut self) -> #individual_struct_name<'c> {
pub fn #signal_name(&mut self) -> #individual_struct_name<'c, C> {
#individual_struct_name {
typed: crate::registry::signal::TypedSignal::new(self.__gd.clone(), #signal_name_str)
typed: TypedSignal::extract(&mut self.__internal_obj, #signal_name_str)
}
}
}
Expand All @@ -96,26 +171,48 @@ fn make_signal_collection(
c = class_name.rust_ty
);

quote! {
let code = quote! {
#[doc = #collection_docs]
pub struct #collection_struct_name<'c> {
__gd: &'c mut Gd<#class_name>,
// C is needed for signals of derived classes that are upcast via Deref; C in that class is the derived class.
pub struct #collection_struct_name<'c, C: WithSignals = #class_name>
{
#[doc(hidden)]
pub(crate) __internal_obj: Option<C::__SignalObj<'c>>,
}

impl<'c> #collection_struct_name<'c> {
impl<'c, C: WithSignals> #collection_struct_name<'c, C> {
#( #provider_methods )*
}
};

impl crate::obj::WithSignals for #class_name {
type SignalCollection<'c> = #collection_struct_name<'c>;
#[doc(hidden)]
type __SignalObject<'c> = Gd<#class_name>;
(code, collection_struct_name)
}

#[doc(hidden)]
fn __signals_from_external(external: &mut Gd<Self>) -> Self::SignalCollection<'_> {
Self::SignalCollection {
__gd: external,
}
fn make_upcast_deref_impl(class_name: &TyName, collection_struct_name: &Ident) -> TokenStream {
// Root of hierarchy, no "upcast" derefs.
if class_name.rust_ty == "Object" {
return TokenStream::new();
}

quote! {
impl<'c, C: WithSignals> std::ops::Deref for #collection_struct_name<'c, C> {
// The whole upcast mechanism is based on C remaining the same even through upcast.
type Target = <
<
#class_name as crate::obj::GodotClass
>::Base as WithSignals
>::SignalCollection<'c, C>;

fn deref(&self) -> &Self::Target {
type Derived = #class_name;
crate::private::signal_collection_to_base::<C, Derived>(self)
}
}

impl<'c, C: WithSignals> std::ops::DerefMut for #collection_struct_name<'c, C> {
fn deref_mut(&mut self) -> &mut Self::Target {
type Derived = #class_name;
crate::private::signal_collection_to_base_mut::<C, Derived>(self)
}
}
}
Expand All @@ -139,10 +236,10 @@ fn make_signal_individual_struct(signal: &ClassSignal, params: &SignalParams) ->
// Embedded in `mod signals`.
quote! {
// Reduce tokens to parse by reusing this type definitions.
type #typed_name<'c> = crate::registry::signal::TypedSignal<'c, #class_ty, #param_tuple>;
type #typed_name<'c, C> = TypedSignal<'c, C, #param_tuple>;

pub struct #individual_struct_name<'c> {
typed: #typed_name<'c>,
pub struct #individual_struct_name<'c, C: WithSignals = #class_ty> {
typed: #typed_name<'c, C>,
}

impl<'c> #individual_struct_name<'c> {
Expand All @@ -151,15 +248,15 @@ fn make_signal_individual_struct(signal: &ClassSignal, params: &SignalParams) ->
}
}

impl<'c> std::ops::Deref for #individual_struct_name<'c> {
type Target = #typed_name<'c>;
impl<'c, C: WithSignals> std::ops::Deref for #individual_struct_name<'c, C> {
type Target = #typed_name<'c, C>;

fn deref(&self) -> &Self::Target {
&self.typed
}
}

impl std::ops::DerefMut for #individual_struct_name<'_> {
impl<C: WithSignals> std::ops::DerefMut for #individual_struct_name<'_, C> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.typed
}
Expand Down
2 changes: 1 addition & 1 deletion godot-core/src/obj/gd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ where
///
/// [`WithUserSignals::signals()`]: crate::obj::WithUserSignals::signals()
#[cfg(since_api = "4.2")]
pub fn signals(&mut self) -> T::SignalCollection<'_> {
pub fn signals(&mut self) -> T::SignalCollection<'_, T> {
T::__signals_from_external(self)
}
}
Expand Down
Loading
Loading