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}