diff --git a/godot-bindings/Cargo.toml b/godot-bindings/Cargo.toml index c1351fc9f..c119857aa 100644 --- a/godot-bindings/Cargo.toml +++ b/godot-bindings/Cargo.toml @@ -15,6 +15,8 @@ homepage = "https://godot-rust.github.io" # requiring no-default-features), so we unfortunately still need to depend on prebuilt and just ignore it. # The artifact generator explicitly excludes that though (to avoid a quasi-circular dependency back to its repo). [features] +experimental-wasm-nothreads = [] + # [version-sync] [[ # [line] api-$kebabVersion = [] api-4-0 = [] diff --git a/godot-bindings/src/lib.rs b/godot-bindings/src/lib.rs index dffc55830..76490116f 100644 --- a/godot-bindings/src/lib.rs +++ b/godot-bindings/src/lib.rs @@ -179,6 +179,22 @@ pub fn emit_godot_version_cfg() { } } +/// Emit `#[cfg(wasm_nothreads)]` flag when compiling to Wasm with the "experimental-wasm-nothreads" feature. +pub fn emit_wasm_nothreads_cfg() { + println!(r#"cargo:rustc-check-cfg=cfg(wasm_nothreads, values(none()))"#); + + // The environment variable for target family has a list of applicable families separated by commas. + // For Emscripten in particular, this can be "unix,wasm". Therefore, to check for the Wasm target, we must check each item in the list. + #[cfg(feature = "experimental-wasm-nothreads")] + if std::env::var("CARGO_CFG_TARGET_FAMILY") + .expect("target family environment variable") + .split(',') + .any(|family| family == "wasm") + { + println!(r#"cargo:rustc-cfg=wasm_nothreads"#); + } +} + // Function for safely removal of build directory. Workaround for errors happening during CI builds: // https://github.com/godot-rust/gdext/issues/616 pub fn remove_dir_all_reliable(path: &Path) { diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index 7cddfb62b..1f3a48eba 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -21,6 +21,7 @@ codegen-lazy-fptrs = [ double-precision = ["godot-codegen/double-precision"] experimental-godot-api = ["godot-codegen/experimental-godot-api"] experimental-threads = ["godot-ffi/experimental-threads"] +experimental-wasm-nothreads = ["godot-ffi/experimental-wasm-nothreads"] debug-log = ["godot-ffi/debug-log"] trace = [] diff --git a/godot-ffi/Cargo.toml b/godot-ffi/Cargo.toml index 539ae1d0e..730e0c518 100644 --- a/godot-ffi/Cargo.toml +++ b/godot-ffi/Cargo.toml @@ -15,6 +15,7 @@ codegen-rustfmt = ["godot-codegen/codegen-rustfmt"] codegen-lazy-fptrs = ["godot-codegen/codegen-lazy-fptrs"] experimental-godot-api = ["godot-codegen/experimental-godot-api"] experimental-threads = [] +experimental-wasm-nothreads = ["godot-bindings/experimental-wasm-nothreads"] debug-log = [] api-custom = ["godot-bindings/api-custom"] diff --git a/godot-ffi/build.rs b/godot-ffi/build.rs index 082b80243..aa08ea282 100644 --- a/godot-ffi/build.rs +++ b/godot-ffi/build.rs @@ -28,4 +28,5 @@ fn main() { println!("cargo:rerun-if-changed=build.rs"); godot_bindings::emit_godot_version_cfg(); + godot_bindings::emit_wasm_nothreads_cfg(); } diff --git a/godot-ffi/src/binding/single_threaded.rs b/godot-ffi/src/binding/single_threaded.rs index 77174aa2d..271659aa4 100644 --- a/godot-ffi/src/binding/single_threaded.rs +++ b/godot-ffi/src/binding/single_threaded.rs @@ -10,13 +10,21 @@ //! If used from different threads then there will be runtime errors in debug mode and UB in release mode. use std::cell::Cell; + +#[cfg(not(wasm_nothreads))] use std::thread::ThreadId; use super::GodotBinding; use crate::ManualInitCell; pub(super) struct BindingStorage { + // No threading when linking against Godot with a nothreads Wasm build. + // Therefore, we just need to check if the bindings were initialized, as all accesses are from the main thread. + #[cfg(wasm_nothreads)] + initialized: Cell, + // Is used in to check that we've been called from the right thread, so must be thread-safe to access. + #[cfg(not(wasm_nothreads))] main_thread_id: Cell>, binding: ManualInitCell, } @@ -30,6 +38,10 @@ impl BindingStorage { #[inline(always)] unsafe fn storage() -> &'static Self { static BINDING: BindingStorage = BindingStorage { + #[cfg(wasm_nothreads)] + initialized: Cell::new(false), + + #[cfg(not(wasm_nothreads))] main_thread_id: Cell::new(None), binding: ManualInitCell::new(), }; @@ -37,6 +49,48 @@ impl BindingStorage { &BINDING } + /// Returns whether the binding storage has already been initialized. + /// + /// It is recommended to use this function for that purpose as the field to check varies depending on the compilation target. + fn initialized(&self) -> bool { + #[cfg(wasm_nothreads)] + return self.initialized.get(); + + #[cfg(not(wasm_nothreads))] + self.main_thread_id.get().is_some() + } + + /// Marks the binding storage as initialized or deinitialized. + /// We store the thread ID to ensure future accesses to the binding only come from the main thread. + /// + /// # Safety + /// Must be called from the main thread. Additionally, the binding storage must be initialized immediately + /// after this function if `initialized` is `true`, or deinitialized if it is `false`. + /// + /// # Panics + /// If attempting to deinitialize before initializing, or vice-versa. + unsafe fn set_initialized(&self, initialized: bool) { + if initialized == self.initialized() { + if initialized { + panic!("already initialized"); + } else { + panic!("deinitialize without prior initialize"); + } + } + + // 'std::thread::current()' fails when linking to a Godot web build without threads. When compiling to wasm-nothreads, + // we assume it is impossible to have multi-threading, so checking if we are in the main thread is not needed. + // Therefore, we don't store the thread ID, but rather just whether initialization already occurred. + #[cfg(wasm_nothreads)] + self.initialized.set(initialized); + + #[cfg(not(wasm_nothreads))] + { + let thread_id = initialized.then(|| std::thread::current().id()); + self.main_thread_id.set(thread_id); + } + } + /// Initialize the binding storage, this must be called before any other public functions. /// /// # Safety @@ -49,9 +103,10 @@ impl BindingStorage { // in which case we can tell that the storage has been initialized, and we don't access `binding`. let storage = unsafe { Self::storage() }; - storage - .main_thread_id - .set(Some(std::thread::current().id())); + // SAFETY: We are about to initialize the binding below, so marking the binding as initialized is correct. + // If we can't initialize the binding at this point, we get a panic before changing the status, thus the + // binding won't be set. + unsafe { storage.set_initialized(true) }; // SAFETY: We are the first thread to set this binding (possibly after deinitialize), as otherwise the above set() would fail and // return early. We also know initialize() is not called concurrently with anything else that can call another method on the binding, @@ -70,12 +125,10 @@ impl BindingStorage { // SAFETY: We only call this once no other operations happen anymore, i.e. no other access to the binding. let storage = unsafe { Self::storage() }; - storage - .main_thread_id - .get() - .expect("deinitialize without prior initialize"); - - storage.main_thread_id.set(None); + // SAFETY: We are about to deinitialize the binding below, so marking the binding as deinitialized is correct. + // If we can't deinitialize the binding at this point, we get a panic before changing the status, thus the + // binding won't be deinitialized. + unsafe { storage.set_initialized(false) }; // SAFETY: We are the only thread that can access the binding, and we know that it's initialized. unsafe { @@ -92,7 +145,10 @@ impl BindingStorage { pub unsafe fn get_binding_unchecked() -> &'static GodotBinding { let storage = Self::storage(); - if cfg!(debug_assertions) { + // We only check if we are in the main thread in debug builds if we aren't building for a non-threaded Godot build, + // since we could otherwise assume there won't be multi-threading. + #[cfg(all(debug_assertions, not(wasm_nothreads)))] + { let main_thread_id = storage.main_thread_id.get().expect( "Godot engine not available; make sure you are not calling it from unit/doc tests", ); @@ -111,7 +167,8 @@ impl BindingStorage { pub fn is_initialized() -> bool { // SAFETY: We don't access the binding. let storage = unsafe { Self::storage() }; - storage.main_thread_id.get().is_some() + + storage.initialized() } } diff --git a/godot/Cargo.toml b/godot/Cargo.toml index f49208ad4..c8393c4af 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -18,6 +18,7 @@ double-precision = ["godot-core/double-precision"] experimental-godot-api = ["godot-core/experimental-godot-api"] experimental-threads = ["godot-core/experimental-threads"] experimental-wasm = [] +experimental-wasm-nothreads = ["godot-core/experimental-wasm-nothreads"] codegen-rustfmt = ["godot-core/codegen-rustfmt"] lazy-function-tables = ["godot-core/codegen-lazy-fptrs"] serde = ["godot-core/serde"] diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 301cd4dd5..612b43bc8 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -68,7 +68,7 @@ //! * **`api-custom`** //! //! Sets the [**API level**](https://godot-rust.github.io/book/toolchain/godot-version.html) to the specified Godot version, -//! or a custom-built local binary. +//! or a custom-built local binary. //! You can use at most one `api-*` feature. If absent, the current Godot minor version is used, with patch level 0.

//! //! * **`double-precision`** @@ -124,6 +124,12 @@ pub mod __docs; #[cfg(all(feature = "lazy-function-tables", feature = "experimental-threads"))] compile_error!("Thread safety for lazy function pointers is not yet implemented."); +#[cfg(all( + feature = "experimental-wasm-nothreads", + feature = "experimental-threads" +))] +compile_error!("Cannot use 'experimental-threads' with a nothreads Wasm build yet."); + #[cfg(all(target_family = "wasm", not(feature = "experimental-wasm")))] compile_error!("Must opt-in using `experimental-wasm` Cargo feature; keep in mind that this is work in progress");