Skip to content

Commit 66d2c61

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

File tree

8 files changed

+90
-11
lines changed

8 files changed

+90
-11
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

+12
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,18 @@ 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+
#[cfg(feature = "experimental-wasm-nothreads")]
187+
if std::env::var("CARGO_CFG_TARGET_FAMILY")
188+
.is_ok_and(|families| families.split(',').any(|family| family == "wasm"))
189+
{
190+
println!(r#"cargo:rustc-cfg=wasm_nothreads"#);
191+
}
192+
}
193+
182194
// Function for safely removal of build directory. Workaround for errors happening during CI builds:
183195
// https://github.com/godot-rust/gdext/issues/616
184196
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

+65-10
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,53 @@ 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+
/// Marks the binding storage as initialized or deinitialized.
53+
/// We store the thread ID to ensure future accesses to the binding only come from the main thread.
54+
///
55+
/// # Safety
56+
/// Must be called from the main thread. Additionally, the binding storage must be initialized immediately
57+
/// after this function if `initialized` is `true`, or deinitialized if it is `false`.
58+
///
59+
/// # Panics
60+
/// If attempting to deinitialize before initializing, or vice-versa.
61+
unsafe fn set_initialized(&self, initialized: bool) {
62+
#[cfg(wasm_nothreads)]
63+
{
64+
if !initialized && !self.initialized.get() {
65+
panic!("deinitialize without prior initialize");
66+
}
67+
68+
// 'std::thread::current()' fails when linking to a Godot web build without threads. When compiling to wasm-nothreads,
69+
// we assume it is impossible to have multi-threading, so checking if we are in the main thread is not needed.
70+
// Therefore, we don't store the thread ID, but rather just whether initialization already occurred.
71+
self.initialized.set(initialized);
72+
}
73+
74+
#[cfg(not(wasm_nothreads))]
75+
{
76+
if initialized {
77+
self.main_thread_id.set(Some(std::thread::current().id()));
78+
} else {
79+
self.main_thread_id
80+
.get()
81+
.expect("deinitialize without prior initialize");
82+
83+
self.main_thread_id.set(None);
84+
}
85+
}
86+
}
87+
4088
/// Initialize the binding storage, this must be called before any other public functions.
4189
///
4290
/// # Safety
@@ -49,9 +97,10 @@ impl BindingStorage {
4997
// in which case we can tell that the storage has been initialized, and we don't access `binding`.
5098
let storage = unsafe { Self::storage() };
5199

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

56105
// SAFETY: We are the first thread to set this binding (possibly after deinitialize), as otherwise the above set() would fail and
57106
// return early. We also know initialize() is not called concurrently with anything else that can call another method on the binding,
@@ -70,12 +119,10 @@ impl BindingStorage {
70119
// SAFETY: We only call this once no other operations happen anymore, i.e. no other access to the binding.
71120
let storage = unsafe { Self::storage() };
72121

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

80127
// SAFETY: We are the only thread that can access the binding, and we know that it's initialized.
81128
unsafe {
@@ -92,7 +139,10 @@ impl BindingStorage {
92139
pub unsafe fn get_binding_unchecked() -> &'static GodotBinding {
93140
let storage = Self::storage();
94141

95-
if cfg!(debug_assertions) {
142+
// We only check if we are in the main thread in debug builds if we aren't building for a non-threaded Godot build,
143+
// since we could otherwise assume there won't be multi-threading.
144+
#[cfg(all(debug_assertions, not(wasm_nothreads)))]
145+
{
96146
let main_thread_id = storage.main_thread_id.get().expect(
97147
"Godot engine not available; make sure you are not calling it from unit/doc tests",
98148
);
@@ -111,6 +161,11 @@ impl BindingStorage {
111161
pub fn is_initialized() -> bool {
112162
// SAFETY: We don't access the binding.
113163
let storage = unsafe { Self::storage() };
164+
165+
#[cfg(wasm_nothreads)]
166+
return storage.initialized.get();
167+
168+
#[cfg(not(wasm_nothreads))]
114169
storage.main_thread_id.get().is_some()
115170
}
116171
}

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)