Skip to content

Type-safe signals for engine classes #1111

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 4 commits into from
Mar 30, 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
11 changes: 9 additions & 2 deletions godot-codegen/src/generator/classes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
use crate::context::{Context, NotificationEnum};
use crate::generator::functions_common::{FnCode, FnDefinition, FnDefinitions};
use crate::generator::method_tables::MethodTableKey;
use crate::generator::{constants, docs, enums, functions_common, notifications, virtual_traits};
use crate::generator::{
constants, docs, enums, functions_common, notifications, signals, virtual_traits,
};
use crate::models::domain::{
ApiView, Class, ClassLike, ClassMethod, ExtensionApi, FnDirection, FnQualifier, Function,
ModName, TyName,
Expand Down Expand Up @@ -121,6 +123,8 @@ 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 enums = enums::make_enums(&class.enums, &cfg_attributes);
let constants = constants::make_constants(&class.constants);
let inherits_macro = format_ident!("unsafe_inherits_transitive_{}", class_name.rust_ty);
Expand All @@ -133,14 +137,16 @@ 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();
let has_sidecar_module = !enums.is_empty() || !builders.is_empty() || signal_types.is_some();

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

let module_doc = docs::make_module_doc(class_name);

let virtual_trait = virtual_traits::make_virtual_methods_trait(
Expand Down Expand Up @@ -256,6 +262,7 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas

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

Expand Down
28 changes: 22 additions & 6 deletions godot-codegen/src/generator/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//!
//! Single module for documentation, rather than having it in each symbol-specific file, so it's easier to keep docs consistent.

use crate::generator::signals;
use crate::models::domain::{ModName, TyName};
use crate::special_cases;
use proc_macro2::Ident;
Expand All @@ -18,6 +19,7 @@ pub fn make_class_doc(
base_ident_opt: Option<Ident>,
has_notification_enum: bool,
has_sidecar_module: bool,
has_signal_collection: bool,
) -> String {
let TyName { rust_ty, godot_ty } = class_name;

Expand All @@ -29,15 +31,28 @@ pub fn make_class_doc(
.to_string()
};

let notify_line = if has_notification_enum {
format!("* [`{rust_ty}Notification`][crate::classes::notify::{rust_ty}Notification]: notification type\n")
let (sidecar_signal_lines, module_name);
if has_sidecar_module {
let module = ModName::from_godot(&class_name.godot_ty).rust_mod;

sidecar_signal_lines = format!("* [`{module}`][crate::classes::{module}]: sidecar module with related enum/flag types\n");
module_name = Some(module);
} else {
sidecar_signal_lines = String::new();
module_name = None;
};

let signal_line = if has_signal_collection {
let signal_coll = signals::make_collection_name(class_name);
let module = module_name.expect("signal implies presence of sidecar module");

format!("* [`{signal_coll}`][crate::classes::{module}::{signal_coll}]: signal collection\n")
} else {
String::new()
};

let sidecar_line = if has_sidecar_module {
let module_name = ModName::from_godot(&class_name.godot_ty).rust_mod;
format!("* [`{module_name}`][crate::classes::{module_name}]: sidecar module with related enum/flag types\n")
let notify_line = if has_notification_enum {
format!("* [`{rust_ty}Notification`][crate::classes::notify::{rust_ty}Notification]: notification type\n")
} else {
String::new()
};
Expand All @@ -59,8 +74,9 @@ pub fn make_class_doc(
{inherits_line}\n\n\
\
Related symbols:\n\n\
{sidecar_line}\
{sidecar_signal_lines}\
* [`{trait_name}`][crate::classes::{trait_name}]: virtual methods\n\
{signal_line}\
{notify_line}\
\n\n\
See also [Godot docs for `{godot_ty}`]({online_link}).\n\n{notes}",
Expand Down
1 change: 1 addition & 0 deletions godot-codegen/src/generator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub mod lifecycle_builtins;
pub mod method_tables;
pub mod native_structures;
pub mod notifications;
pub mod signals;
pub mod utility_functions;
pub mod virtual_traits;

Expand Down
225 changes: 225 additions & 0 deletions godot-codegen/src/generator/signals.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

// Code duplication: while there is some overlap with godot-macros/signal.rs for #[signal] handling, this file here is quite a bit simpler,
// as it only deals with predefined signal definitions (no user-defined #[cfg], visibility, etc). On the other hand, there is documentation
// for these signals, and integration is slightly different due to lack of WithBaseField trait. Nonetheless, some parts could potentially
// be extracted into a future crate shared by godot-codegen and godot-macros.

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

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

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 signal_types = signals
.iter()
.zip(all_params.iter())
.map(|(signal, params)| make_signal_individual_struct(signal, params));

let class_name = class.name();

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

#[cfg(since_api = "4.2")]
mod signals {
use crate::obj::Gd;
use super::re_export::#class_name;
use super::*;

#signal_collection_struct
#( #signal_types )*
}
})
}

// Used outside, to document class with links to this type.
pub fn make_collection_name(class_name: &TyName) -> Ident {
format_ident!("SignalsIn{}", class_name.rust_ty)
}

fn make_individual_struct_name(signal_name: &str) -> Ident {
let signal_pascal_name = conv::to_pascal_case(signal_name);
format_ident!("Sig{}", signal_pascal_name)
}

fn make_signal_collection(
class: &Class,
signals: &[ClassSignal],
params: &[SignalParams],
) -> TokenStream {
let class_name = class.name();
let collection_struct_name = make_collection_name(class_name);

let provider_methods = signals.iter().zip(params).map(|(sig, params)| {
let signal_name_str = &sig.name;
let signal_name = ident(&sig.name);
let individual_struct_name = make_individual_struct_name(&sig.name);
let provider_docs = format!("Signature: `({})`", params.formatted_types);

quote! {
// Important to return lifetime 'c here, not '_.
#[doc = #provider_docs]
pub fn #signal_name(&mut self) -> #individual_struct_name<'c> {
#individual_struct_name {
typed: crate::registry::signal::TypedSignal::new(self.__gd.clone(), #signal_name_str)
}
}
}
});

let collection_docs = format!(
"A collection of signals for the [`{c}`][crate::classes::{c}] class.",
c = class_name.rust_ty
);

quote! {
#[doc = #collection_docs]
pub struct #collection_struct_name<'c> {
__gd: &'c mut Gd<#class_name>,
}

impl<'c> #collection_struct_name<'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>;

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

fn make_signal_individual_struct(signal: &ClassSignal, params: &SignalParams) -> TokenStream {
let individual_struct_name = make_individual_struct_name(&signal.name);

let SignalParams {
param_list,
type_list,
name_list,
..
} = params;

let class_name = &signal.surrounding_class;
let class_ty = quote! { #class_name };
let param_tuple = quote! { ( #type_list ) };
let typed_name = format_ident!("Typed{}", individual_struct_name);

// 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>;

pub struct #individual_struct_name<'c> {
typed: #typed_name<'c>,
}

impl<'c> #individual_struct_name<'c> {
pub fn emit(&mut self, #param_list) {
self.typed.emit_tuple( (#name_list) );
}
}

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

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

impl std::ops::DerefMut for #individual_struct_name<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.typed
}
}
}
}

// ----------------------------------------------------------------------------------------------------------------------------------------------

struct SignalParams {
/// `name: Type, ...`
param_list: TokenStream,

/// `Type, ...` -- for example inside a tuple type.
type_list: TokenStream,

/// `name, ...` -- for example inside a tuple value.
name_list: TokenStream,

/// `"name: Type, ..."` in nice format.
formatted_types: String,
}

impl SignalParams {
fn new(params: &[FnParam]) -> Self {
use std::fmt::Write;

let mut param_list = TokenStream::new();
let mut type_list = TokenStream::new();
let mut name_list = TokenStream::new();
let mut formatted_types = String::new();
let mut first = true;

for param in params.iter() {
let param_name = safe_ident(&param.name.to_string());
let param_ty = &param.type_;

param_list.extend(quote! { #param_name: #param_ty, });
type_list.extend(quote! { #param_ty, });
name_list.extend(quote! { #param_name, });

let formatted_ty = match param_ty {
RustTy::EngineClass { inner_class, .. } => format!("Gd<{inner_class}>"),
other => other.to_string(),
};

if first {
first = false;
} else {
write!(formatted_types, ", ").unwrap();
}

write!(formatted_types, "{}: {}", param_name, formatted_ty).unwrap();
}

Self {
param_list,
type_list,
name_list,
formatted_types,
}
}
}
11 changes: 9 additions & 2 deletions godot-codegen/src/models/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ pub struct Class {
pub constants: Vec<ClassConstant>,
pub enums: Vec<Enum>,
pub methods: Vec<ClassMethod>,
pub signals: Vec<ClassSignal>,
}

impl ClassLike for Class {
Expand Down Expand Up @@ -414,8 +415,6 @@ pub struct ClassMethod {
pub surrounding_class: TyName,
}

impl ClassMethod {}

impl Function for ClassMethod {
fn common(&self) -> &FunctionCommon {
&self.common
Expand Down Expand Up @@ -443,6 +442,14 @@ impl fmt::Display for ClassMethod {

// ----------------------------------------------------------------------------------------------------------------------------------------------

pub struct ClassSignal {
pub name: String,
pub parameters: Vec<FnParam>,
pub surrounding_class: TyName,
}

// ----------------------------------------------------------------------------------------------------------------------------------------------

#[derive(Copy, Clone, Debug)]
pub enum FnDirection {
/// Godot -> Rust.
Expand Down
Loading
Loading