use std::sync::atomic::{AtomicBool, Ordering};
use super::GodotBinding;
use crate::ManualInitCell;
pub(super) struct BindingStorage {
initialized: AtomicBool,
binding: ManualInitCell<GodotBinding>,
}
impl BindingStorage {
#[inline(always)]
unsafe fn storage() -> &'static Self {
static BINDING: BindingStorage = BindingStorage {
initialized: AtomicBool::new(false),
binding: ManualInitCell::new(),
};
&BINDING
}
fn initialized(&self) -> bool {
self.initialized.load(Ordering::Acquire)
}
pub unsafe fn initialize(binding: GodotBinding) {
let storage = unsafe { Self::storage() };
assert!(!storage.initialized(), "already initialized");
unsafe { storage.binding.set(binding) };
storage.initialized.store(true, Ordering::Release);
}
pub unsafe fn deinitialize() {
let storage = unsafe { Self::storage() };
assert!(
storage.initialized(),
"deinitialize without prior initialize"
);
storage.initialized.store(false, Ordering::Release);
unsafe { storage.binding.clear() };
}
#[inline(always)]
pub unsafe fn get_binding_unchecked() -> &'static GodotBinding {
let storage = unsafe { Self::storage() };
#[cfg(safeguards_balanced)] #[cfg_attr(published_docs, doc(cfg(safeguards_balanced)))]
assert_binding_live(&storage.initialized);
unsafe { storage.binding.get_unchecked() }
}
pub fn is_initialized() -> bool {
let storage = unsafe { Self::storage() };
storage.initialized()
}
pub(super) fn ensure_main_thread() {
#[cfg(all(safeguards_balanced, not(wasm_nothreads)))] #[cfg_attr(published_docs, doc(cfg(all(safeguards_balanced, not(wasm_nothreads)))))]
if !crate::is_main_thread() {
if std::thread::panicking() {
eprintln!(
"ERROR: Attempted to access binding from different thread than main thread; this is UB.\n\
Cannot panic because panic unwind is already in progress. Please check surrounding messages to fix the bug."
);
} else {
panic!(
"attempted to access binding from different thread than main thread; \
this is UB - use the \"experimental-threads\" feature."
)
};
}
}
}
unsafe impl Sync for BindingStorage {}
unsafe impl Send for BindingStorage {}
#[cfg(safeguards_balanced)] #[cfg_attr(published_docs, doc(cfg(safeguards_balanced)))]
#[inline(always)]
fn assert_binding_live(initialized: &AtomicBool) {
if !initialized.load(Ordering::Acquire) {
not_live_panic();
}
}
#[cfg(safeguards_balanced)] #[cfg_attr(published_docs, doc(cfg(safeguards_balanced)))]
#[cold]
#[inline(never)]
fn not_live_panic() -> ! {
panic!(
"Godot binding accessed before initialization or after deinitialization. \
This typically means a `#[ctor]`/`#[dtor]` constructor, a library destructor, or a leftover user thread touched the Godot API \
outside the engine's load/unload window."
)
}
#[cfg(all(test, safeguards_balanced))] #[cfg_attr(published_docs, doc(cfg(all(test, safeguards_balanced))))]
mod tests {
use super::*;
#[test]
fn live_check_passes_when_initialized() {
assert_binding_live(&AtomicBool::new(true));
}
#[test]
#[should_panic(expected = "accessed before initialization or after deinitialization")]
fn live_check_panics_when_not_initialized() {
assert_binding_live(&AtomicBool::new(false));
}
}
pub struct GdextConfig {
pub tool_only_in_editor: bool,
}
impl GdextConfig {
pub fn new(tool_only_in_editor: bool) -> Self {
Self {
tool_only_in_editor,
}
}
}