Skip to content

Conversation

JoeyEamigh
Copy link

I have been testing this patch for the past two days on both MacOS Sonoma and MacOS Tahoe, and it seems to fix the issues that I was seeing with hot reload in #1364. I unfortunately do not have a Windows machine to test on, so I am not sure about knock-on effects of this code change. If this change adversely affects Windows, it would likely be prudent to split the MacOS hot reload out similar to the Linux hot reload.

Thank you for this amazing project! Let me know if there is anything I can do, or if I need to split out the hot reload functionality for MacOS.

Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for your contribution! 👍

I wrote down a question about reload semantics, maybe it's also a misunderstanding on my side.

fn clear(&mut self) {
self.entries.clear();
// Don't clear entries - ClassId objects with old indices may still exist and need to access them.
// This is especially important during hot reload where static ClassIds persist.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you happen to find out why static ClassIds persist across hot reloads? This indicates that the library is not properly unloaded before being reloaded.

I fear while this may fix this specific problem, there are many instances of other statics: in godot-rust, in other Rust libraries, and in user code. I don't see it realistic to expect from the user to not use static (or at least not without custom cleanup logic), if this works well on other platforms.

So I'm wondering if this is is not just fighting symptoms, and it's somehow possible to fix the root cause 🤔 what do you think?

Copy link
Author

@JoeyEamigh JoeyEamigh Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MacOS doesn't allow for unloading dylibs, which makes this a difficult problem to solve.

I definitely agree that the dylib is not unloading properly, but from my experimentation this seemed to apply to editor-level code. If I initialized tracing_subscriber in the impl ExtensionLibrary, there would already be a global subscriber on next reload. When I moved the subscriber to only init on the Scene level though, I stopped having collisions. I didn't spend that much time looking into it though, and made other changes around that time so it's possible I elided the [tracing-subscriber] bug another way.

Simply clearing the Thread ID allows for hot reloading (as I was hitting a set() panic), and I stopped clearing the ClassIds because the editor would note non-fatal reference errors after the reload. It doesn't seem to cause any Id collision however, so I chalked it up to an acceptable slow memory leak in development.

I can attach a debugger and dig further into this in the next few days for sure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoeyEamigh can you replicate your issue with our hot-reloading demo project? Because I can't. Hot reloading works fine with our example even on macOS, so I suspect that there is some specific code that breaks hot reloading.

@GodotRust
Copy link

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

@JoeyEamigh
Copy link
Author

JoeyEamigh commented Oct 14, 2025

@TitanNano (bringing this out of the code review comments), you were correct that the hot-reload demo does not trigger the crash. I dug into it and was able to create a very simple additional test case for the hot-reload demo that causes the crash consistently for me. master...JoeyEamigh:gdext:test_hot_reload_gdextension_edited.

Basically, I discovered that the crash occurs when the rust.gdextension file has been dirtied since the last time the Godot window was focused. I am not quite clear on why the mechanism for reloading when a dylib has changed vs reloading when a .gdextension file has changed is different. It's clear that the mechanism is not the same, however. After the .gdextension file is changed, __gdext_load_library is called through GDExtensionManager::reload_extensions. This is seemingly not the case when the dylib has changed, as we make it all the way to GDExtension::finish_reload, even though we still started in GDExtensionManager::reload_extensions. As far as I can tell, loader->open_library(p_path) is called either way, but for some reason when just the dylib has changed my __gdext_load_library breakpoint isn't hit again.

I am currently unsure if this is related to the non-fatal godot INTERNAL ERROR: Unable to recreate GDExtension instance - does this extension support hot reloading? message that I get after a successful reload in the hot-reload example. From what I can tell with the debugger it's because Object::reset_internal_extension (core/object/object.cpp:2224) is being called twice. I haven't seen this error in my own projects, but it's showing for me in the hot-reload, so thought it was worth mentioning.

Not quite sure what the right thing to do here is, but hopefully with the test case you will be able to see where I'm coming from? Bromeon, you are definitely correct that my initial PR's removal of clearing ClassIds is just fighting the symptoms. The MAIN_THREAD_ID.clear(); part of the patch, while working excellently, certainly implies that something is amiss. Would it be helpful to PR the test case?

Logs on `cargo build -p hot-reload --features godot/api-4-5,godot/__debug-log` reload
[Rust]      Deinit level Editor
Unregister classes of level Editor...
[Rust]      Deinit level Scene
Unregister classes of level Scene...
Unregister class: Reloadable
Class Reloadable unloaded
Unregister class: NoDefault
    Storage::mark_destroyed_by_godot:  2126629652202 (lcy=Destroying)
    Storage::drop:        2126629652202
Class NoDefault unloaded
[Rust]      Deinit level Servers
Unregister classes of level Servers...
[Rust]      Deinit level Core
Unregister classes of level Core...
2025-10-13 19:58:20.683780-0400 godot.macos.editor.dev.arm64[10222:3122952] [AMCP] 128259         
Initialize gdext...
Godot version against which gdext was compiled: v4.5.stable.official
Godot version of GDExtension API at runtime: GDExtensionGodotVersion { major: 4, minor: 5, patch: 1, string: 0x109484eb5 }
Loaded interface.
Loaded global method table.
Loaded utility function table.
Loaded builtin method table.
Assigned binding.
Initialize godot-rust (API v4.5.stable.official, runtime v4.5.1.rc.custom_build)
Load class method table for level 'Core'...
Core level: loaded 6 classes and 215 methods in 0.000798875s.
Auto-register classes at level `Core`...
All classes for level `Core` auto-registered.
[Rust]      Init level Core
Load class method table for level 'Servers'...
Servers level: loaded 1 classes and 503 methods in 0.002265333s.
Auto-register classes at level `Servers`...
All classes for level `Servers` auto-registered.
[Rust]      Init level Servers
Load class method table for level 'Scene'...
Scene level: loaded 21 classes and 827 methods in 0.00328675s.
Check Godot precision setting...
Is double precision: Godot=single, gdext=single
Auto-register classes at level `Scene`...
Register class:   Reloadable at level `Scene`
Register class:   NoDefault at level `Scene`
   Register fn:   Reloadable::get_number
   Register fn:   Reloadable::from_string
   Register fn:   Reloadable::get_favorite_planet
   Register fn:   Reloadable::set_favorite_planet
Class Reloadable loaded.
   Register fn:   NoDefault::obtain
Class NoDefault loaded.
All classes for level `Scene` auto-registered.
[Rust]      Init level Scene
Load class method table for level 'Editor'...
Editor level: loaded 2 classes and 34 methods in 0.000118667s.
Auto-register classes at level `Editor`...
All classes for level `Editor` auto-registered.
[Rust]      Init level Editor
2025-10-13 20:00:13.453191-0400 godot.macos.editor.dev.arm64[10222:3122464] [ERROR] core/object/object.cpp:2224:reset_internal_extension(): Unable to recreate GDExtension instance - does this extension support hot reloading? Parameter "_extension_instance" is null.
ERROR: Unable to recreate GDExtension instance - does this extension support hot reloading?
   at: reset_internal_extension (core/object/object.cpp:2224)
2025-10-13 20:00:13.453561-0400 godot.macos.editor.dev.arm64[10222:3122952] [AMCP] 128259          
2025-10-13 20:00:13.579387-0400 godot.macos.editor.dev.arm64[10222:3122464] [miscellany] CLIENT ERROR: TUINSRemoteViewController does not override -viewServiceDidTerminateWithError: and thus cannot react to catastrophic errors beyond logging them
    Storage::construct:   Base { id: 2304686162611, class: Node }  (T=hot_reload::NoDefault)
    Storage::construct:   Base { id: 2304669468424, class: Node }  (T=hot_reload::Reloadable)
    Storage::mark_destroyed_by_godot:  2304669468424 (lcy=Destroying)
    Storage::drop:        2304669468424
RawGd::drop:      RawGd { id: 2304686162611, class: NoDefault }
Call stack to `GDExtensionManager::reload_extensions` on `cargo build -p hot-reload --features godot/api-4-5,godot/__debug-log` reload
GDExtension::finish_reload() (/Users/joey/src/godot/core/extension/gdextension.cpp:1041)
GDExtensionManager::reload_extension(String const&) (/Users/joey/src/godot/core/extension/gdextension_manager.cpp:191)
GDExtensionManager::reload_extensions() (/Users/joey/src/godot/core/extension/gdextension_manager.cpp:341)
void call_with_variant_args_helper<GDExtensionManager>(GDExtensionManager*, void (GDExtensionManager::*)(), Variant const**, Callable::CallError&, IndexSequence<...>) (/Users/joey/src/godot/core/variant/binder_common.h:223)
void call_with_variant_args<GDExtensionManager>(GDExtensionManager*, void (GDExtensionManager::*)(), Variant const**, int, Callable::CallError&) (/Users/joey/src/godot/core/variant/binder_common.h:337)
CallableCustomMethodPointer<GDExtensionManager, void>::call(Variant const**, int, Variant&, Callable::CallError&) const (/Users/joey/src/godot/core/object/callable_method_pointer.h:103)
Callable::callp(Variant const**, int, Variant&, Callable::CallError&) const (/Users/joey/src/godot/core/variant/callable.cpp:57)
CallQueue::_call_function(Callable const&, Variant const*, int, bool) (/Users/joey/src/godot/core/object/message_queue.cpp:220)
CallQueue::flush() (/Users/joey/src/godot/core/object/message_queue.cpp:268)
SceneTree::physics_process(double) (/Users/joey/src/godot/scene/main/scene_tree.cpp:645)
Main::iteration() (/Users/joey/src/godot/main/main.cpp:4747)
invocation function for block in OS_MacOS_NSApp::start_main() (/Users/joey/src/godot/platform/macos/os_macos.mm:1102)
[macos stuff]
Logs on `touch rust.gdextension` reload
[Rust]      Deinit level Editor
Unregister classes of level Editor...
[Rust]      Deinit level Scene
Unregister classes of level Scene...
Unregister class: Reloadable
Class Reloadable unloaded
Unregister class: NoDefault
    Storage::mark_destroyed_by_godot:  2126528988903 (lcy=Destroying)
    Storage::drop:        2126528988903
Class NoDefault unloaded
[Rust]      Deinit level Servers
Unregister classes of level Servers...
[Rust]      Deinit level Core
Unregister classes of level Core...
Initialize gdext...
Godot version against which gdext was compiled: v4.5.stable.official
[print_error] [panic godot-ffi/src/toolbox.rs:469]
  unsafe precondition(s) violated: hint::unreachable_unchecked must never be reached
  
  This indicates a bug in the program. This Undefined Behavior check is optional, and cannot be relied on for safety.
(backtrace disabled, run application with `RUST_BACKTRACE=1` environment variable)
thread caused non-unwinding panic. aborting.
Call stack to `__gdext_load_library` on `touch rust.gdextension` reload (lightly cleaned)
[panic stuff]
core::hint::unreachable_unchecked::precondition_check (/Users/joey/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ub_checks.rs:72)
core::hint::unreachable_unchecked (/Users/joey/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ub_checks.rs:77)
godot_ffi::toolbox::manual_init_cell::ManualInitCell<T>::set (/Users/joey/src/gdext/godot-ffi/src/toolbox.rs:469)
godot_ffi::initialize (/Users/joey/src/gdext/godot-ffi/src/lib.rs:201)
godot_core::init::__gdext_load_library::{{closure}} (/Users/joey/src/gdext/godot-core/src/init/mod.rs:68)
[rust catch_unwind stuff]
godot_core::init::__gdext_load_library (/Users/joey/src/gdext/godot-core/src/init/mod.rs:115)
gdext_rust_init (/Users/joey/src/gdext/itest/hot-reload/rust/src/lib.rs:12)
GDExtensionLibraryLoader::initialize(void (* (*)(char const*))(), Ref<GDExtension> const&, GDExtensionInitialization*) (/Users/joey/src/godot/core/extension/gdextension_library_loader.cpp:226)
GDExtension::open_library(String const&, Ref<GDExtensionLoader> const&) (/Users/joey/src/godot/core/extension/gdextension.cpp:741)
GDExtensionManager::reload_extension(String const&) (/Users/joey/src/godot/core/extension/gdextension_manager.cpp:181)
GDExtensionManager::reload_extensions() (/Users/joey/src/godot/core/extension/gdextension_manager.cpp:341)
void call_with_variant_args_helper<GDExtensionManager>(GDExtensionManager*, void (GDExtensionManager::*)(), Variant const**, Callable::CallError&, IndexSequence<...>) (/Users/joey/src/godot/core/variant/binder_common.h:223)
void call_with_variant_args<GDExtensionManager>(GDExtensionManager*, void (GDExtensionManager::*)(), Variant const**, int, Callable::CallError&) (/Users/joey/src/godot/core/variant/binder_common.h:337)
CallableCustomMethodPointer<GDExtensionManager, void>::call(Variant const**, int, Variant&, Callable::CallError&) const (/Users/joey/src/godot/core/object/callable_method_pointer.h:103)
Callable::callp(Variant const**, int, Variant&, Callable::CallError&) const (/Users/joey/src/godot/core/variant/callable.cpp:57)
CallQueue::_call_function(Callable const&, Variant const*, int, bool) (/Users/joey/src/godot/core/object/message_queue.cpp:220)
CallQueue::flush() (/Users/joey/src/godot/core/object/message_queue.cpp:268)
SceneTree::physics_process(double) (/Users/joey/src/godot/scene/main/scene_tree.cpp:645)
Main::iteration() (/Users/joey/src/godot/main/main.cpp:4747)
invocation function for block in OS_MacOS_NSApp::start_main() (/Users/joey/src/godot/platform/macos/os_macos.mm:1102)
[macos stuff]

(EDIT by Yarwin – formatting in last <details> section)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants