Skip to content

Commit c961170

Browse files
committed
Code generation for engine-declared signals
1 parent 474abad commit c961170

File tree

15 files changed

+438
-88
lines changed

15 files changed

+438
-88
lines changed

godot-codegen/src/generator/classes.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
use crate::context::{Context, NotificationEnum};
88
use crate::generator::functions_common::{FnCode, FnDefinition, FnDefinitions};
99
use crate::generator::method_tables::MethodTableKey;
10-
use crate::generator::{constants, docs, enums, functions_common, notifications, virtual_traits};
10+
use crate::generator::{
11+
constants, docs, enums, functions_common, notifications, signals, virtual_traits,
12+
};
1113
use crate::models::domain::{
1214
ApiView, Class, ClassLike, ClassMethod, ExtensionApi, FnDirection, FnQualifier, Function,
1315
ModName, TyName,
@@ -121,6 +123,8 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
121123
builders,
122124
} = make_class_methods(class, &class.methods, &cfg_attributes, ctx);
123125

126+
let signal_types = signals::make_class_signals(class, &class.signals, ctx);
127+
124128
let enums = enums::make_enums(&class.enums, &cfg_attributes);
125129
let constants = constants::make_constants(&class.constants);
126130
let inherits_macro = format_ident!("unsafe_inherits_transitive_{}", class_name.rust_ty);
@@ -256,6 +260,7 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
256260

257261
#builders
258262
#enums
263+
#signal_types
259264
};
260265
// note: TypePtr -> ObjectPtr conversion OK?
261266

godot-codegen/src/generator/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub mod lifecycle_builtins;
2525
pub mod method_tables;
2626
pub mod native_structures;
2727
pub mod notifications;
28+
pub mod signals;
2829
pub mod utility_functions;
2930
pub mod virtual_traits;
3031

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
// Code duplication: while there is some overlap with godot-macros/signal.rs for #[signal] handling, this file here is quite a bit simpler,
9+
// as it only deals with predefined signal definitions (no user-defined #[cfg], visibility, etc). On the other hand, there is documentation
10+
// for these signals, and integration is slightly different due to lack of WithBaseField trait. Nonetheless, some parts could potentially
11+
// be extracted into a future crate shared by godot-codegen and godot-macros.
12+
13+
use crate::context::Context;
14+
use crate::conv;
15+
use crate::models::domain::{Class, ClassLike, ClassSignal, FnParam, RustTy, TyName};
16+
use crate::util::{ident, safe_ident};
17+
use proc_macro2::{Ident, TokenStream};
18+
use quote::{format_ident, quote};
19+
20+
pub fn make_class_signals(
21+
class: &Class,
22+
signals: &[ClassSignal],
23+
_ctx: &mut Context,
24+
) -> TokenStream {
25+
if signals.is_empty() {
26+
return TokenStream::new();
27+
}
28+
29+
let all_params: Vec<SignalParams> = signals
30+
.iter()
31+
.map(|s| SignalParams::new(&s.parameters))
32+
.collect();
33+
34+
let signal_collection_struct = make_signal_collection(class, signals, &all_params);
35+
36+
let signal_types = signals
37+
.iter()
38+
.zip(all_params.iter())
39+
.map(|(signal, params)| make_signal_individual_struct(signal, params));
40+
41+
quote! {
42+
#signal_collection_struct
43+
#( #signal_types )*
44+
}
45+
}
46+
47+
// Used outside, to document class with links to this type.
48+
pub fn make_collection_name(class_name: &TyName) -> Ident {
49+
format_ident!("SignalsIn{}", class_name.rust_ty)
50+
}
51+
52+
fn make_individual_struct_name(signal_name: &str) -> Ident {
53+
let signal_pascal_name = conv::to_pascal_case(signal_name);
54+
format_ident!("Sig{}", signal_pascal_name)
55+
}
56+
57+
fn make_signal_collection(
58+
class: &Class,
59+
signals: &[ClassSignal],
60+
params: &[SignalParams],
61+
) -> TokenStream {
62+
let class_name = class.name();
63+
let collection_struct_name = make_collection_name(class_name);
64+
65+
let provider_methods = signals.iter().zip(params).map(|(sig, params)| {
66+
let signal_name_str = &sig.name;
67+
let signal_name = ident(&sig.name);
68+
let individual_struct_name = make_individual_struct_name(&sig.name);
69+
let provider_docs = format!("Signature: `({})`", params.formatted_types);
70+
71+
quote! {
72+
// Important to return lifetime 'c here, not '_.
73+
#[doc = #provider_docs]
74+
pub fn #signal_name(&mut self) -> #individual_struct_name<'c> {
75+
#individual_struct_name {
76+
typed: crate::registry::signal::TypedSignal::new(self.__gd.clone(), #signal_name_str)
77+
}
78+
}
79+
}
80+
});
81+
82+
let collection_docs = format!(
83+
"A collection of signals for the [`{c}`][crate::classes::{c}] class.",
84+
c = class_name.rust_ty
85+
);
86+
87+
quote! {
88+
#[doc = #collection_docs]
89+
#[cfg(since_api = "4.2")]
90+
pub struct #collection_struct_name<'c> {
91+
__gd: &'c mut Gd<re_export::#class_name>,
92+
}
93+
94+
#[cfg(since_api = "4.2")]
95+
impl<'c> #collection_struct_name<'c> {
96+
#( #provider_methods )*
97+
}
98+
99+
#[cfg(since_api = "4.2")]
100+
impl crate::obj::WithSignals for re_export::#class_name {
101+
type SignalCollection<'c> = #collection_struct_name<'c>;
102+
#[doc(hidden)]
103+
type __SignalObject<'c> = Gd<re_export::#class_name>;
104+
105+
#[doc(hidden)]
106+
fn __signals_from_external(external: &mut Gd<Self>) -> Self::SignalCollection<'_> {
107+
Self::SignalCollection {
108+
__gd: external,
109+
}
110+
}
111+
}
112+
}
113+
}
114+
115+
fn make_signal_individual_struct(signal: &ClassSignal, params: &SignalParams) -> TokenStream {
116+
let individual_struct_name = make_individual_struct_name(&signal.name);
117+
118+
let SignalParams {
119+
param_list,
120+
type_list,
121+
name_list,
122+
..
123+
} = params;
124+
125+
let class_name = &signal.surrounding_class;
126+
let class_ty = quote! { re_export::#class_name };
127+
let param_tuple = quote! { ( #type_list ) };
128+
129+
quote! {
130+
#[cfg(since_api = "4.2")]
131+
pub struct #individual_struct_name<'c> {
132+
pub typed: crate::registry::signal::TypedSignal<'c, #class_ty, #param_tuple>,
133+
}
134+
135+
#[cfg(since_api = "4.2")]
136+
impl #individual_struct_name<'_> {
137+
pub fn emit(&mut self, #param_list) {
138+
self.typed.emit_tuple( (#name_list) );
139+
}
140+
}
141+
142+
#[cfg(since_api = "4.2")]
143+
impl<'c> std::ops::Deref for #individual_struct_name<'c> {
144+
type Target = crate::registry::signal::TypedSignal<'c, #class_ty, #param_tuple>;
145+
146+
fn deref(&self) -> &Self::Target {
147+
&self.typed
148+
}
149+
}
150+
151+
#[cfg(since_api = "4.2")]
152+
impl std::ops::DerefMut for #individual_struct_name<'_> {
153+
fn deref_mut(&mut self) -> &mut Self::Target {
154+
&mut self.typed
155+
}
156+
}
157+
}
158+
}
159+
160+
// ----------------------------------------------------------------------------------------------------------------------------------------------
161+
162+
struct SignalParams {
163+
/// `name: Type, ...`
164+
param_list: TokenStream,
165+
166+
/// `Type, ...` -- for example inside a tuple type.
167+
type_list: TokenStream,
168+
169+
/// `name, ...` -- for example inside a tuple value.
170+
name_list: TokenStream,
171+
172+
/// `"name: Type, ..."` in nice format.
173+
formatted_types: String,
174+
}
175+
176+
impl SignalParams {
177+
fn new(params: &[FnParam]) -> Self {
178+
use std::fmt::Write;
179+
180+
let mut param_list = TokenStream::new();
181+
let mut type_list = TokenStream::new();
182+
let mut name_list = TokenStream::new();
183+
let mut formatted_types = String::new();
184+
let mut first = true;
185+
186+
for param in params.iter() {
187+
let param_name = safe_ident(&param.name.to_string());
188+
let param_ty = &param.type_;
189+
190+
param_list.extend(quote! { #param_name: #param_ty, });
191+
type_list.extend(quote! { #param_ty, });
192+
name_list.extend(quote! { #param_name, });
193+
194+
let formatted_ty = match param_ty {
195+
RustTy::EngineClass { inner_class, .. } => format!("Gd<{inner_class}>"),
196+
other => other.to_string(),
197+
};
198+
199+
if first {
200+
first = false;
201+
} else {
202+
write!(formatted_types, ", ").unwrap();
203+
}
204+
205+
write!(formatted_types, "{}: {}", param_name, formatted_ty).unwrap();
206+
}
207+
208+
Self {
209+
param_list,
210+
type_list,
211+
name_list,
212+
formatted_types,
213+
}
214+
}
215+
}

godot-codegen/src/models/domain.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
use crate::context::Context;
1414
use crate::conv;
15-
use crate::models::json::{JsonMethodArg, JsonMethodReturn, JsonSignal};
15+
use crate::models::json::{JsonMethodArg, JsonMethodReturn};
1616
use crate::util::{ident, option_as_slice, safe_ident};
1717

1818
use proc_macro2::{Ident, Literal, TokenStream};

godot-codegen/src/models/domain_mapping.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ use crate::models::domain::{
1313
FunctionCommon, GodotApiVersion, ModName, NativeStructure, Operator, Singleton, TyName,
1414
UtilityFunction,
1515
};
16-
use crate::models::json::{JsonBuiltinClass, JsonBuiltinMethod, JsonBuiltinSizes, JsonClass, JsonClassConstant, JsonClassMethod, JsonConstructor, JsonEnum, JsonEnumConstant, JsonExtensionApi, JsonHeader, JsonMethodReturn, JsonNativeStructure, JsonOperator, JsonSignal, JsonSingleton, JsonUtilityFunction};
16+
use crate::models::json::{
17+
JsonBuiltinClass, JsonBuiltinMethod, JsonBuiltinSizes, JsonClass, JsonClassConstant,
18+
JsonClassMethod, JsonConstructor, JsonEnum, JsonEnumConstant, JsonExtensionApi, JsonHeader,
19+
JsonMethodReturn, JsonNativeStructure, JsonOperator, JsonSignal, JsonSingleton,
20+
JsonUtilityFunction,
21+
};
1722
use crate::util::{get_api_level, ident, option_as_slice};
1823
use crate::{conv, special_cases};
1924
use proc_macro2::Ident;
@@ -112,7 +117,7 @@ impl Class {
112117

113118
let signals = option_as_slice(&json.signals)
114119
.iter()
115-
.map(|s| {
120+
.filter_map(|s| {
116121
let surrounding_class = &ty_name;
117122
ClassSignal::from_json(s, surrounding_class, ctx)
118123
})
@@ -524,12 +529,16 @@ impl ClassSignal {
524529
json_signal: &JsonSignal,
525530
surrounding_class: &TyName,
526531
ctx: &mut Context,
527-
) -> Self {
528-
Self {
532+
) -> Option<Self> {
533+
if special_cases::is_signal_deleted(surrounding_class, json_signal) {
534+
return None;
535+
}
536+
537+
Some(Self {
529538
name: json_signal.name.clone(),
530539
parameters: FnParam::new_range(&json_signal.arguments, ctx),
531540
surrounding_class: surrounding_class.clone(),
532-
}
541+
})
533542
}
534543
}
535544

godot-codegen/src/special_cases/special_cases.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525

2626
use crate::conv::to_enum_type_uncached;
2727
use crate::models::domain::{Enum, RustTy, TyName};
28-
use crate::models::json::{JsonBuiltinMethod, JsonClassMethod, JsonUtilityFunction};
28+
use crate::models::json::{JsonBuiltinMethod, JsonClassMethod, JsonSignal, JsonUtilityFunction};
2929
use crate::special_cases::codegen_special_cases;
30+
use crate::util::option_as_slice;
3031
use crate::Context;
3132
use proc_macro2::Ident;
3233
// Deliberately private -- all checks must go through `special_cases`.
@@ -536,6 +537,14 @@ pub fn is_builtin_method_deleted(_class_name: &TyName, method: &JsonBuiltinMetho
536537
codegen_special_cases::is_builtin_method_excluded(method)
537538
}
538539

540+
/// True if signal is absent from codegen (only when surrounding class is excluded).
541+
pub fn is_signal_deleted(_class_name: &TyName, signal: &JsonSignal) -> bool {
542+
// If any argument type (a class) is excluded.
543+
option_as_slice(&signal.arguments)
544+
.iter()
545+
.any(|arg| codegen_special_cases::is_class_excluded(&arg.type_))
546+
}
547+
539548
/// True if builtin type is excluded (`NIL` or scalars)
540549
pub fn is_builtin_type_deleted(class_name: &TyName) -> bool {
541550
let name = class_name.godot_ty.as_str();

godot-core/src/obj/gd.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -711,12 +711,17 @@ where
711711
/// Access user-defined signals of this object.
712712
///
713713
/// For classes that have at least one `#[signal]` defined, returns a collection of signal names. Each returned signal has a specialized
714-
/// API for connecting and emitting signals in a type-safe way. This method is the equivalent of [`WithSignals::signals()`], but when
715-
/// called externally (not from `self`). If you are within the `impl` of a class, use `self.signals()` directly instead.
714+
/// API for connecting and emitting signals in a type-safe way. This method is the equivalent of [`WithUserSignals::signals()`], but when
715+
/// called externally (not from `self`). Furthermore, this is also available for engine classes, not just user-defined ones.
716+
///
717+
/// When you are within the `impl` of a class, use `self.signals()` directly instead.
716718
///
717719
/// If you haven't already, read the [book chapter about signals](https://godot-rust.github.io/book/register/signals.html) for a
718720
/// walkthrough.
719-
pub fn signals(&self) -> T::SignalCollection<'_> {
721+
///
722+
/// [`WithUserSignals::signals()`]: crate::obj::WithUserSignals::signals()
723+
#[cfg(since_api = "4.2")]
724+
pub fn signals(&mut self) -> T::SignalCollection<'_> {
720725
T::__signals_from_external(self)
721726
}
722727
}

0 commit comments

Comments
 (0)