Signals hold a RefCount, which doesn't match the GDScript behaviour
- Dominant language
- Rust
- Stars
- 5.2k
- Forks
- 312
- Avg merge
- 11h 10m
- Merged PRs (30d)
- 10
Description
I have an `Emitter` that emits `Watchers`. The `Watcher` is `RefCounted`. When it is dropped, they are expected to untangle and remove themselves from the emitter, through a Drop trait. It's not working because the `Emitter` seems to be holding on to a reference to it. As far as I can tell, this only happens when the `Emitter` has a signal connection to the `Watcher`. Without the signal, this doesn't happen.
This does not match a GDScript equivalent. When a the last RefCounted reference is lost (or a Node is freed, etc), a signal doesn't hold it open. I have some sample code of what this looks like.
```rust
use godot::prelude::*;
#[derive(GodotClass)]
#[class(base=RefCounted,no_init)]
struct DropEmitter {
#[export]
label: GString,
base: Base,
}
#[godot_api]
impl IRefCounted for DropEmitter {}
#[godot_api]
impl DropEmitter {
#[func]
fn make_with_label(label: GString) -> Gd {
Gd::from_init_fn(|base| Self { label, base })
}
#[signal]
fn trigger_watchers();
#[func]
fn emit_watcher(&mut self, watcher: GString) -> Gd {
let watch_label = format!("{}:{}", self.label, watcher);
let t = DropWatcher::make_with_label(GString::from(&watch_label));
self.signals()
.trigger_watchers()
.connect_other(&t, DropWatcher::emit_test_signal);
t
}
}
#[derive(GodotClass)]
#[class(base=RefCounted,no_init)]
struct DropWatcher {
label: GString,
base: Base,
}
#[godot_api]
impl IRefCounted for DropWatcher {}
#[godot_api]
impl DropWatcher {
#[func]
fn make_with_label(label: GString) -> Gd {
Gd::from_init_fn(|base| Self { label, base })
}
#[func]
fn emit_test_signal(&mut self) {
let l = self.label.clone();
self.signals().test_signal().emit(&l);
}
#[signal]
fn test_signal(label: GString);
}
impl Drop for DropWatcher {
fn drop(&mut self) {
godot_print!("{} dropped!", self.label);
}
}
```
GDScript Testing
```gdscript
extends Node
class Emitter:
signal trigger
func make_watcher(label:String)->DropWatcher:
var output = DropWatcher.make_with_label(label)
trigger.connect(output.emit_test_signal)
return output
var drop_emitter = DropEmitter.make_with_label("test emitter")
func _ready() -> void:
print("==== test: drop watcher_inside ====")
var watcher_inside := drop_emitter.emit_watcher("watcher inside emitter")
watcher_inside = null
print("==== test: drop pure_watcher ====")
var pure_watcher := DropWatcher.make_with_label("pure watcher")
pure_watcher = null
print("==== test: drop side_watcher ====")
var side_watcher := DropWatcher.make_with_label("side attached watcher")
drop_emitter.trigger_watchers.connect(side_watcher.emit_test_signal)
side_watcher = null
print("==== test: drop emitter ====")
drop_emitter = null
print("==== test: try gd script version ====`")
var emitter = Emitter.new()
var gd_script_watcher = emitter.make_watcher("gd script watcher")
print("==== test: drop gd watcher ====`")
gd_script_watcher = null
print("==== test: drop gd emitter ====`")
emitter = null
pass
```
Contributor guide
Research direction
Start by reproducing the Rust and GDScript examples in the issue, focusing on signal connections to RefCounted watchers and when their Drop behavior runs. Compare the watcher lifetime after its last external reference is cleared; done when Rust signal connections no longer keep the watcher alive, matching the GDScript behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- game-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100