godotengine / godotengine/godot

Hot reloading in Linux/macOS fails if dynamic library is not unloaded

Open
#90,108 10 comments 4 reactions 0 assignees View on GitHub
bug topic:gdextension
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.