Skip to content

A hot-reloaded extension never releases its bindings on engine singletons, so the editor crashes at quit #2084

Open
godotengine/godot
#125122

Description

@DragonStorm97

Godot version

4.8-dev7 (official Windows build)

godot-cpp version or commit hash

694a264 (also reproduced at 5ed72a0; the code involved is unchanged in master at e103d34)

System information

Windows 11, MSVC, x86_64, editor build of the extension

Issue description

After an editor hot reload of a reloadable extension, the editor crashes with an access violation (0xC0000005) at quit. This happens every time, provided the reloaded image has touched any engine singleton.

The cause. godot-cpp releases the instance bindings it put on engine singletons (Time, ResourceUID, IP, Engine, anything reached through get_singleton()) only in ClassDB::deinitialize(GDEXTENSION_INITIALIZATION_CORE) (src/core/class_db.cpp:447-461). That call is reached only when the CORE level's initialisation count returns to zero (src/godot.cpp:212-215, :244-248).

  • A first load works. It initialises from CORE, so at quit the count goes from 1 to 0 and the bindings are released.
  • A reloaded image does not. The engine initialises it from the extension's minimum level, not from CORE (core/extension/gdextension_manager.cpp:47-56). At quit its CORE count goes from 0 to -1, so the release is skipped.

The bindings therefore stay on the engine's objects. Godot closes the library in unregister_core_types (core/register_core_types.cpp:440, gdextension.cpp:877-880) and only afterwards deletes ResourceUID, IP and Time (:442, :444, :492). ~Object then calls each binding's free_callback (core/object/object.cpp:2353-2357), which is godot-cpp's _gde_binding_free_callback in an image that is no longer mapped.

The engine clears a reloadable extension's tracked bindings on a reload (gdextension_manager.cpp:185-189), but never at shutdown.

Steps to reproduce

  1. Build an extension with entry_symbol minimum level SCENE and reloadable = true. Make it call something like godot::Time::get_singleton()->get_ticks_msec() after it is initialised.
  2. Open its project in the editor, then run GDExtensionManager.reload_extension("res://<its .gdextension>") (or trigger a hot reload by rebuilding).
  3. Quit the editor. It exits with 0xC0000005. Without the reload it exits with 0.

We saw it in 3 of 3 runs at 694a264 and 5 of 5 at 5ed72a0.

Workaround

The extension's terminator calls godot::ClassDB::deinitialize(GDEXTENSION_INITIALIZATION_CORE) itself when the image never received a CORE initialisation. With that, the same runs exit 0 (10 of 10). A second call after a normal release does nothing, because engine_singletons is already empty (src/core/class_db.cpp:451-455).

Suggested fix

Either of these:

  • In deinitialize_level, release the singleton bindings when the library's last initialised level is torn down, whatever that level is.
  • Have the engine initialise a reloaded library from CORE.

Minimal reproduction project (MRP)

Not attached. The steps above are the whole of it: any reloadable extension that touches an engine singleton reproduces it.

Activity

  1. dsnopek commented on Nov 3, 2026

    @dsnopek
    Collaborator

    Does this PR #1908 help for you?

    This could be a duplicate of #1900

  2. added
    bugThis has been identified as a bug
    on Nov 3, 2026
  3. dsnopek commented on Nov 3, 2026

    @dsnopek
    Collaborator
    • The engine initialises it from the extension's minimum level, not from CORE (core/extension/gdextension_manager.cpp:47-56)

    This refers to this code:

    	if (level >= 0) { // Already initialized up to some level.
    		int32_t minimum_level = 0;
    		if (!p_first_load) {
    			minimum_level = p_extension->get_minimum_library_initialization_level();
    			if (minimum_level < MIN(level, GDExtension::INITIALIZATION_LEVEL_SCENE)) {
    				return LOAD_STATUS_NEEDS_RESTART;
    			}
    		}
    		// Initialize up to current level.
    		for (int32_t i = minimum_level; i <= level; i++) {
    			p_extension->initialize_library(GDExtension::InitializationLevel(i));
    		}
    	}

    I agree that this does seem wrong.

    I think the comparison and returning LOAD_STATUS_NEEDS_RELOAD is fine. But calling initialize_library() starting from the minimum initialization level seems wrong - we shouldn't be skipping earlier levels.

    UPDATE: Here's a DRAFT PR that I think is correct, but I need to dig into it some more: godotengine/godot#125122

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugThis has been identified as a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions