Skip to content
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

Merged
merged 1 commit into from
Jul 14, 2024

Conversation

PgBiel
Copy link
Contributor

@PgBiel PgBiel commented Jul 10, 2024

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 to std::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 to Sync/Send trait impls). Therefore, this PR is marked as draft until I add it. (EDIT: Decided to postpone this to a future PR, especially because experimental-threads isn't really usable on Wasm at the moment, at least to export to release.)

@GodotRust
Copy link

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<()>>,
Copy link
Contributor

@Ughuuu Ughuuu Jul 10, 2024

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?

Copy link
Member

@Bromeon Bromeon Jul 10, 2024

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.

Copy link
Contributor Author

@PgBiel PgBiel Jul 10, 2024

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.

Copy link
Member

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 🙂

Copy link
Contributor Author

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.

@Bromeon Bromeon added feature Adds functionality to the library c: ffi Low-level components and interaction with GDExtension API labels Jul 10, 2024
Copy link
Member

@Bromeon Bromeon left a 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:

gdext/godot/src/lib.rs

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<()>>,
Copy link
Member

@Bromeon Bromeon Jul 10, 2024

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
Comment on lines 20 to 24
experimental-wasm = []
experimental-wasm-nothreads = [
"experimental-wasm",
"godot-core/experimental-wasm-nothreads",
]
Copy link
Member

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 🤔

Copy link
Contributor Author

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).

@Bromeon Bromeon added c: wasm WebAssembly export target and removed c: ffi Low-level components and interaction with GDExtension API labels Jul 10, 2024
@PgBiel
Copy link
Contributor Author

PgBiel commented Jul 10, 2024

You should probably add another compile error if both features are simultaneously specified, mentioning this combination is not yet implemented.

I think that's very smart actually, and should help make this fix land earlier. We could add a fix for experimental-threads in a later PR, especially considering I don't really have, at the moment, a complex enough project with which to extensively test the interactions between experimental-threads and this flag.

EDIT: To be fair, we can't effectively use experimental-threads in a Wasm project, at least when compiling to release, since compiling Wasm to release currently requires lazy-function-tables (lest compilation consumes tens of gigabytes of memory), which is not yet thread-safe, so this is less important. Of course, you can always ship a debug binary, though that's not very ideal.

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?

I think that could be nice, as long as we ensure it doesn't become less readable rather than the opposite. Something like #[cfg(wasm-nothreads)] could work.

One alternative is to not apply De Morgan's laws here and just write #[cfg(not(all(target_family = "wasm", feature = "experimental-wasm-nothreads")))]. Though sometimes that's a bit less readable at a glance compared to the status quo, at least in terms of quickly visualizing the effective conditions that trigger that cfg.

EDIT: To be honest, I think I'm in favor of keeping the cfgs as they are for now. I feel like the complexity of creating the build-time function would currently outweigh the readability problems regarding the current cfg assertions, since they are restricted to a single, small file. We could reconsider this once we adapt the feature to experimental-threads, as I believe this set of cfg assertions would then be used more frequently.

A better readability boost would be to use cfg_if::cfg_if!, but that does imply an additional dependency (unless we find a way to inline it).

So, I think that should be it for now.

@PgBiel PgBiel force-pushed the wasm-nothreads branch 3 times, most recently from 7742933 to 26db942 Compare July 11, 2024 02:23
@PgBiel PgBiel marked this pull request as ready for review July 11, 2024 03:47
Copy link
Member

@Bromeon Bromeon left a 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 cfgs as they are for now. I feel like the complexity of creating the build-time function would currently outweigh the readability problems regarding the current cfg assertions, since they are restricted to a single, small file. We could reconsider this once we adapt the feature to experimental-threads, as I believe this set of cfg 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.

Comment on lines 109 to 117
#[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);
}
Copy link
Member

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.

Comment on lines 73 to 81
// '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()));
}
Copy link
Member

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().

@PgBiel PgBiel force-pushed the wasm-nothreads branch 5 times, most recently from 15e2ba4 to 66d2c61 Compare July 11, 2024 21:29
@PgBiel
Copy link
Contributor Author

PgBiel commented Jul 11, 2024

Okay, I've applied your suggestions. Let me know what you think!

Copy link
Member

@Bromeon Bromeon left a 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 👍

Comment on lines 187 to 188
if std::env::var("CARGO_CFG_TARGET_FAMILY")
.is_ok_and(|families| families.split(',').any(|family| family == "wasm"))
Copy link
Member

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?

Copy link
Contributor Author

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".

Copy link
Member

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().

Comment on lines +59 to +71
/// # Panics
/// If attempting to deinitialize before initializing, or vice-versa.
Copy link
Member

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");
    }
}

Copy link
Contributor Author

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 :)

Copy link
Contributor Author

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.

Copy link
Member

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 🙂

Comment on lines 74 to 92
#[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);
}
}
}
Copy link
Member

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:

Suggested change
#[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.

@Bromeon Bromeon added this pull request to the merge queue Jul 14, 2024
Merged via the queue into godot-rust:master with commit d35869f Jul 14, 2024
15 checks passed
@Bromeon
Copy link
Member

Bromeon commented Jul 14, 2024

Thank you! 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: wasm WebAssembly export target feature Adds functionality to the library
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants