Skip to content

Commit 82a14f6

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

File tree

7 files changed

+164
-13
lines changed

7 files changed

+164
-13
lines changed

godot-core/src/init/mod.rs

+24
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_reload_workaround::default_set_hot_reload(),
33+
Some(true) => sys::linux_reload_workaround::enable_hot_reload(),
34+
Some(false) => sys::linux_reload_workaround::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

+3
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

+15-13
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

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

godot-macros/src/gdextension.rs

+3
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

+25
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)