godot_ffi/binding/
single_threaded.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//! Non-thread safe binding storage.
9//!
10//! If used from different threads then there will be runtime errors in debug mode and UB in release mode.
11
12use std::cell::Cell;
13
14use super::GodotBinding;
15use crate::ManualInitCell;
16
17pub(super) struct BindingStorage {
18    // No threading when linking against Godot with a nothreads Wasm build.
19    // Therefore, we just need to check if the bindings were initialized, as all accesses are from the main thread.
20    initialized: Cell<bool>,
21    binding: ManualInitCell<GodotBinding>,
22}
23
24impl BindingStorage {
25    /// Get the static binding storage.
26    ///
27    /// # Safety
28    ///
29    /// You must not access `binding` from a thread different from the thread [`initialize`](BindingStorage::initialize) was first called from.
30    #[inline(always)]
31    unsafe fn storage() -> &'static Self {
32        static BINDING: BindingStorage = BindingStorage {
33            initialized: Cell::new(false),
34            binding: ManualInitCell::new(),
35        };
36
37        &BINDING
38    }
39
40    /// Returns whether the binding storage has already been initialized.
41    ///
42    /// It is recommended to use this function for that purpose as the field to check varies depending on the compilation target.
43    fn initialized(&self) -> bool {
44        self.initialized.get()
45    }
46
47    /// Marks the binding storage as initialized or deinitialized.
48    /// We store the thread ID to ensure future accesses to the binding only come from the main thread.
49    ///
50    /// # Safety
51    /// Must be called from the main thread. Additionally, the binding storage must be initialized immediately
52    /// after this function if `initialized` is `true`, or deinitialized if it is `false`.
53    ///
54    /// # Panics
55    /// If attempting to deinitialize before initializing, or vice-versa.
56    unsafe fn set_initialized(&self, initialized: bool) {
57        if initialized == self.initialized() {
58            if initialized {
59                panic!("already initialized");
60            } else {
61                panic!("deinitialize without prior initialize");
62            }
63        }
64
65        self.initialized.set(initialized);
66    }
67
68    /// Initialize the binding storage, this must be called before any other public functions.
69    ///
70    /// # Safety
71    /// Must be called from the main thread.
72    ///
73    /// # Panics
74    /// If called while already initialized. Note that calling it after `deinitialize()` is possible, e.g. for Linux hot-reload.
75    pub unsafe fn initialize(binding: GodotBinding) {
76        // SAFETY: Either we are the first call to `initialize` and so we are calling from the same thread as ourselves. Or we are a later call,
77        // in which case we can tell that the storage has been initialized, and we don't access `binding`.
78        let storage = unsafe { Self::storage() };
79
80        // SAFETY: We are about to initialize the binding below, so marking the binding as initialized is correct.
81        // If we can't initialize the binding at this point, we get a panic before changing the status, thus the
82        // binding won't be set.
83        unsafe { storage.set_initialized(true) };
84
85        // SAFETY: We are the first thread to set this binding (possibly after deinitialize), as otherwise the above set() would fail and
86        // return early. We also know initialize() is not called concurrently with anything else that can call another method on the binding,
87        // since this method is called from the main thread and so must any other methods.
88        unsafe { storage.binding.set(binding) };
89    }
90
91    /// Deinitialize the binding storage.
92    ///
93    /// # Safety
94    /// Must be called from the main thread.
95    ///
96    /// # Panics
97    /// If called while not initialized.
98    pub unsafe fn deinitialize() {
99        // SAFETY: We only call this once no other operations happen anymore, i.e. no other access to the binding.
100        let storage = unsafe { Self::storage() };
101
102        // SAFETY: We are about to deinitialize the binding below, so marking the binding as deinitialized is correct.
103        // If we can't deinitialize the binding at this point, we get a panic before changing the status, thus the
104        // binding won't be deinitialized.
105        unsafe { storage.set_initialized(false) };
106
107        // SAFETY: We are the only thread that can access the binding, and we know that it's initialized.
108        unsafe {
109            storage.binding.clear();
110        }
111    }
112
113    /// Get the binding from the binding storage.
114    ///
115    /// # Safety
116    /// - Must be called from the main thread.
117    /// - The binding must be initialized.
118    #[inline(always)]
119    pub unsafe fn get_binding_unchecked() -> &'static GodotBinding {
120        // SAFETY: The bindings were initialized on the main thread because `initialize` must be called from the main thread,
121        // and this function is called from the main thread.
122        let storage = unsafe { Self::storage() };
123
124        Self::ensure_main_thread();
125
126        // SAFETY: This function can only be called when the binding is initialized and from the main thread, so we know that it's initialized.
127        unsafe { storage.binding.get_unchecked() }
128    }
129
130    pub fn is_initialized() -> bool {
131        // SAFETY: We don't access the binding.
132        let storage = unsafe { Self::storage() };
133
134        storage.initialized()
135    }
136
137    fn ensure_main_thread() {
138        // Check that we're on the main thread. Only enabled with balanced+ safeguards and, for Wasm, in threaded builds.
139        // In wasm_nothreads, there's only one thread, so no check is needed.
140        #[cfg(all(safeguards_balanced, not(wasm_nothreads)))] #[cfg_attr(published_docs, doc(cfg(all(safeguards_balanced, not(wasm_nothreads)))))]
141        if !crate::is_main_thread() {
142            // If a binding is accessed the first time, this will panic and start unwinding. It can then happen that during unwinding,
143            // another FFI call happens (e.g. Godot destructor), which would cause immediate abort, swallowing the error message.
144            // Thus check if a panic is already in progress.
145
146            if std::thread::panicking() {
147                eprintln!(
148                    "ERROR: Attempted to access binding from different thread than main thread; this is UB.\n\
149                    Cannot panic because panic unwind is already in progress. Please check surrounding messages to fix the bug."
150                );
151            } else {
152                panic!(
153                    "attempted to access binding from different thread than main thread; \
154                    this is UB - use the \"experimental-threads\" feature."
155                )
156            };
157        }
158    }
159}
160
161// SAFETY: We ensure that `binding` is only ever accessed from the same thread that initialized it.
162unsafe impl Sync for BindingStorage {}
163// SAFETY: We ensure that `binding` is only ever accessed from the same thread that initialized it.
164unsafe impl Send for BindingStorage {}
165
166// ----------------------------------------------------------------------------------------------------------------------------------------------
167
168pub struct GdextConfig {
169    pub tool_only_in_editor: bool,
170    is_editor: std::cell::OnceCell<bool>,
171}
172
173impl GdextConfig {
174    pub fn new(tool_only_in_editor: bool) -> Self {
175        Self {
176            tool_only_in_editor,
177            is_editor: std::cell::OnceCell::new(),
178        }
179    }
180
181    pub fn is_editor_or_init(&self, is_editor: impl FnOnce() -> bool) -> bool {
182        *self.is_editor.get_or_init(is_editor)
183    }
184}