godotengine / godotengine/godot
Hot reloading in Linux/macOS fails if dynamic library is not unloaded
- Dominant language
- C++
- Stars
- 117k
- Forks
- 26.8k
- PR merge metrics
- PR metrics pending
Description
### Tested versions
4.3.dev.custom_build.29b3d9e9e
### System information
Linux (WSL2), Linux + macOS GitHub Action runners
### Issue description
If a GDExtension dynamic library meets certain conditions, the Linux operating system will not fully unload `.so` files. Attempting to hot-reload will thus access a stale `.so` version, not applying changes on the binding side.
We have reproduced this in Rust simply by using the [`ThreadId`](https://doc.rust-lang.org/std/thread/struct.ThreadId.html) type. Here's the link to a downstream PR that works around the problem by removing this type: https://github.com/godot-rust/gdext/pull/653. Obviously this is not a robust solution, since users may use that type as well.
I do not fully understand _what_ precisely causes the problem in this case, but there is a [detailed article](https://fasterthanli.me/articles/so-you-want-to-live-reload-rust#what-can-prevent-dlclose-from-unloading-a-library) that goes into possible reasons on why `dlclose()` may not unload a library. Most likely it has to do with thread-local destructors that are inserted by the compiler.
---
Now I haven't yet tried to reproduce this in C or C++, but I have the feeling this is not a Rust-only issue, as other languages may use the same underlying OS APIs and thread abstractions. Maybe other users have made similar experiences?
I did not experience problems on macOS myself (only GitHub action CI), but users have reported hot reloading to not work reliably there, either.
If this issue is confirmed, we should maybe consider using the Windows approach of copying dynamic libraries on all OSes. It seems like for `.so`, renaming the file is the only way to 100% counter any "caching" or "not-yet-unloaded" situations. I'm open to suggestions though.
### Steps to reproduce
In Rust, use `ThreadId` on the binding side. Recompile a file with any changes, e.g. printing something different.
Hot-reload will _appear_ to work: no errors from `GDExtensionManager.reload_extension()`.
However, changes in the `.so` are not picked up.
### Minimal reproduction project (MRP)
N/A
Contributor guide
Research direction
Start at GDExtensionManager.reload_extension() and compare the existing Windows dynamic-library handling with the Linux and macOS behavior described here. Reproduce with a Rust binding that uses ThreadId, then verify that recompiling the library and changing its output causes hot-reload to load the new .so rather than a stale version.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, linux, macos, rust
- Domain
- game-dev, operating-systems
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100