Skip to content

Add unbind() for reentrant scope - #1708

Merged
Bromeon merged 1 commit into
masterfrom
feature/reentrant-scope
Oct 4, 2026
Merged

Bromeon merged 1 commit into
masterfrom
feature/reentrant-scope

Conversation

@Bromeon

@Bromeon Bromeon commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

Addresses a long-standing confusion around re-entrancy into objects.

When you have a &mut self, call into Godot and then somehow call back into the same object, this is called re-entrant. Without special APIs like self.base_mut() or self.signals().xy().emit() (initialized via connect_self()), this causes a double-borrow panic because &mut self is an exclusive reference that cannot be borrowed again.

Closes #916.
Closes #1656.
Closes #1692.
Closes #1697.

Today, base_mut() can be used as a guard in the following way:

{
    let _guard = self.base_mut();
    // re-entrant code here
} // guard dropped

You can now do the equivalent with:

self.unbind(|this| {
    // re-entrant code here
    // `this` gives you Gd<Self::Base> ref, if needed
});

Name "unbind" because it's pretty much the reverse operation of Gd::bind_mut(), which goes from Gd<T> to &mut T. I considered "reentrant" or "reentrant_scope" but I believe this is easier to conceptualize. I'm aware of collision with Callable APIs, but those are much more niche and can also be renamed to bind_args/unbind_args or so.

Some examples, also in the doc:

Example: reentrant signal

A Player owns a Weapon. If that weapon is out of ammo, an out_of_ammo signal is emitted, which links back to the Player::reload method. Now if Player::shoot(&mut self) causes the weapon to be empty, that callback needs to access &mut self again. This wouldn't normally work since it's already borrowed, but unbind() explicitly allows it.

#[derive(GodotClass)]
#[class(init, base = Node)]
struct Player {
    #[init(node = "Weapon")]
    weapon: OnReady<Gd<Weapon>>,
    base: Base<Node>,
}

#[godot_api]
impl INode for Player {
    fn ready(&mut self) {
        self.weapon.signals()
            .out_of_ammo()
            .connect_other(&*self, Self::reload);
    }
}

#[godot_api]
impl Player {
    fn reload(&mut self) { /* ... */ }

    fn shoot(&mut self) {
        // Copy Gd<Weapon> which isn't tied to `self` (no borrows).
        let mut weapon = self.weapon.clone();

        // Weapon::fire() can trigger Player::reload(). We already
        // borrow &mut self here, so without unbind() this panics.
        self.unbind(|_| weapon.bind_mut().fire());
    }
}

Example: dynamic calls through Godot VM

Hidden turrets pop up when an alarm is raised. Turret::raise_alarm() notifies all turrets in the group via call_group(), which re-enters on_alarm() on self. Then set_visible() synchronously sends the VISIBILITY_CHANGED notification, again reentrantly.

#[derive(GodotClass)]
#[class(init, base = Node2D)]
struct Turret {
    base: Base<Node2D>,
}

#[godot_api]
impl Turret {
    #[func]
    fn on_alarm(&mut self) {}

    fn raise_alarm(&mut self) {
        self.unbind(|base| {
            base.get_tree().call_group("turrets", "on_alarm", &[]); // calls self.on_alarm().
            base.set_visible(true); // calls self.on_notification().
        });
    }
}

#[godot_api]
impl INode2D for Turret {
    fn on_notification(&mut self, what: CanvasItemNotification) {
        if what == CanvasItemNotification::VISIBILITY_CHANGED {
            // Refresh visuals.
        }
    }
}

@Bromeon Bromeon added feature Adds functionality to the library c: core Core components labels Sep 27, 2026
@GodotRust

Copy link
Copy Markdown

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1708

@TitanNano

Copy link
Copy Markdown
Contributor

I have skimmed over the changes, and I think it looks the way I expected it to look. 😄

@Bromeon
Bromeon added this pull request to the merge queue Oct 1, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 1, 2026
@Bromeon
Bromeon added this pull request to the merge queue Oct 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 4, 2026
@Bromeon
Bromeon added this pull request to the merge queue Oct 4, 2026
Merged via the queue into master with commit 0b680b4 Oct 4, 2026
17 checks passed
@Bromeon
Bromeon deleted the feature/reentrant-scope branch October 4, 2026 21:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: core Core components feature Adds functionality to the library

Projects

None yet

3 participants