Skip to content

Commit dc9ba89

Browse files
authored
Merge pull request #1146 from godot-rust/qol/withsignals-everywhere
User classes expose typed-signal API even without `#[signal]`
2 parents 086f7e6 + 99b76e6 commit dc9ba89

File tree

9 files changed

+165
-20
lines changed

9 files changed

+165
-20
lines changed

godot-core/src/obj/gd.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,8 @@ impl<T: GodotClass> Gd<T> {
326326

327327
/// Equivalent to [`upcast::<Object>()`][Self::upcast], but without bounds.
328328
// Not yet public because it might need _mut/_ref overloads, and 6 upcast methods are a bit much...
329-
pub(crate) fn upcast_object(self) -> Gd<classes::Object> {
329+
#[doc(hidden)] // no public API, but used by #[signal].
330+
pub fn upcast_object(self) -> Gd<classes::Object> {
330331
self.owned_cast()
331332
.expect("Upcast to Object failed. This is a bug; please report it.")
332333
}

godot-macros/src/class/data_models/inherent_impl.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,17 @@ struct SignalAttr {
7171
pub no_builder: bool,
7272
}
7373

74-
// ----------------------------------------------------------------------------------------------------------------------------------------------
75-
76-
pub struct InherentImplAttr {
74+
pub(crate) struct InherentImplAttr {
7775
/// For implementation reasons, there can be a single 'primary' impl block and 0 or more 'secondary' impl blocks.
7876
/// For now, this is controlled by a key in the 'godot_api' attribute.
7977
pub secondary: bool,
78+
79+
/// When typed signal generation is explicitly disabled by the user.
80+
pub no_typed_signals: bool,
8081
}
8182

83+
// ----------------------------------------------------------------------------------------------------------------------------------------------
84+
8285
/// Codegen for `#[godot_api] impl MyType`
8386
pub fn transform_inherent_impl(
8487
meta: InherentImplAttr,
@@ -107,8 +110,12 @@ pub fn transform_inherent_impl(
107110

108111
// For each #[func] in this impl block, create one constant.
109112
let func_name_constants = make_funcs_collection_constants(&funcs, &class_name);
110-
let (signal_registrations, signal_symbol_types) =
111-
make_signal_registrations(&signals, &class_name, &class_name_obj)?;
113+
let (signal_registrations, signal_symbol_types) = make_signal_registrations(
114+
&signals,
115+
&class_name,
116+
&class_name_obj,
117+
meta.no_typed_signals,
118+
)?;
112119

113120
#[cfg(feature = "codegen-full")]
114121
let rpc_registrations = crate::class::make_rpc_registrations_fn(&class_name, &funcs);
@@ -206,6 +213,23 @@ pub fn transform_inherent_impl(
206213
}
207214
}
208215

216+
/* Re-enable if we allow controlling declarative macros for signals (base_field_macro, visibility_macros).
217+
fn extract_hint_attribute(impl_block: &mut venial:: Impl) -> ParseResult<GodotApiHints> {
218+
// #[hint(has_base_field = BOOL)]
219+
let has_base_field;
220+
if let Some(mut hints) = KvParser::parse_remove(&mut impl_block.attributes, "hint")? {
221+
has_base_field = hints.handle_bool("has_base_field")?;
222+
} else {
223+
has_base_field = None;
224+
}
225+
226+
// #[hint(class_visibility = pub(crate))]
227+
// ...
228+
229+
Ok(GodotApiHints { has_base_field })
230+
}
231+
*/
232+
209233
fn process_godot_fns(
210234
class_name: &Ident,
211235
impl_block: &mut venial::Impl,

godot-macros/src/class/data_models/signal.rs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ pub fn make_signal_registrations(
182182
signals: &[SignalDefinition],
183183
class_name: &Ident,
184184
class_name_obj: &TokenStream,
185+
no_typed_signals: bool,
185186
) -> ParseResult<(Vec<TokenStream>, Option<TokenStream>)> {
186187
let mut signal_registrations = Vec::new();
187188

@@ -210,8 +211,11 @@ pub fn make_signal_registrations(
210211
signal_registrations.push(registration);
211212
}
212213

214+
// Rewrite the above using #[cfg].
213215
#[cfg(since_api = "4.2")]
214-
let signal_symbols = make_signal_symbols(class_name, collection_api);
216+
let signal_symbols =
217+
(!no_typed_signals).then(|| make_signal_symbols(class_name, collection_api));
218+
215219
#[cfg(before_api = "4.2")]
216220
let signal_symbols = None;
217221

@@ -311,7 +315,7 @@ impl SignalCollection {
311315
.push(make_signal_individual_struct(details))
312316
}
313317

314-
pub fn is_empty(&self) -> bool {
318+
fn is_empty(&self) -> bool {
315319
self.individual_structs.is_empty()
316320
}
317321
}
@@ -380,18 +384,26 @@ fn make_signal_individual_struct(details: &SignalDetails) -> TokenStream {
380384

381385
/// Generates symbolic API for signals:
382386
/// * collection (unspecified-name struct holding methods to access each signal)
387+
/// * can be absent if the class declares no own #[signal]s.
383388
/// * individual signal types
384389
/// * trait impls
385390
fn make_signal_symbols(
386391
class_name: &Ident,
387392
collection_api: SignalCollection,
388393
// max_visibility: SignalVisibility,
389-
) -> Option<TokenStream> {
390-
// Future note: if we add Rust->Rust inheritance, then the WithSignals trait must be unconditionally implemented.
391-
if collection_api.is_empty() {
392-
return None;
393-
}
394+
) -> TokenStream {
395+
// Earlier implementation generated a simplified code when no #[signal] was declared: only WithSignals/WithUserSignals impl, but no own
396+
// collection, instead the associated type pointing to the base class. This has however some problems:
397+
// * Part of the reason for user-defined collection is to store UserSignalObject instead of Gd, which can store &mut self.
398+
// This is necessary for self.signals().some_base_signal().emit(), if such a signal is connected to Self::method_mut;
399+
// Gd would cause a borrow error.
400+
// * Once we add Rust-Rust inheritance, we'd need to differentiate case again, which can be tricky since #[godot_api] has no information
401+
// about the base class.
402+
//
403+
// As compromise, we always generate a collection struct if either at least 1 #[signal] is declared or the struct has a Base<T> field.
404+
// We also provide opt-out via #[godot_api(no_typed_signals)].
394405

406+
let declares_no_signals = collection_api.is_empty();
395407
let collection_struct_name = format_ident!("__godot_Signals_{}", class_name);
396408
let collection_struct_methods = &collection_api.provider_methods;
397409
let with_signals_impl = make_with_signals_impl(class_name, &collection_struct_name);
@@ -431,7 +443,7 @@ fn make_signal_symbols(
431443

432444
let visibility_macro = util::format_class_visibility_macro(class_name);
433445

434-
let code = quote! {
446+
let mut code = quote! {
435447
#visibility_macro! {
436448
#[allow(non_camel_case_types)]
437449
#[doc(hidden)] // Only on struct, not methods, to allow completion in IDEs.
@@ -455,9 +467,21 @@ fn make_signal_symbols(
455467
#( #individual_structs )*
456468
};
457469

458-
Some(code)
470+
// base_field_macro! is a macro that expands to all input tokens if the class declares a Base<T> field, and to nothing otherwise.
471+
// This makes sure that WithSignals is only implemented for classes with a base field, and avoids compile errors about it.
472+
// Only when no #[signal] is declared -> otherwise the user explicitly requests it, and a compile error is better to guide them.
473+
if declares_no_signals {
474+
let base_field_macro = util::format_class_base_field_macro(class_name);
475+
476+
code = quote! {
477+
#base_field_macro! { #code }
478+
};
479+
}
480+
481+
code
459482
}
460483

484+
/// Declare `impl WithSignals` and `impl WithUserSignals` with own signal collection.
461485
fn make_with_signals_impl(class_name: &Ident, collection_struct_name: &Ident) -> TokenStream {
462486
quote! {
463487
impl ::godot::obj::WithSignals for #class_name {
@@ -467,10 +491,10 @@ fn make_with_signals_impl(class_name: &Ident, collection_struct_name: &Ident) ->
467491
type __SignalObj<'c> = ::godot::private::UserSignalObject<'c, Self>;
468492

469493
#[doc(hidden)]
470-
fn __signals_from_external(external: &mut Gd<Self>) -> Self::SignalCollection<'_, Self> {
494+
fn __signals_from_external(external: &mut ::godot::obj::Gd<Self>) -> Self::SignalCollection<'_, Self> {
471495
Self::SignalCollection {
472496
__internal_obj: Some(::godot::private::UserSignalObject::External {
473-
gd: external.clone().upcast::<Object>()
497+
gd: external.clone().upcast_object()
474498
})
475499
}
476500
}

godot-macros/src/class/derive_godot_class.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
142142
pub struct #funcs_collection_struct_name {}
143143
};
144144

145+
// Note: one limitation is that macros don't work for `impl nested::MyClass` blocks.
145146
let visibility_macro = make_visibility_macro(class_name, class.vis_marker.as_ref());
147+
let base_field_macro = make_base_field_macro(class_name, fields.base_field.is_some());
146148

147149
Ok(quote! {
148150
impl ::godot::obj::GodotClass for #class_name {
@@ -174,6 +176,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
174176
#user_class_impl
175177
#init_expecter
176178
#visibility_macro
179+
#base_field_macro
177180
#( #deprecations )*
178181
#( #errors )*
179182

@@ -211,6 +214,25 @@ fn make_visibility_macro(
211214
}
212215
}
213216

217+
/// Generates code for a decl-macro, which evaluates to nothing if the class has no base field.
218+
fn make_base_field_macro(class_name: &Ident, has_base_field: bool) -> TokenStream {
219+
let macro_name = util::format_class_base_field_macro(class_name);
220+
221+
if has_base_field {
222+
quote! {
223+
macro_rules! #macro_name {
224+
( $( $tt:tt )* ) => { $( $tt )* };
225+
}
226+
}
227+
} else {
228+
quote! {
229+
macro_rules! #macro_name {
230+
( $( $tt:tt )* ) => {};
231+
}
232+
}
233+
}
234+
}
235+
214236
/// Checks at compile time that a function with the given name exists on `Self`.
215237
#[must_use]
216238
pub fn make_existence_check(ident: &Ident) -> TokenStream {

godot-macros/src/class/godot_api.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,20 @@ fn parse_inherent_impl_attr(meta: TokenStream) -> Result<super::InherentImplAttr
1717
let item = venial_parse_meta(&meta, format_ident!("godot_api"), &quote! { fn func(); })?;
1818
let mut attr = KvParser::parse_required(item.attributes(), "godot_api", &meta)?;
1919
let secondary = attr.handle_alone("secondary")?;
20+
let no_typed_signals = attr.handle_alone("no_typed_signals")?;
2021
attr.finish()?;
2122

22-
Ok(super::InherentImplAttr { secondary })
23+
if no_typed_signals && secondary {
24+
return bail!(
25+
meta,
26+
"#[godot_api]: keys `secondary` and `no_typed_signals` are mutually exclusive; secondary blocks allow no signals anyway"
27+
)?;
28+
}
29+
30+
Ok(super::InherentImplAttr {
31+
secondary,
32+
no_typed_signals,
33+
})
2334
}
2435

2536
pub fn attribute_godot_api(

godot-macros/src/util/kv_parser.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,16 @@ impl KvParser {
3838
}
3939

4040
/// Create a new parser which checks for presence of an `#[expected]` attribute.
41+
///
42+
/// Returns `Ok(None)` if the attribute is not present.
4143
pub fn parse(attributes: &[venial::Attribute], expected: &str) -> ParseResult<Option<Self>> {
4244
let mut found_attr: Option<Self> = None;
4345

4446
for attr in attributes.iter() {
4547
let path = &attr.path;
4648
if path_is_single(path, expected) {
4749
if found_attr.is_some() {
48-
return bail!(attr, "only a single #[{expected}] attribute allowed",);
50+
return bail!(attr, "only a single #[{expected}] attribute allowed");
4951
}
5052

5153
let attr_name = expected.to_string();
@@ -59,6 +61,40 @@ impl KvParser {
5961
Ok(found_attr)
6062
}
6163

64+
/// Like `parse()`, but removes the attribute from the list.
65+
///
66+
/// Useful for `#[proc_macro_attributes]`, where handled attributes must not show up in resulting code.
67+
// Currently unused.
68+
pub fn parse_remove(
69+
attributes: &mut Vec<venial::Attribute>,
70+
expected: &str,
71+
) -> ParseResult<Option<Self>> {
72+
let mut found_attr: Option<Self> = None;
73+
let mut found_index: Option<usize> = None;
74+
75+
for (i, attr) in attributes.iter().enumerate() {
76+
let path = &attr.path;
77+
if path_is_single(path, expected) {
78+
if found_attr.is_some() {
79+
return bail!(attr, "only a single #[{expected}] attribute allowed");
80+
}
81+
82+
let attr_name = expected.to_string();
83+
found_index = Some(i);
84+
found_attr = Some(Self {
85+
span: attr.tk_brackets.span,
86+
map: ParserState::parse(attr_name, &attr.value)?,
87+
});
88+
}
89+
}
90+
91+
if let Some(index) = found_index {
92+
attributes.remove(index);
93+
}
94+
95+
Ok(found_attr)
96+
}
97+
6298
pub fn span(&self) -> Span {
6399
self.span
64100
}

godot-macros/src/util/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,3 +409,8 @@ pub fn format_funcs_collection_struct(class_name: &Ident) -> Ident {
409409
pub fn format_class_visibility_macro(class_name: &Ident) -> Ident {
410410
format_ident!("__godot_{class_name}_vis_macro")
411411
}
412+
413+
/// Returns the name of the macro used to communicate whether the `struct` (class) contains a base field.
414+
pub fn format_class_base_field_macro(class_name: &Ident) -> Ident {
415+
format_ident!("__godot_{class_name}_has_base_field_macro")
416+
}

itest/rust/src/builtin_tests/containers/signal_test.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,24 @@ fn signal_symbols_engine_inherited_internal() {
327327
node.free();
328328
}
329329

330+
// Test that Node signals are accessible from a derived class, when the class itself has no #[signal] declarations.
331+
// Verifies the code path that only generates the traits, no dedicated signal collection.
332+
#[cfg(since_api = "4.2")]
333+
#[itest]
334+
fn signal_symbols_engine_inherited_no_own_signals() {
335+
let mut obj = Receiver::new_alloc();
336+
337+
let mut sig = obj.signals().property_list_changed();
338+
sig.connect_self(|this: &mut Receiver| {
339+
this.receive_int(941);
340+
});
341+
342+
obj.notify_property_list_changed();
343+
assert_eq!(obj.bind().last_received.get(), LastReceived::Int(941));
344+
345+
obj.free();
346+
}
347+
330348
#[itest]
331349
fn signal_construction_and_id() {
332350
let mut object = RefCounted::new_gd();
@@ -451,6 +469,8 @@ struct Receiver {
451469

452470
#[godot_api]
453471
impl Receiver {
472+
// Do not declare any #[signal]s here -- explicitly test this implements WithSignal without them.
473+
454474
fn last_received(&self) -> LastReceived {
455475
self.last_received.get()
456476
}

itest/rust/src/object_tests/object_test.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -997,7 +997,9 @@ pub mod object_test_gd {
997997
}
998998
use nested::ObjectTest;
999999

1000-
#[godot_api]
1000+
// Disabling signals allows nested::ObjectTest, which would fail otherwise due to generated decl-macro being out-of-scope.
1001+
#[godot_api(no_typed_signals)]
1002+
// #[hint(has_base_field = false)] // if we allow more fine-grained control in the future
10011003
impl nested::ObjectTest {
10021004
#[func]
10031005
fn pass_object(&self, object: Gd<Object>) -> i64 {

0 commit comments

Comments
 (0)