Skip to content

Commit 4f06a2a

Browse files
committed
Add faster-than-lime's workaround for hot reloading unloading library
Add thread-id checking back in
1 parent 824cb6c commit 4f06a2a

File tree

7 files changed

+167
-13
lines changed

7 files changed

+167
-13
lines changed

godot-core/src/init/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ pub unsafe fn __gdext_load_library<E: ExtensionLibrary>(
2424
init: *mut sys::GDExtensionInitialization,
2525
) -> sys::GDExtensionBool {
2626
let init_code = || {
27+
// Make sure the first thing we do is check whether hot reloading should be enabled or not. This is to ensure that if we do anything to
28+
// cause TLS-destructors to run then we have a setting already for how to deal with them. Otherwise this could cause the default
29+
// behavior to kick in and disable hot reloading.
30+
#[cfg(target_os = "linux")]
31+
match E::override_hot_reload() {
32+
None => sys::linux::default_set_hot_reload(),
33+
Some(true) => sys::linux::enable_hot_reload(),
34+
Some(false) => sys::linux::disable_hot_reload(),
35+
}
36+
2737
let tool_only_in_editor = match E::editor_run_behavior() {
2838
EditorRunBehavior::ToolClassesOnly => true,
2939
EditorRunBehavior::AllClasses => false,
@@ -209,6 +219,20 @@ pub unsafe trait ExtensionLibrary {
209219
fn on_level_deinit(level: InitLevel) {
210220
// Nothing by default.
211221
}
222+
223+
/// Whether to enable hot reloading of this library. Return `None` to use the default behavior.
224+
///
225+
/// Enabling this will ensure that the library can be hot reloaded. If this is disabled then hot reloading may still work, but there is no
226+
/// guarantee. Enabling this may also lead to memory leaks, so it should not be enabled for builds that are intended to be final builds.
227+
///
228+
/// By default this is enabled for debug builds and disabled for release builds.
229+
///
230+
/// Note that this is only checked *once* upon initializing the library. Changing this from `true` to `false` will be picked up as the
231+
/// library is then fully reloaded upon hot-reloading, however changing it from `false` to `true` is almost certainly not going to work
232+
/// unless hot-reloading is already working regardless of this setting.
233+
fn override_hot_reload() -> Option<bool> {
234+
None
235+
}
212236
}
213237

214238
/// Determines if and how an extension's code is run in the editor.

godot-ffi/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ trace = []
2121
[dependencies]
2222
paste = "1"
2323

24+
[target.'cfg(target_os = "linux")'.dependencies]
25+
libc = "0.2.153"
26+
2427
[target.'cfg(target_family = "wasm")'.dependencies]
2528
gensym = "0.1.1"
2629

godot-ffi/src/binding/single_threaded.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
//! If used from different threads then there will be runtime errors in debug mode and UB in release mode.
1111
1212
use std::cell::Cell;
13+
use std::thread::ThreadId;
1314

1415
use super::GodotBinding;
1516
use crate::ManualInitCell;
1617

1718
pub(super) struct BindingStorage {
1819
// Is used in to check that we've been called from the right thread, so must be thread-safe to access.
19-
is_initialized: Cell<bool>,
20+
main_thread_id: Cell<Option<ThreadId>>,
2021
binding: ManualInitCell<GodotBinding>,
2122
}
2223

@@ -29,7 +30,7 @@ impl BindingStorage {
2930
#[inline(always)]
3031
unsafe fn storage() -> &'static Self {
3132
static BINDING: BindingStorage = BindingStorage {
32-
is_initialized: Cell::new(false),
33+
main_thread_id: Cell::new(None),
3334
binding: ManualInitCell::new(),
3435
};
3536

@@ -48,11 +49,9 @@ impl BindingStorage {
4849
// in which case we can tell that the storage has been initialized, and we don't access `binding`.
4950
let storage = unsafe { Self::storage() };
5051

51-
let was_initialized = storage.is_initialized.replace(true);
52-
assert!(
53-
!was_initialized,
54-
"initialize must only be called at startup or after deinitialize"
55-
);
52+
storage
53+
.main_thread_id
54+
.set(Some(std::thread::current().id()));
5655

5756
// SAFETY: We are the first thread to set this binding (possibly after deinitialize), as otherwise the above set() would fail and
5857
// return early. We also know initialize() is not called concurrently with anything else that can call another method on the binding,
@@ -71,8 +70,12 @@ impl BindingStorage {
7170
// SAFETY: We only call this once no other operations happen anymore, i.e. no other access to the binding.
7271
let storage = unsafe { Self::storage() };
7372

74-
let was_initialized = storage.is_initialized.replace(false);
75-
assert!(was_initialized, "deinitialize without prior initialize");
73+
storage
74+
.main_thread_id
75+
.get()
76+
.expect("deinitialize without prior initialize");
77+
78+
storage.main_thread_id.set(None);
7679

7780
// SAFETY: We are the only thread that can access the binding, and we know that it's initialized.
7881
unsafe {
@@ -89,8 +92,7 @@ impl BindingStorage {
8992
pub unsafe fn get_binding_unchecked() -> &'static GodotBinding {
9093
let storage = Self::storage();
9194

92-
// FIXME currently disabled as it breaks hot-reloading on Linux, see https://github.com/godot-rust/gdext/pull/653.
93-
/*if cfg!(debug_assertions) {
95+
if cfg!(debug_assertions) {
9496
let main_thread_id = storage.main_thread_id.get().expect(
9597
"Godot engine not available; make sure you are not calling it from unit/doc tests",
9698
);
@@ -100,7 +102,7 @@ impl BindingStorage {
100102
std::thread::current().id(),
101103
"attempted to access binding from different thread than main thread; this is UB - use the \"experimental-threads\" feature."
102104
);
103-
}*/
105+
}
104106

105107
// SAFETY: This function can only be called when the binding is initialized and from the main thread, so we know that it's initialized.
106108
unsafe { storage.binding.get_unchecked() }
@@ -109,7 +111,7 @@ impl BindingStorage {
109111
pub fn is_initialized() -> bool {
110112
// SAFETY: We don't access the binding.
111113
let storage = unsafe { Self::storage() };
112-
storage.is_initialized.get()
114+
storage.main_thread_id.get().is_some()
113115
}
114116
}
115117

godot-ffi/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ mod compat;
3535
mod extras;
3636
mod global;
3737
mod godot_ffi;
38+
#[cfg(target_os = "linux")]
39+
pub mod linux;
3840
mod opaque;
3941
mod plugins;
4042
mod string_cache;

godot-ffi/src/linux.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
//! Linux-specific configuration.
9+
10+
// Avoid TLS-destructors preventing the dynamic library from being closed.
11+
// See: https://fasterthanli.me/articles/so-you-want-to-live-reload-rust#what-can-prevent-dlclose-from-unloading-a-library
12+
13+
use std::ffi::c_void;
14+
use std::sync::OnceLock;
15+
16+
pub type NextFn = unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void);
17+
18+
static SYSTEM_THREAD_ATEXIT: OnceLock<Option<NextFn>> = OnceLock::new();
19+
static HOT_RELOADING_ENABLED: OnceLock<bool> = OnceLock::new();
20+
21+
fn system_thread_atexit() -> &'static Option<NextFn> {
22+
SYSTEM_THREAD_ATEXIT.get_or_init(|| unsafe {
23+
#[allow(clippy::transmute_ptr_to_ref)]
24+
let name = c"__cxa_thread_atexit_impl".as_ptr();
25+
std::mem::transmute(libc::dlsym(
26+
libc::RTLD_NEXT,
27+
#[allow(clippy::transmute_ptr_to_ref)]
28+
name,
29+
))
30+
})
31+
}
32+
33+
pub fn enable_hot_reload() {
34+
// If hot reloading is enabled then we should properly unload the library, so this will only be called once.
35+
HOT_RELOADING_ENABLED
36+
.set(true)
37+
.expect("hot reloading should only be set once")
38+
}
39+
40+
pub fn disable_hot_reload() {
41+
// If hot reloading is disabled then we may call this method multiple times.
42+
_ = HOT_RELOADING_ENABLED.set(false)
43+
}
44+
45+
pub fn default_set_hot_reload() {
46+
// By default we enable hot reloading for debug builds, as it's likely that the user may want hot reloading in debug builds.
47+
// Release builds however should avoid leaking memory, so we disable hot reloading support by default.
48+
if cfg!(debug_assertions) {
49+
enable_hot_reload()
50+
} else {
51+
disable_hot_reload()
52+
}
53+
}
54+
55+
fn is_hot_reload_enabled() -> bool {
56+
// Assume hot reloading is disabled unless something else has been specified already. This is the better default as thread local storage
57+
// destructors exist for good reasons.
58+
// This is needed for situations like unit-tests, where we may create TLS-destructors without explicitly calling any of the methods
59+
// that set hot reloading to be enabled or disabled.
60+
*HOT_RELOADING_ENABLED.get_or_init(|| false)
61+
}
62+
63+
/// Turns glibc's TLS destructor register function, `__cxa_thread_atexit_impl`,
64+
/// into a no-op if hot reloading is enabled.
65+
///
66+
/// # Safety
67+
/// This needs to be public for symbol visibility reasons, but you should
68+
/// never need to call this yourself
69+
pub unsafe fn thread_atexit(func: *mut c_void, obj: *mut c_void, dso_symbol: *mut c_void) {
70+
if is_hot_reload_enabled() {
71+
// Avoid registering TLS destructors on purpose, to avoid
72+
// double-frees and general crashiness
73+
} else if let Some(system_thread_atexit) = *system_thread_atexit() {
74+
// Hot reloading is disabled, and system provides `__cxa_thread_atexit_impl`,
75+
// so forward the call to it.
76+
system_thread_atexit(func, obj, dso_symbol);
77+
} else {
78+
// Hot reloading is disabled *and* we don't have `__cxa_thread_atexit_impl`,
79+
// throw hands up in the air and leak memory.
80+
}
81+
}
82+
83+
#[macro_export]
84+
macro_rules! register_hot_reload_workaround {
85+
() => {
86+
#[no_mangle]
87+
pub unsafe extern "C" fn __cxa_thread_atexit_impl(
88+
func: *mut ::std::ffi::c_void,
89+
obj: *mut ::std::ffi::c_void,
90+
dso_symbol: *mut ::std::ffi::c_void,
91+
) {
92+
$crate::linux::thread_atexit(func, obj, dso_symbol);
93+
}
94+
};
95+
}

godot-macros/src/gdextension.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,8 @@ pub fn attribute_gdextension(item: venial::Item) -> ParseResult<TokenStream> {
100100
// Ensures that the init function matches the signature advertised in FFI header
101101
let _unused: ::godot::sys::GDExtensionInitializationFunction = Some(#entry_point);
102102
}
103+
104+
#[cfg(target_os = "linux")]
105+
::godot::sys::register_hot_reload_workaround!();
103106
})
104107
}

godot-macros/src/util/kv_parser.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,31 @@ impl KvParser {
161161
Ok(Some(int))
162162
}
163163

164+
#[allow(dead_code)]
165+
pub fn handle_bool(&mut self, key: &str) -> ParseResult<Option<bool>> {
166+
let Some(expr) = self.handle_expr(key)? else {
167+
return Ok(None);
168+
};
169+
170+
let mut tokens = expr.into_iter();
171+
let Some(TokenTree::Ident(id)) = tokens.next() else {
172+
return bail!(key, "missing value for '{key}' (must be bool literal)");
173+
};
174+
175+
if let Some(surplus) = tokens.next() {
176+
return bail!(
177+
key,
178+
"value for '{key}' must be bool literal; found extra {surplus:?}"
179+
);
180+
}
181+
182+
let Ok(b) = id.to_string().parse() else {
183+
return bail!(key, "value for '{key}' must be bool literal; found {id:?}");
184+
};
185+
186+
Ok(Some(b))
187+
}
188+
164189
/// Handles a key that must be provided and must have an identifier as the value.
165190
pub fn handle_ident_required(&mut self, key: &str) -> ParseResult<Ident> {
166191
self.handle_ident(key)?

0 commit comments

Comments
 (0)