Skip to content

Commit 1481ed1

Browse files
committed
add support for nothreads build to single threaded init
1 parent 79edae3 commit 1481ed1

File tree

8 files changed

+97
-12
lines changed

8 files changed

+97
-12
lines changed

godot-bindings/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ homepage = "https://godot-rust.github.io"
1515
# requiring no-default-features), so we unfortunately still need to depend on prebuilt and just ignore it.
1616
# The artifact generator explicitly excludes that though (to avoid a quasi-circular dependency back to its repo).
1717
[features]
18+
experimental-wasm-nothreads = []
19+
1820
# [version-sync] [[
1921
# [line] api-$kebabVersion = []
2022
api-4-0 = []

godot-bindings/src/lib.rs

+16
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,22 @@ pub fn emit_godot_version_cfg() {
179179
}
180180
}
181181

182+
/// Emit `#[cfg(wasm_nothreads)]` flag when compiling to Wasm with the "experimental-wasm-nothreads" feature.
183+
pub fn emit_wasm_nothreads_cfg() {
184+
println!(r#"cargo:rustc-check-cfg=cfg(wasm_nothreads, values(none()))"#);
185+
186+
// The environment variable for target family has a list of applicable families separated by commas.
187+
// For Emscripten in particular, this can be "unix,wasm". Therefore, to check for the Wasm target, we must check each item in the list.
188+
#[cfg(feature = "experimental-wasm-nothreads")]
189+
if std::env::var("CARGO_CFG_TARGET_FAMILY")
190+
.expect("target family environment variable")
191+
.split(',')
192+
.any(|family| family == "wasm")
193+
{
194+
println!(r#"cargo:rustc-cfg=wasm_nothreads"#);
195+
}
196+
}
197+
182198
// Function for safely removal of build directory. Workaround for errors happening during CI builds:
183199
// https://github.com/godot-rust/gdext/issues/616
184200
pub fn remove_dir_all_reliable(path: &Path) {

godot-core/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ codegen-lazy-fptrs = [
2121
double-precision = ["godot-codegen/double-precision"]
2222
experimental-godot-api = ["godot-codegen/experimental-godot-api"]
2323
experimental-threads = ["godot-ffi/experimental-threads"]
24+
experimental-wasm-nothreads = ["godot-ffi/experimental-wasm-nothreads"]
2425
debug-log = ["godot-ffi/debug-log"]
2526
trace = []
2627

godot-ffi/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ codegen-rustfmt = ["godot-codegen/codegen-rustfmt"]
1515
codegen-lazy-fptrs = ["godot-codegen/codegen-lazy-fptrs"]
1616
experimental-godot-api = ["godot-codegen/experimental-godot-api"]
1717
experimental-threads = []
18+
experimental-wasm-nothreads = ["godot-bindings/experimental-wasm-nothreads"]
1819
debug-log = []
1920

2021
api-custom = ["godot-bindings/api-custom"]

godot-ffi/build.rs

+1
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ fn main() {
2828
println!("cargo:rerun-if-changed=build.rs");
2929

3030
godot_bindings::emit_godot_version_cfg();
31+
godot_bindings::emit_wasm_nothreads_cfg();
3132
}

godot-ffi/src/binding/single_threaded.rs

+68-11
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,21 @@
1010
//! If used from different threads then there will be runtime errors in debug mode and UB in release mode.
1111
1212
use std::cell::Cell;
13+
14+
#[cfg(not(wasm_nothreads))]
1315
use std::thread::ThreadId;
1416

1517
use super::GodotBinding;
1618
use crate::ManualInitCell;
1719

1820
pub(super) struct BindingStorage {
21+
// No threading when linking against Godot with a nothreads Wasm build.
22+
// Therefore, we just need to check if the bindings were initialized, as all accesses are from the main thread.
23+
#[cfg(wasm_nothreads)]
24+
initialized: Cell<bool>,
25+
1926
// Is used in to check that we've been called from the right thread, so must be thread-safe to access.
27+
#[cfg(not(wasm_nothreads))]
2028
main_thread_id: Cell<Option<ThreadId>>,
2129
binding: ManualInitCell<GodotBinding>,
2230
}
@@ -30,13 +38,59 @@ impl BindingStorage {
3038
#[inline(always)]
3139
unsafe fn storage() -> &'static Self {
3240
static BINDING: BindingStorage = BindingStorage {
41+
#[cfg(wasm_nothreads)]
42+
initialized: Cell::new(false),
43+
44+
#[cfg(not(wasm_nothreads))]
3345
main_thread_id: Cell::new(None),
3446
binding: ManualInitCell::new(),
3547
};
3648

3749
&BINDING
3850
}
3951

52+
/// Returns whether the binding storage has already been initialized.
53+
///
54+
/// It is recommended to use this function for that purpose as the field to check varies depending on the compilation target.
55+
fn initialized(&self) -> bool {
56+
#[cfg(wasm_nothreads)]
57+
return self.initialized.get();
58+
59+
#[cfg(not(wasm_nothreads))]
60+
self.main_thread_id.get().is_some()
61+
}
62+
63+
/// Marks the binding storage as initialized or deinitialized.
64+
/// We store the thread ID to ensure future accesses to the binding only come from the main thread.
65+
///
66+
/// # Safety
67+
/// Must be called from the main thread. Additionally, the binding storage must be initialized immediately
68+
/// after this function if `initialized` is `true`, or deinitialized if it is `false`.
69+
///
70+
/// # Panics
71+
/// If attempting to deinitialize before initializing, or vice-versa.
72+
unsafe fn set_initialized(&self, initialized: bool) {
73+
if initialized == self.initialized() {
74+
if initialized {
75+
panic!("already initialized");
76+
} else {
77+
panic!("deinitialize without prior initialize");
78+
}
79+
}
80+
81+
// 'std::thread::current()' fails when linking to a Godot web build without threads. When compiling to wasm-nothreads,
82+
// we assume it is impossible to have multi-threading, so checking if we are in the main thread is not needed.
83+
// Therefore, we don't store the thread ID, but rather just whether initialization already occurred.
84+
#[cfg(wasm_nothreads)]
85+
self.initialized.set(initialized);
86+
87+
#[cfg(not(wasm_nothreads))]
88+
{
89+
let thread_id = initialized.then(|| std::thread::current().id());
90+
self.main_thread_id.set(thread_id);
91+
}
92+
}
93+
4094
/// Initialize the binding storage, this must be called before any other public functions.
4195
///
4296
/// # Safety
@@ -49,9 +103,10 @@ impl BindingStorage {
49103
// in which case we can tell that the storage has been initialized, and we don't access `binding`.
50104
let storage = unsafe { Self::storage() };
51105

52-
storage
53-
.main_thread_id
54-
.set(Some(std::thread::current().id()));
106+
// SAFETY: We are about to initialize the binding below, so marking the binding as initialized is correct.
107+
// If we can't initialize the binding at this point, we get a panic before changing the status, thus the
108+
// binding won't be set.
109+
unsafe { storage.set_initialized(true) };
55110

56111
// SAFETY: We are the first thread to set this binding (possibly after deinitialize), as otherwise the above set() would fail and
57112
// 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 {
70125
// SAFETY: We only call this once no other operations happen anymore, i.e. no other access to the binding.
71126
let storage = unsafe { Self::storage() };
72127

73-
storage
74-
.main_thread_id
75-
.get()
76-
.expect("deinitialize without prior initialize");
77-
78-
storage.main_thread_id.set(None);
128+
// SAFETY: We are about to deinitialize the binding below, so marking the binding as deinitialized is correct.
129+
// If we can't deinitialize the binding at this point, we get a panic before changing the status, thus the
130+
// binding won't be deinitialized.
131+
unsafe { storage.set_initialized(false) };
79132

80133
// SAFETY: We are the only thread that can access the binding, and we know that it's initialized.
81134
unsafe {
@@ -92,7 +145,10 @@ impl BindingStorage {
92145
pub unsafe fn get_binding_unchecked() -> &'static GodotBinding {
93146
let storage = Self::storage();
94147

95-
if cfg!(debug_assertions) {
148+
// We only check if we are in the main thread in debug builds if we aren't building for a non-threaded Godot build,
149+
// since we could otherwise assume there won't be multi-threading.
150+
#[cfg(all(debug_assertions, not(wasm_nothreads)))]
151+
{
96152
let main_thread_id = storage.main_thread_id.get().expect(
97153
"Godot engine not available; make sure you are not calling it from unit/doc tests",
98154
);
@@ -111,7 +167,8 @@ impl BindingStorage {
111167
pub fn is_initialized() -> bool {
112168
// SAFETY: We don't access the binding.
113169
let storage = unsafe { Self::storage() };
114-
storage.main_thread_id.get().is_some()
170+
171+
storage.initialized()
115172
}
116173
}
117174

godot/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ double-precision = ["godot-core/double-precision"]
1818
experimental-godot-api = ["godot-core/experimental-godot-api"]
1919
experimental-threads = ["godot-core/experimental-threads"]
2020
experimental-wasm = []
21+
experimental-wasm-nothreads = ["godot-core/experimental-wasm-nothreads"]
2122
codegen-rustfmt = ["godot-core/codegen-rustfmt"]
2223
lazy-function-tables = ["godot-core/codegen-lazy-fptrs"]
2324
serde = ["godot-core/serde"]

godot/src/lib.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
//! * **`api-custom`**
6969
//!
7070
//! Sets the [**API level**](https://godot-rust.github.io/book/toolchain/godot-version.html) to the specified Godot version,
71-
//! or a custom-built local binary.
71+
//! or a custom-built local binary.
7272
//! You can use at most one `api-*` feature. If absent, the current Godot minor version is used, with patch level 0.<br><br>
7373
//!
7474
//! * **`double-precision`**
@@ -124,6 +124,12 @@ pub mod __docs;
124124
#[cfg(all(feature = "lazy-function-tables", feature = "experimental-threads"))]
125125
compile_error!("Thread safety for lazy function pointers is not yet implemented.");
126126

127+
#[cfg(all(
128+
feature = "experimental-wasm-nothreads",
129+
feature = "experimental-threads"
130+
))]
131+
compile_error!("Cannot use 'experimental-threads' with a nothreads Wasm build yet.");
132+
127133
#[cfg(all(target_family = "wasm", not(feature = "experimental-wasm")))]
128134
compile_error!("Must opt-in using `experimental-wasm` Cargo feature; keep in mind that this is work in progress");
129135

0 commit comments

Comments
 (0)