godot_ffi/
linux_reload_workaround.rs

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
15use std::ffi::c_void;
16use std::sync::OnceLock;
17
18static SYSTEM_THREAD_ATEXIT: OnceLock<Option<ThreadAtexitFn>> = OnceLock::new();
19static HOT_RELOADING_ENABLED: OnceLock<bool> = OnceLock::new();
20
21pub fn default_set_hot_reload() {
22    // By default, we enable hot reloading for debug builds, as it's likely that the user may want hot reloading in debug builds.
23    // Release builds however should avoid leaking memory, so we disable hot reloading support by default.
24    // In the future, this might consider the .gdextension `is_reloadable` flag, or whether Godot is using an editor or export build.
25    if cfg!(debug_assertions) {
26        enable_hot_reload()
27    } else {
28        disable_hot_reload()
29    }
30}
31
32// ----------------------------------------------------------------------------------------------------------------------------------------------
33// Publicly accessible
34
35#[macro_export]
36macro_rules! register_hot_reload_workaround {
37    () => {
38        #[no_mangle]
39        #[doc(hidden)]
40        pub unsafe extern "C" fn __cxa_thread_atexit_impl(
41            func: *mut ::std::ffi::c_void,
42            obj: *mut ::std::ffi::c_void,
43            dso_symbol: *mut ::std::ffi::c_void,
44        ) {
45            $crate::linux_reload_workaround::thread_atexit(func, obj, dso_symbol);
46        }
47    };
48}
49
50type ThreadAtexitFn = unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void);
51
52pub fn system_thread_atexit() -> &'static Option<ThreadAtexitFn> {
53    SYSTEM_THREAD_ATEXIT.get_or_init(|| unsafe {
54        let name = c"__cxa_thread_atexit_impl".as_ptr();
55        std::mem::transmute(libc::dlsym(libc::RTLD_NEXT, name))
56    })
57}
58
59pub fn is_hot_reload_enabled() -> bool {
60    // Assume hot reloading is disabled unless something else has been specified already. This is the better default as thread local storage
61    // destructors exist for good reasons.
62    // This is needed for situations like unit-tests, where we may create TLS-destructors without explicitly calling any of the methods
63    // that set hot reloading to be enabled or disabled.
64    *HOT_RELOADING_ENABLED.get_or_init(|| false)
65}
66
67/// Turns glibc's TLS destructor register function, `__cxa_thread_atexit_impl`,
68/// into a no-op if hot reloading is enabled.
69///
70/// # Safety
71/// This needs to be public for symbol visibility reasons, but you should
72/// never need to call this yourself
73pub unsafe fn thread_atexit(func: *mut c_void, obj: *mut c_void, dso_symbol: *mut c_void) {
74    if is_hot_reload_enabled() {
75        // Avoid registering TLS destructors on purpose, to avoid
76        // double-frees and general crashiness
77    } else if let Some(system_thread_atexit) = *system_thread_atexit() {
78        // Hot reloading is disabled, and system provides `__cxa_thread_atexit_impl`,
79        // so forward the call to it.
80        // SAFETY: Is only called by the system when thread_atexit should be called.
81        unsafe { system_thread_atexit(func, obj, dso_symbol) };
82    } else {
83        // Hot reloading is disabled *and* we don't have `__cxa_thread_atexit_impl`,
84        // throw hands up in the air and leak memory.
85    }
86}
87
88// ----------------------------------------------------------------------------------------------------------------------------------------------
89// Implementation
90
91fn enable_hot_reload() {
92    // If hot reloading is enabled then we should properly unload the library, so this will only be called once.
93    HOT_RELOADING_ENABLED
94        .set(true)
95        .expect("hot reloading should only be set once")
96}
97
98fn disable_hot_reload() {
99    // If hot reloading is disabled then we may call this method multiple times.
100    _ = HOT_RELOADING_ENABLED.set(false)
101}