-
-
Notifications
You must be signed in to change notification settings - Fork 207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for nothreads wasm builds (Godot 4.3+) #794
Conversation
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-794 |
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. | ||
#[cfg(all(target_family = "wasm", feature = "experimental-wasm-nothreads"))] | ||
main_thread_id: Cell<Option<()>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we even need the variable in case of no threads? Can't we remove it? And also the if's where this is used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, Option<()>
is bool
with extra steps 😉
Or is there existing code that just checks for presence without getting the inner value?
If we can omit the field entirely, even better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it was convenient to do this to avoid adding more cfgs where we check for initialization by checking for Some
in other methods. We can replace the field with a simple bool one, though that'll lead to more conditional compilation. Maybe it doesn't matter that much though, since it'd be restricted to just this file.
We can't remove the field entirely as we still need to check for initialization, we just don't need the thread ID itself anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would there be a lot to change?
If yes,Option<()>
is probably OK but then a comment should explain why not bool 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've changed it to bool. It wasn't that much tbh.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot! 💪
Right now, I've only applied the fix to single-threaded gdext code, that is, without the
experimental-threads
feature toggled on.
You should probably add another compile error if both features are simultaneously specified, mentioning this combination is not yet implemented. You can place it next to the existing ones:
Lines 124 to 132 in 79edae3
#[cfg(all(feature = "lazy-function-tables", feature = "experimental-threads"))] | |
compile_error!("Thread safety for lazy function pointers is not yet implemented."); | |
#[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"); | |
// See also https://github.com/godotengine/godot/issues/86346. | |
#[cfg(all(feature = "double-precision", not(feature = "api-custom")))] | |
compile_error!("The feature `double-precision` currently requires `api-custom` due to incompatibilities in the GDExtension API JSON."); |
There are now a lot of complex cfgs, like
#[cfg(any(
not(target_family = "wasm"),
not(feature = "experimental-wasm-nothreads")
))]
I wonder if we could maybe check these two features in godot-ffi/build.rs
and emit a custom cfg (see e.g. here) in their place, which is checked within godot-ffi/src
files?
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. | ||
#[cfg(all(target_family = "wasm", feature = "experimental-wasm-nothreads"))] | ||
main_thread_id: Cell<Option<()>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, Option<()>
is bool
with extra steps 😉
Or is there existing code that just checks for presence without getting the inner value?
If we can omit the field entirely, even better.
godot/Cargo.toml
Outdated
experimental-wasm = [] | ||
experimental-wasm-nothreads = [ | ||
"experimental-wasm", | ||
"godot-core/experimental-wasm-nothreads", | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about adding a compile_error!
to detect when both experimental-wasm
and experimental-wasm-nothreads
are specified, but since this is always done here, I don't think it's possible?
We would need to treat the two features separately and complicate some existing code in godot
to use #[cfg(any(...))]
instead 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove the dependency on "experimental-wasm"
and instead gate the changes behind all(target_family = "wasm", feature = "experimental-wasm-nothreads")
, which should be enough as target_family = "wasm"
implies feature = "experimental-wasm"
(we have a compiler error set in place if you use wasm without that feature).
I think that's very smart actually, and should help make this fix land earlier. We could add a fix for EDIT: To be fair, we can't effectively use
I think that could be nice, as long as we ensure it doesn't become less readable rather than the opposite. Something like One alternative is to not apply De Morgan's laws here and just write EDIT: To be honest, I think I'm in favor of keeping the A better readability boost would be to use So, I think that should be it for now. |
7742933
to
26db942
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EDIT: To be honest, I think I'm in favor of keeping the
cfg
s as they are for now. I feel like the complexity of creating the build-time function would currently outweigh the readability problems regarding the currentcfg
assertions, since they are restricted to a single, small file. We could reconsider this once we adapt the feature toexperimental-threads
, as I believe this set ofcfg
assertions would then be used more frequently.
I don't agree here 😉
You currently have 12 (!) occurrences of the #[cfg]
, with varying flavors of:
#[cfg(all(target_family = "wasm", feature = "experimental-wasm-nothreads"))]
#[cfg(any(
not(target_family = "wasm"),
not(feature = "experimental-wasm-nothreads")
))]
#[cfg(all(
debug_assertions,
any(
not(target_family = "wasm"),
not(feature = "experimental-wasm-nothreads")
)
))]
#[cfg(any(
not(target_family = "wasm"),
not(feature = "experimental-wasm-nothreads")
))]
Emitting a custom cfg
is not complex, it's two lines in build.rs (I hope I got syntax right):
println!(r#"cargo:rustc-check-cfg=cfg(wasm_nothreads, values(none()))"#);
println!(r#"cargo:rustc-cfg=wasm_nothreads"#);
And this will simplify all of the above to single-line #[cfg]
statements, and it makes the special ones also easier to read:
#[cfg(all(debug_assertions, wasm_nothreads))]
But I added some other ideas on how to simplify code by setting the right abstraction boundaries, and maybe they could be combined with your approach of Option<()>
. It really seems that could cut down on many #[cfg]
s.
#[cfg(all(target_family = "wasm", feature = "experimental-wasm-nothreads"))] | ||
{ | ||
if !storage.initialized.get() { | ||
panic!("deinitialize without prior initialize"); | ||
} | ||
|
||
storage.main_thread_id.set(None); | ||
storage.initialized.set(false); | ||
} | ||
|
||
#[cfg(any( | ||
not(target_family = "wasm"), | ||
not(feature = "experimental-wasm-nothreads") | ||
))] | ||
{ | ||
storage | ||
.main_thread_id | ||
.get() | ||
.expect("deinitialize without prior initialize"); | ||
|
||
storage.main_thread_id.set(None); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be simplified to
assert!(self.is_initialized(), "deinitialize without prior initialize");
self.set_initialized(false);
for both cases. set_initialized()
would be a function that either sets the cell to false
or to None
.
If you use Option<()>
, the implementation will not even need to differentiate cases.
// 'std::thread::current()' fails when linking to a Godot build without threads. When this feature is enabled, | ||
// we assume it is impossible to have multi-threading, so checking if we are in the main thread is not needed. | ||
// Therefore, we assign a bogus value for the thread ID instead, so we can still check for prior initialization | ||
// through this field later. | ||
#[cfg(all(target_family = "wasm", feature = "experimental-wasm-nothreads"))] | ||
{ | ||
storage.initialized.set(true); | ||
} | ||
|
||
#[cfg(any( | ||
not(target_family = "wasm"), | ||
not(feature = "experimental-wasm-nothreads") | ||
))] | ||
{ | ||
storage | ||
.main_thread_id | ||
.set(Some(std::thread::current().id())); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could also become
self.set_initialized(true)
which then has a diverging implementation based on the #[cfg]
, just like is_initialized()
.
15e2ba4
to
66d2c61
Compare
Okay, I've applied your suggestions. Let me know what you think! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks much nicer, thanks a lot! I also appreciate the detailed comments and docs about safety 👍
godot-bindings/src/lib.rs
Outdated
if std::env::var("CARGO_CFG_TARGET_FAMILY") | ||
.is_ok_and(|families| families.split(',').any(|family| family == "wasm")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume you cannot check directly against wasm
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, for Emscripten it comes as "unix,wasm"
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, maybe add that as a comment.
Also, isn't that env var always set? If yes, you should .expect()
it rather than .is_ok_and()
.
/// # Panics | ||
/// If attempting to deinitialize before initializing, or vice-versa. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 2nd part is not checked though.
I think the precondition can be verified outside the #[cfg]
, e.g. like this:
if initialized == self.is_initialized() {
if initialized {
panic!("already initialized");
} else {
panic!("deinitialize without prior initialize");
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, I think this means it wasn't checked before either. So we'll get a free bug fix :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! Seems to be working fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Yes, it's always nice to have these drive-by bugfixes 🙂
#[cfg(not(wasm_nothreads))] | ||
{ | ||
if initialized { | ||
self.main_thread_id.set(Some(std::thread::current().id())); | ||
} else { | ||
self.main_thread_id | ||
.get() | ||
.expect("deinitialize without prior initialize"); | ||
|
||
self.main_thread_id.set(None); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the above changes, this can then be:
#[cfg(not(wasm_nothreads))] | |
{ | |
if initialized { | |
self.main_thread_id.set(Some(std::thread::current().id())); | |
} else { | |
self.main_thread_id | |
.get() | |
.expect("deinitialize without prior initialize"); | |
self.main_thread_id.set(None); | |
} | |
} | |
} | |
#[cfg(not(wasm_nothreads))] | |
{ | |
let thread_id = initialized.then(|| std::thread::current().id()); | |
self.main_thread_id.set(thread_id); | |
} | |
} |
Same for the other block.
Thank you! 🙂 |
Adds support for linking with Godot 4.3 web export built with
threads=no
(see godotengine/godot#85939). Requires NOT specifying the-pthread
emscripten flag.I'm not sure if it's possible to build with
threads=no
outside of the web export template, so for now I've restricted the fix to wasm targets. We could potentially remove that restriction if we find that Godot can be built that way for non-wasm targets as well (for maximum compatibility).This fix consists of adding a flag,
experimental-wasm-nothreads
(bikesheddable) which removes calls tostd::thread::current()
, as this appears to panic without emscripten pthread support (see #438 (comment)).Right now, I've only applied the fix to single-threaded gdext code, that is, without the
experimental-threads
feature toggled on. I've tested this with a very simple project and it seems to work, in principle (haven't tested more complex projects).I have yet to add support for
experimental-threads
(which is needed since the extension's code might depend on that feature even when exported to a single-threaded environment, due toSync
/Send
trait impls). Therefore, this PR is marked as draft until I add it. (EDIT: Decided to postpone this to a future PR, especially becauseexperimental-threads
isn't really usable on Wasm at the moment, at least to export to release.)