godot-rust / gdext

Rust bindings for Godot 4
https://mastodon.gamedev.place/@GodotRust
Mozilla Public License 2.0
3.04k stars 189 forks source link

WebAssembly support #438

Open Bromeon opened 1 year ago

Bromeon commented 1 year ago

This issue serves as a knowledge base for approching WASM builds. Ideally it should have more consolidated pieces of information. Please edit your responses to update anything outdated.

For free-form discussion, check out the Discord thread.

PgBiel commented 1 year ago

Hello, thanks for opening this issue! Hopefully other contributors can see this and feel motivated to help :smile:

(EDIT: results in this comment are outdated; see https://github.com/godot-rust/gdext/issues/438#issuecomment-1822177121)

I'd like to share here some of the progress we have so far. In particular, I tried to summarize our current progress in a gist, after attempting to compile a Gdext project to WASM from scratch: https://gist.github.com/PgBiel/ffa695a479ef4466cb24755db983950b

The most important section in the link above is the gdext-wasm-min-report.md file, at the top, where I summarize what I found, also based on the most recent discussions from the Discord thread:

  1. Setup: Tools and steps needed to try to compile Gdext to WASM (at least, to our best knowledge so far), through the wasm32-unknown-emscripten Rust target. Basically, you'll need a nightly Rust toolchain in order to recompile the Rust standard library (currently needed due to build flags related to threading in WASM), along with custom emscripten flags (more specifically --no-entry -sSIDE_MODULE=2 -sUSE_PTHREADS=1 - the required ones that we know of so far). You'll also need at least -g -sASSERTIONS=2 -sSTACK_OVERFLOW_CHECK=2 -sDEMANGLE_SUPPORT=1 (maybe a few others) to enable debug assertions and symbols on the browser (mapping errors/compiled WASM to source code requires this Chrome extension, though, with those debug flags enabled).

  2. Results (and updates): Errors I found which are currently stopping the gdext web export from working (those errors show up when trying to run, on the browser (Firefox/Chromium), the project exported to the web through the Godot editor). The two main ones are load count too large, which seems to be related to some inefficiency somewhere in gdext codegen (as that error disappears when compiling with --release, or with default-features = false (which apparently disables a good chunk of codegen), and thus is not an unavoidable error), and memory access out of bounds (which translates to a Stack Overflow/invalid pointer address error when enabling debug assertions) - this last one has been the larger blocker so far (although different errors have been obtained by experimenting with different sets of emscripten flags).

I've been playing with this last error but haven't found much so far. Curiously, with the right settings (larger stack size etc.), I managed to receive an index out of bounds message on Firefox instead, which apparently occurred in some internal emscripten function cull_zombies; I couldn't decompile the WASM in firefox (/map to source code), but I suspect it was related to this line, which suggests there's something related to threading going on:

https://github.com/emscripten-core/emscripten/blob/ef3e4e3b044de98e1811546e0bc605c65d3412f4/system/lib/pthread/em_task_queue.c#L80

In the Discord thread, there were other theories such as problems with function pointer tables being too large, but also a theory that function pointers in WASM aren't "legit" function pointers due to JavaScript interop in WASM, so using them may require adaptation somehow.

Overall, we need some more investigation on this matter before determining what we can do in gdext to solve this. (It's possible that there are upstream problems as well, from Godot and/or from emscripten, but we just don't know yet if that's the case.) Let's hope other interested contributors - and/or WASM experts :eyes: - drop by and give their opinions as well :wink:

Esption commented 1 year ago

As of writing, this is what I know needs to be done. There's still some gdext work to be done, it seems like, but if anyone gets involved this should hopefully serve as a recap. If anyone wants to run things by me I'm happy to help at least get something going. I'm usually easy to reach on discord. But please read this first!

Compile gdextension web template

You'll need to manually compile the template. Godot doesn't ship with a web template that has gdextension support. It's important to note this template will be SPECIFIC to the version of emscripten that it was compiled with. This probably means you'll need to install emsdk along with a few other tools. Follow the official docs for a guide on what to do, and where to place the template zip.

You do not need to recompile the entire editor. ONLY the template. If compiling from git, don't forget to make use of tags prior to compiling anything so it matches the editor version you are using.

Setup Rust config

You need to be able to use unstable features. So, a nightly build is probably the best choice.

rustup toolchain install nightly
rustup default nightly
rustup target add wasm32-unknown-emscripten

At the root directory of your project, you'll need to make a .cargo/config.toml file.

The big thing is needing to compile your extension with SHARED_MEMORY. This requires a few flags and rebuilding std (this is because the std included from rustup was not compiled with it enabled).

[unstable]
build-std = ["std"]

[target.wasm32-unknown-emscripten]
rustflags = [
    "-C", "link-args=-sSIDE_MODULE=2 -sEXPORT_ALL=1",
    "-Zlink-native-libraries=no",
    "-C", "link-args=-pthread",
    "-C", "target-feature=+bulk-memory",
    "-C", "target-feature=+atomics",
    "-C", "target-feature=+mutable-globals",
    "-C", "link-args=-sSHARED_MEMORY=1",
]

You'll then have to either compile with --target wasm32-unknown-emscripten or add

[build]
target = ["wasm32-unknown-emscripten"]

to the .cargo/config.toml file

Don't forget to add a web section to your extension's .gdextension file. Something like

web.debug = "res://path/to/debug/project.wasm"
web.release = "res://path/to/release/project.wasm"

Setup a web server

You need to have HTTPS and enable some cross-origin headers. Either use certbot or a self-signed cert.
For nginx, enabling the necessary headers is quite simple by just adding

add_header 'Cross-Origin-Opener-Policy' 'same-origin';
add_header 'Cross-Origin-Embedder-Policy' 'require-corp';

inside the location {} section of your site. Look for something similar for other web servers.

Debugging

Use chrome and install this extension. It'll basically allow the devtools (F12) to act like a worse LLDB. Breakpoints, memory inspector, etc.

Random notes

Esption commented 11 months ago

Newer info:

Esption commented 10 months ago

Thanks to @zecozephyr for figuring out how to work around some of the rust+emscripten limitations, we currently have gdext on wasm working with the dodge-the-creeps example.

We're hoping to get more people to test with their projects to jump in and confirm/deny this. This is coming with a big DISCLAIMER that this patch is probably buggy and not production ready. On top of that, we literally found two bugs in emscripten in the process of this, so... yeah. Bugs!

KNOWN CAVEATS

Godot 4.1.3+ or 4.2-dev is necessary. The only browser supported appears to be Chrome (Firefox and Safari don't work with GDExtension yet, discussion here).

Steps to test wasm patch

  1. Ensure you have a nightly build of Rust with the rust-src component, and install the emscripten target. With rustup, that looks something like
    rustup toolchain install nightly
    rustup component add rust-src --toolchain nightly
    rustup target add wasm32-unknown-emscripten --toolchain nightly
  2. Install Emscripten. Prefer to use Emscripten 3.1.39, as that's the maximum version Godot itself builds with (even though gdext itself should work with later versions). Using emsdk that would be something like
    git clone https://github.com/emscripten-core/emsdk.git
    cd emsdk
    ./emsdk install 3.1.39
    ./emsdk activate 3.1.39
    source ./emsdk.sh     (or ./emsdk.bat on windows)
    • Note You probably also want to follow the given prompts to add emcc to your $PATH otherwise you'll need to run the source ./emsdk.sh / ./emsdk.bat for every new shell.
  3. Add this to your .cargo/config.toml
    [target.wasm32-unknown-emscripten]
    rustflags = [
    "-C", "link-args=-sSIDE_MODULE=2",
    "-C", "link-args=-sUSE_PTHREADS=1",
    "-C", "target-feature=+atomics,+bulk-memory,+mutable-globals",
    "-Zlink-native-libraries=no"
    ]
  4. Edit your Cargo.toml to add the "experimental-wasm" feature.
    godot = { git = "https://github.com/godot-rust/gdext", branch = "master", features = ["experimental-wasm", "lazy-function-tables"] }

    NOTE: May need to enable the "lazy-function-tables" feature for a successful runtime and a SIGNIFICANTLY shorter compile time.

  5. Add the web target to your project's .gdextension file. Will be similar to the others in that file, roughly something like this
    web.debug.wasm32 = "res://../target/wasm32-unknown-emscripten/debug/EXTENSIONNAME.wasm"
    web.release.wasm32 = "res://../target/wasm32-unknown-emscripten/release/EXTENSIONNAME.wasm"
  6. Verify that you're using emcc version 3.1.39 with emcc --version and then compile your code. Debug builds seem to take a while longer to load in the browser, but release builds take a very long time to compile, as well as a lot of RAM. Compile with:
    cargo +nightly build -Zbuild-std --target wasm32-unknown-emscripten
  7. In godot, add a web target and ensure that the "Extensions Support" checkbox is ticked ON. Set it to export wherever you would like
  8. From here you have two choices to run your game, as you need to spawn a server to serve the HTML file (just double-clicking probably won't work).

    • The first and easiest choice is through the Godot editor itself: from the main scene view godot, press "Remote Debug > Run in Browser"

      image
    • NOTE 1: The above will always run a debug build, so make sure to compile with debug (or change the path to the debug build in the NAME.gdextension file) if you'd like to use the "Run in Browser" button.

    • NOTE 2: If your default browser isn't Chrome, you'll need to copy the URL and paste it into Chrome (e.g. manually head to localhost:8060 and click the HTML file), as your game will not work on Firefox or Safari (as of the time of writing).

    • Alternatively, run the web export process fully and point a web-server at the exported files. If this is what you'd like to do, you're going to need to have a web-server with some specific cross-origin headers to get this part to work. A quick way to do this is through simple-http-server:

    cargo install simple-http-server
    simple-http-server --coop --coep --nocache ./path/to/export/

    And then open up a Chrome/Chromium browser at localhost:8000/EXPORTNAME.html. It may take a while to load, especially if you built with debug instead of release.

    • NOTE: Both solutions above are only suitable for testing. If you want to share your game to others properly, then you'll want to setup an actual HTTPS web-server.

Discussion

Either post here or post in the discord thread where most of this has been taking place.

zecozephyr commented 10 months ago

Preliminary support has now been merged into master by https://github.com/godot-rust/gdext/pull/493.

Instructions remain largely unchanged except that one now needs to enable the feature experimental-wasm to explicitly opt-in to wasm builds.

DeprecatedLuke commented 5 months ago

Since I know that there's quite a few people looking to run gdext on wasm, but are afraid to use it due to instability here's a few words:

If you haven't made anything yet within godot and are able to switch engines the bevy game engine is pretty good if you want to use rust and have web-assembly/full multiplatform support. Bevy is in much earlier stages than godot, but so is gdext and it's understandable that each of them have their own challenges. Bevy having no editor could be preferrable as I've seen people ditch the editor for pure-code scene creation in some cases.

As someone who had experience now with both - gdext and bevy are good enough to make work with few days of research and hands-on fixes, as a bonus - bevy has webgpu support.

Bromeon commented 5 months ago

@DeprecatedLuke please don't hijack the issue tracker only to advertise other projects. People are aware that there are many choices in the Rust ecosystem.

Godot itself works absolutely fine with web (there are tons of game jam entries for it, and I have used it myself). The main issue specific to GDExtension is that it's not yet fully supported in non-Chromium based browsers; something that will likely improve.

If you want to discuss the issue further, please bring it up on Discord, here is not the right place.

Bromeon commented 3 months ago

I wonder if https://github.com/godotengine/godot-cpp/pull/1489 has relevance for us (panics might use the same mechanism as exceptions) 🤔

PgBiel commented 3 months ago

Perhaps, but this will depend on Rust and LLVM support for wasm exceptions, which seems to be fairly early days still. However, as seen here https://github.com/rust-lang/rust/issues/118168 , since last year both the language and stdlib got support for building with panic=unwind (it's still opt-in and unstable, so an explicit -Z flag is needed), so maybe it's worth experimenting with it now? Seems like LLVM might still have some problems with it, but I don't know enough about the current situation to tell without testing.

PgBiel commented 3 months ago

A new problem seems to be appearing when exporting gdext to Wasm on Godot 4.3 (commit 26d1577f3985363faab48a65e9a0d9eed0e26d86), even with Emscripten 3.1.62, which supposedly brings fixes for dynamic linking (something we depend on). Here are my observations so far:

  1. The error is of the form Aborted(Assertion failed: undefined symbol 'invoke_v'. perhaps a side module was not linked in? if this global was expected to arrive from a system library, try to build the MAIN_MODULE with EMCC_FORCE_STDLIBS=1 in the environment), where invoke_v varies depending on which function call triggered it (sometimes it's invoke_vii, sometimes invoke_iii). It occurs inside gdext_rust_init generated by the #[gdextension] macro (see below).
  2. It manifests itself regardless of building Godot web export templates with threads=yes or threads=no (and disabling -pthread). In other words, this is unrelated to thread support.
  3. Enabling the linker flag -g and using the WASM Debugger extension, I traced the error back to this specific call: https://github.com/godot-rust/gdext/blob/79edae358e224225248f0f6f7ca3727130d22fd5/godot-macros/src/gdextension.rs#L90
  4. It appears that the call above generates, in wasm, call $invoke_v to call the gdext_registration function, but invoke_v is not available, so that fails.
  5. After some experimenting, any call to a Rust function at this point in the code would generate a call to invoke_(returned wasm type)(parameters' wasm types) and crash.
  6. I could get the gdext_registration script to run by 1. inlining the gdext_registration function into gdext_rust_init, 2. replacing the CString with a byte literal let script = b"JS code here \0" and later replacing the call script.as_ptr() with a simple pointer cast: script as *const _ as *const std::ffi::c_char.
  7. This is nice, because the JS script doesn't have to go through Wasm, thus not triggering invoke_signaturehere inside it. Additionally, this script has some logic to fix missing symbols before calling gdext's auto-generated registration functions for godot classes. In particular, it tries to fix missing dynCall_v (for example) functions (we were getting errors related to missing dynCall_signaturehere functions before). So, it seemed logical that we could just improve upon this logic to also fix missing invoke_v (and similar) symbols to make calls to Rust functions work, so I tried to replace line 62 (below) with if (sym.startsWith("dynCall_") || sym.startsWith("invoke_")) {: https://github.com/godot-rust/gdext/blob/79edae358e224225248f0f6f7ca3727130d22fd5/godot-macros/src/gdextension.rs#L62
  8. However, unfortunately the problem would still manifest itself as soon as the script called the first registration function.
  9. I added some console.log statements to debug this, in particular to print every single sym checked in this for loop (not only dynCall symbols), and it seems that, while the dynCall symbols are correctly checked and patched, the invoke symbols are simply not checked by the for loop at all. They don't appear to be present in dso_exports.

I haven't been able to proceed from here. I'm not really sure of the semantics behind dso_exports, or in particular what determines the symbols that go in it, so I can't tell, at this moment, if this is Godot's fault, Emscripten's fault, or just some specific flag we're missing. (Worth mentioning that -sEMULATE_FUNCTION_POINTERS was tested but did not fix it.) I'm not sure how we could make the invoke symbols appear in dso_exports. But I'll send updates once I get more information. Paging @zecozephyr for further ideas.

Details, Artifacts, Reproducers

Versions of things

Emscripten 3.1.62 gdext 79edae358e224225248f0f6f7ca3727130d22fd5 rustc 1.81.0-nightly (6be96e386 2024-07-09) Godot 4.3-master 26d1577f3985363faab48a65e9a0d9eed0e26d86

Full logs from my latest attempt: run.log

My modified lib.rs code (I inlined #[gdextension] to test this):

Note that emscripten_preregistration isn't used anymore since I manually inlined its body. (#[inline(always)] helped but wasn't very reliable.) Also note that pkgName is set to hello_gdext in the script - this should be changed to the name of your library crate.

lib.rs code

```rs mod player; use godot::prelude::*; struct MyExtension; // #[gdextension] // unsafe impl ExtensionLibrary for MyExtension {} unsafe impl ExtensionLibrary for MyExtension {} // #[cfg(target_os = "emscripten")] #[inline(always)] fn emscripten_preregistration() { let script = b"var pkgName = \'hello_gdext\';\n console.log(\"[DEBUG] Reached point A.\");\n var libName = pkgName.replaceAll(\'-\', \'_\') + \'.wasm\';\n console.log(\"[DEBUG] Reached point B.\");\n var dso = LDSO.loadedLibsByName[libName];\n // This property was renamed as of emscripten 3.1.34\n var dso_exports = \"module\" in dso ? dso[\"module\"] : dso[\"exports\"];\n var registrants = [];\n console.log(\"[DEBUG] Reached point C.\");\n for (sym in dso_exports) {\n console.log(`[DEBUG] Let\'s check this symbol \'${sym}\'...`);\n if (sym.startsWith(\"dynCall_\") || sym.startsWith(\"invoke_\")) {\n console.log(\"[DEBUG] It is special...\");\n if (!(sym in Module)) {\n console.log(`Patching Module with ${sym}`);\n Module[sym] = dso_exports[sym];\n }\n } else if (sym.startsWith(\"rust_gdext_registrant_\")) {\n registrants.push(sym);\n console.log(\"[DEBUG] Pushed one.\");\n }\n }\n for (sym of registrants) {\n console.log(`Running registrant ${sym}`);\n dso_exports[sym]();\n }\n console.log(\"Added\", registrants.length, \"plugins to registry!\");\n \0"; extern "C" { fn emscripten_run_script(script: *const std::ffi::c_char); } unsafe { emscripten_run_script(script as *const _ as *const std::ffi::c_char); } } #[no_mangle] unsafe extern "C" fn gdext_rust_init( interface_or_get_proc_address: ::godot::sys::InitCompat, library: ::godot::sys::GDExtensionClassLibraryPtr, init: *mut ::godot::sys::GDExtensionInitialization, ) -> ::godot::sys::GDExtensionBool { #[cfg(target_os = "emscripten")] { let script = b"var pkgName = \'hello_gdext\';\n console.log(\"[DEBUG] Reached point A.\");\n var libName = pkgName.replaceAll(\'-\', \'_\') + \'.wasm\';\n console.log(\"[DEBUG] Reached point B.\");\n var dso = LDSO.loadedLibsByName[libName];\n // This property was renamed as of emscripten 3.1.34\n var dso_exports = \"module\" in dso ? dso[\"module\"] : dso[\"exports\"];\n var registrants = [];\n console.log(\"[DEBUG] Reached point C.\");\n for (sym in dso_exports) {\n console.log(`[DEBUG] Let\'s check this symbol \'${sym}\'...`);\n if (sym.startsWith(\"dynCall_\") || sym.startsWith(\"invoke_\")) {\n console.log(\"[DEBUG] It is special...\");\n if (!(sym in Module)) {\n console.log(`Patching Module with ${sym}`);\n Module[sym] = dso_exports[sym];\n }\n } else if (sym.startsWith(\"rust_gdext_registrant_\")) {\n registrants.push(sym);\n console.log(\"[DEBUG] Pushed one.\");\n }\n }\n for (sym of registrants) {\n console.log(`Running registrant ${sym}`);\n dso_exports[sym]();\n }\n console.log(\"Added\", registrants.length, \"plugins to registry!\");\n \0"; extern "C" { fn emscripten_run_script(script: *const std::ffi::c_char); } unsafe { emscripten_run_script(script as *const _ as *const std::ffi::c_char); } } // emscripten_preregistration(); ::godot::init::__gdext_load_library::(interface_or_get_proc_address, library, init) } fn __static_type_check() { let _unused: ::godot::sys::GDExtensionInitializationFunction = Some(gdext_rust_init); } #[no_mangle] #[doc(hidden)] #[cfg(target_os = "linux")] pub unsafe extern "C" fn __cxa_thread_atexit_impl( func: *mut ::std::ffi::c_void, obj: *mut ::std::ffi::c_void, dso_symbol: *mut ::std::ffi::c_void, ) { ::godot::sys::linux_reload_workaround::thread_atexit(func, obj, dso_symbol); } ```

Here's the script above in a more readable form:

var pkgName = {env!("CARGO_PKG_NAME")};
console.log("[DEBUG] Reached point A.");
var libName = pkgName.replaceAll('-', '_') + '.wasm';
console.log("[DEBUG] Reached point B.");
var dso = LDSO.loadedLibsByName[libName];
// This property was renamed as of emscripten 3.1.34
var dso_exports = "module" in dso ? dso["module"] : dso["exports"];
var registrants = [];
console.log("[DEBUG] Reached point C.");
for (sym in dso_exports) {
    console.log(`[DEBUG] Let's check this symbol '${sym}'...`);
    if (sym.startsWith("dynCall_") || sym.startsWith("invoke_")) {
        console.log("[DEBUG] It is special...");
        if (!(sym in Module)) {
            console.log(`Patching Module with ${sym}`);
            Module[sym] = dso_exports[sym];
        }
    } else if (sym.startsWith("rust_gdext_registrant_")) {
        registrants.push(sym);
        console.log("[DEBUG] Pushed one.");
    }
}
for (sym of registrants) {
    console.log(`Running registrant ${sym}`);
    dso_exports[sym]();
}
console.log("Added",  registrants.length, "plugins to registry!");

.cargo/config.toml (added some flags to enable debugging):

[target.wasm32-unknown-emscripten]
rustflags = [
  "-C",
  "link-args=-sSIDE_MODULE=2",
  # "-C",
  # "link-args=-pthread",                                    # was -sUSE_PTHREADS=1 in earlier emscripten versions
  "-C",
  "target-feature=+atomics,+bulk-memory,+mutable-globals",
  "-Clink-args=-sEXPORT_ALL=1",
  # Trying out stuff
  "-Clink-arg=-O0",
  "-Clink-arg=-g",
  # "-Clink-arg=-sASSERTIONS=2",
  "-Clink-arg=-sDEMANGLE_SUPPORT=1",
  # "-Clink-arg=-sEMULATE_FUNCTION_POINTER_CASTS",
  # ---
  "-Zlink-native-libraries=no",
]

Test project (adapted from https://github.com/PgBiel/hello-gdext-wasm, but updated for compatibility with gdext 0.1): wasm-test-project.zip

Compiled web export templates (with and without threads):

godot.web.template_debug.wasm32.dlink.zip godot.web.template_debug.wasm32.nothreads.dlink.zip

PgBiel commented 3 months ago

Update: Fix for Godot 4.3-beta2+ found!

TL;DR: Change your .cargo/config.toml to

[target.wasm32-unknown-emscripten]
rustflags = [
  "-C",
  "link-args=-sSIDE_MODULE=2",
  "-C",
  "link-args=-pthread",                                    # was -sUSE_PTHREADS=1 in earlier emscripten versions
  "-C",
  "target-feature=+atomics,+bulk-memory,+mutable-globals",
  "-Cllvm-args=-enable-emscripten-cxx-exceptions=0",
  "-Zlink-native-libraries=no",
]

That is, add "-Cllvm-args=-enable-emscripten-cxx-exceptions=0" to it.

This will work with the official Godot web export templates for 4.3-beta2 - recompiling Godot is not needed (at least, for this sample project I'm smoke-testing with).

If the flag above doesn't fix it (it should), try adding the rustflags below as well (please ping me if you happen to need to use those flags):

[target.wasm32-unknown-emscripten]
rustflags = [
  # ... other flags ...
  "-Clink-arg=-fwasm-exceptions",
  "-C",
  "link-args=-sSUPPORT_LONGJMP=wasm",
  "-Cllvm-args=-enable-emscripten-cxx-exceptions=0",
  "-Cllvm-args=-wasm-enable-sjlj",
  "-C",
  "link-args=-sDISABLE_EXCEPTION_CATCHING=1",
]

Fun fact

As a result, gdext is also running on Firefox (didn't work there before)! :tada:

What happened?

Basically, since https://github.com/godotengine/godot/pull/93143 (which was merged in time for Godot 4.3-beta2), Godot's web export templates are compiled with -sSUPPORT_LONGJMP=wasm, which changes how exceptions work in wasm. GDExtension users need to use that flag with emscripten as well for full compatibility with Godot's web build, otherwise Bad Things can happen. I noticed this requirement from the fact that godot-cpp (the library for GDExtension in C++) had to add this flag too: https://github.com/godotengine/godot-cpp/pull/1489

I tried to add that linker flag directly, but that didn't fix it. After some googling, I got here https://github.com/rust-lang/rust/issues/112195#issuecomment-1573060218 . Adding all of these flags as well fixed it!

Eventually, after some testing, the only flag necessary turned out to be "-Cllvm-args=-enable-emscripten-cxx-exceptions=0".

How did I get here?

After doing some research regarding the invoke_... functions mentioned in the comment above, as well as looking and searching through Emscripten's codebase, I eventually found this:

https://github.com/emscripten-core/emscripten/blob/34c1aa36052b1882058f22aa1916437ba0872690/tools/emscripten.py#L995-L998

Which suggested that invoke functions are only exported when -sSUPPORT_LONGJMP=emscripten and/or -sDISABLE_EXCEPTION_CATCHING=0 (two settings which seem to be linked) are set.

I then remembered seeing Bromeon's comment above https://github.com/godot-rust/gdext/issues/438#issuecomment-2167555284 , about Godot switching to -sSUPPORT_LONGJMP=wasm, which made me have an "aha!" moment: now that Godot is using this flag, it might not be exporting the invoke_... functions anymore. Further, this might imply that setting -sSUPPORT_LONGJMP=wasm on our side would fix it by having emscripten not generate calls to invoke_... at all.

With the testing above, this turned out to be the case! (With a few adjustments...)

PgBiel commented 3 months ago

Regarding single-threaded wasm builds

Godot 4.3's web export template can be compiled with threads=no to disable multi-threading (since https://github.com/godotengine/godot/pull/85939). This can be useful to improve web export compatibility in some contexts. To support this, one has to remove the -pthread flag while compiling the GDExtension (that is, remove the lines containing -Clink-args=-pthread from .cargo/config.toml).

(Also, based on https://github.com/godotengine/godot-cpp/pull/1451 , we should eventually warn gdext users that you should have separate threaded - with web.debug/release.threads.wasm32 - and non-threaded - with web.debug/release.wasm32 - wasm builds.)

However, while the fix in the above comment works to remove the invoke_-related errors, it appears gdext itself doesn't support a threads=no build yet, as a panic occurs during init, specifically on this call to std::thread::current():

https://github.com/godot-rust/gdext/blob/79edae358e224225248f0f6f7ca3727130d22fd5/godot-ffi/src/binding/single_threaded.rs#L102

Full logs

```log [C/C++ DevTools Support (DWARF)] Loading debug symbols for wasm://wasm/hello_gdext.wasm-0b61c0d6... VM74:18 Running registrant rust_gdext_registrant___gensym_abc20a22af3d4ce596c2106f05dd70f8 VM74:18 Running registrant rust_gdext_registrant___gensym_5c516572276c4221889ba4ff9b4c2425 VM74:21 Added 2 plugins to registry! Hello Gdext.js:46812 Aborted(undefined) onPrintError @ Hello Gdext.js:46812 abort @ Hello Gdext.js:561 ___cxa_throw @ Hello Gdext.js:15564 $panic_unwind::imp::panic::h2f129ac708c2ef5d @ emcc.rs:110 $__rust_start_panic @ lib.rs:100 $rust_panic @ panicking.rs:857 $std::panicking::rust_panic_with_hook::h0a73500366cd9d48 @ panicking.rs:821 $std::panicking::begin_panic_handler::_$u7b$$u7b$closure$u7d$$u7d$::h074888851122b07b @ hello_gdext.wasm-0b61c0d6:0x15f7f9 $std::sys::backtrace::__rust_end_short_backtrace::hfd00b625b158fbd6 @ backtrace.rs:171 $rust_begin_unwind @ panicking.rs:661 $core::panicking::panic_fmt::h3181722ab72257cd @ panicking.rs:74 $core::panicking::panic_display::h90455b1a7f394b21 @ panicking.rs:264 $core::option::expect_failed::h15de83ddbb89ea02 @ option.rs:2023 $core::option::Option$LT$T$GT$::expect::h01f3b96be4d69a4b @ option.rs:926 $std::thread::current::h3db11b9453b88171 @ mod.rs:747 $godot_ffi::binding::single_threaded::BindingStorage::initialize::hca50a5aad300095a @ single_threaded.rs:54 $godot_ffi::binding::initialize_binding::h693d3b5b51932c57 @ mod.rs:229 $godot_ffi::initialize::hb56eb1a096004668 @ lib.rs:191 $godot_core::init::__gdext_load_library::_$u7b$$u7b$closure$u7d$$u7d$::h9d6d1c2377ac11a1 @ mod.rs:44 $core::ops::function::FnOnce::call_once::hfe3b5d72725fcec9 @ function.rs:250 $std::panicking::try::do_call::h670e33ec316b1f8a @ panicking.rs:553 $__rust_try @ hello_gdext.wasm-0b61c0d6:0x1a032 $std::panicking::try::h841d2c637343daa1 @ panicking.rs:517 $std::panic::catch_unwind::h19be830f5594ac2c @ panic.rs:350 $godot_core::private::handle_panic_with_print::h8f822c92e6964dc4 @ private.rs:281 $godot_core::private::handle_panic::hfedd59d33aaf8511 @ private.rs:218 $godot_core::init::__gdext_load_library::h22feb0dba4d03d9b @ mod.rs:63 $gdext_rust_init @ lib.rs:8 $func120802 @ 0c895ea6:0x2aff928 $func120821 @ 0c895ea6:0x2b028e5 $func120828 @ 0c895ea6:0x2b0334d $func108540 @ 0c895ea6:0x2806e32 $func108544 @ 0c895ea6:0x28077ff $func108547 @ 0c895ea6:0x2808783 $func108554 @ 0c895ea6:0x280a218 $func121195 @ 0c895ea6:0x2b26455 $func121210 @ 0c895ea6:0x2b27373 $func103419 @ 0c895ea6:0x262c14d $func1392 @ 0c895ea6:0x2de17a $_Z14godot_web_mainiPPc @ 0c895ea6:0x2d6c71 __Z14godot_web_mainiPPc @ Hello Gdext.js:2030 $__main_argc_argv @ 0071a6a6:0x8fa0d callMain @ Hello Gdext.js:46235 (anonymous) @ Hello Gdext.js:47139 (anonymous) @ Hello Gdext.js:47134 Promise.then (async) start @ Hello Gdext.js:47113 (anonymous) @ Hello Gdext.js:47172 Promise.then (async) startGame @ Hello Gdext.js:47171 (anonymous) @ Hello Gdext.html:181 (anonymous) @ Hello Gdext.html:195 Hello Gdext.html:139 RuntimeError: Aborted(undefined) at abort (Hello Gdext.js:580:11) at ___cxa_throw (Hello Gdext.js:15564:29) at panic_unwind::imp::panic::h2f129ac708c2ef5d (emcc.rs:110) at __rust_start_panic (lib.rs:100) at rust_panic (panicking.rs:857) at std::panicking::rust_panic_with_hook::h0a73500366cd9d48 (panicking.rs:821) at std::panicking::begin_panic_handler::_$u7b$$u7b$closure$u7d$$u7d$::h074888851122b07b (hello_gdext.wasm-0b61c0d6:0x15f7f9) at std::sys::backtrace::__rust_end_short_backtrace::hfd00b625b158fbd6 (backtrace.rs:171) at rust_begin_unwind (panicking.rs:661) at core::panicking::panic_fmt::h3181722ab72257cd (panicking.rs:74) displayFailureNotice @ Hello Gdext.html:139 Promise.then (async) (anonymous) @ Hello Gdext.html:191 (anonymous) @ Hello Gdext.html:195 [C/C++ DevTools Support (DWARF)] Loaded debug symbols for wasm://wasm/hello_gdext.wasm-0b61c0d6, found 1374 source file(s) ```

Seems like we could work around this somehow by avoiding calling this function, but perhaps there is some other flag missing which would make it work. Either way, non-threaded builds do not work at the moment because of this.

PgBiel commented 2 months ago

One issue found by @Ughuuu (posting for awareness, but also as a reminder so we can make a fix later): the current workaround for emscripten support assumes that the wasm binary is named YOUR_CRATE_HERE.wasm, but it might be renamed, resulting in cryptic errors related to attempting to access "undefined" (the lib wasn't found). We can improve this by first throwing a more helpful error if the library with the crate's name wasn't found ("please don't rename the binary" but a bit more formal). Ideally, we'd find some other way to detect our own lib name, noting that we have access to the Module object; maybe it has some insight. Some compile-time parameter/env var could also work, but wouldn't be optimal for extensions made to be distributed, since end users might still accidentally rename the binaries - but I guess a more helpful error would already be enough in this case, since a smoke test would fail and immediately indicate the reason.

Bromeon commented 1 month ago

Note also some changes in flags like https://github.com/godotengine/godot-cpp/pull/1566.

We currently elaborate in the book how to compile WASM, but I wonder if there's a better way than copy-pasting a .cargo/config.toml file, which will also become outdated if flags change?

PgBiel commented 1 month ago

In principle it seems inevitable that we'll be playing a game of "cat and mouse" here, having to manually keep up with any new flags introduced by Godot, though it'd be nice if:

  1. gdext could provide at least some of those flags by default;
    • I actually have no idea if this is possible in Rust, and a quick search didn't yield many useful results. But we should keep looking.
  2. we could have some CI job to check whether wasm export is still working with latest godot (if not, indicates a new flag could be necessary).
    • a smoke test would be enough for most cases (game doesn't crash on startup), but of course there could be more elaborate bugs which only show up later on (most would be unrelated to emscripten flags though).
Rune580 commented 1 month ago

Getting a bizarre issue that I can't find a solution for. I've tried every single flag combination in this thread, but I keep getting "resolved is not a function".

I've tested with both Firefox and Chromium on ArchLinux.

emcc: 3.1.39 rustc: 1.81.0 godot: v4.3.stable.arch_linux

It seems like something called __handle_stack_overflow is not getting resolved.

I've attached relevant screenshots from debugging with chromium ![image](https://github.com/user-attachments/assets/819c2fa5-789c-40f3-97f4-1fb4e8bd36ee) ![image](https://github.com/user-attachments/assets/cfd2cc68-44c8-40ba-8887-a2f12ed99509) ![image](https://github.com/user-attachments/assets/ef176173-a5e8-41bb-a146-80c56bf22966) ![image](https://github.com/user-attachments/assets/27639d9e-51ad-4693-b0c7-f9b340773614) ![image](https://github.com/user-attachments/assets/8b8501dd-4f96-4638-88db-38f884036b20) ![image](https://github.com/user-attachments/assets/4d2f8920-1e83-4f8b-8fbd-0d8946a15966) ![image](https://github.com/user-attachments/assets/e85922e6-d078-48fc-adc5-d0ed96547422) ![image](https://github.com/user-attachments/assets/1047bff4-e375-43f5-a028-0324003b3743)
Here's the relevant wasm code(?) ``` (func $gdext_rust_init (;465;) (export "gdext_rust_init") (param $var0 i32) (param $var1 i32) (param $var2 i32) (result i32) (local $var3 i32) (local $var4 i32) (local $var5 i32) (local $var6 i32) (local $var7 i32) (local $var8 i32) (local $var9 i32) (local $var10 i32) (local $var11 i32) (local $var12 i32) global.get $__stack_pointer local.set $var3 i32.const 16 local.set $var4 local.get $var3 local.get $var4 i32.sub local.set $var5 local.get $var5 local.tee $var11 global.get $global260 i32.gt_u local.get $var11 global.get $global261 i32.lt_u i32.or if local.get $var11 call $__handle_stack_overflow end local.get $var11 global.set $__stack_pointer local.get $var5 local.get $var0 i32.store offset=4 local.get $var5 local.get $var1 i32.store offset=8 local.get $var5 local.get $var2 i32.store offset=12 call $game_rust::emscripten_preregistration::h82d9c921dce99919 local.get $var0 local.get $var1 local.get $var2 call $godot_core::init::__gdext_load_library::h1e85aea9eacf5420 local.set $var6 i32.const 255 local.set $var7 local.get $var6 local.get $var7 i32.and local.set $var8 i32.const 16 local.set $var9 local.get $var5 local.get $var9 i32.add local.set $var10 local.get $var10 local.tee $var12 global.get $global260 i32.gt_u local.get $var12 global.get $global261 i32.lt_u i32.or if local.get $var12 call $__handle_stack_overflow // Trace seems to suggest this is the call that ends up failing to resolve end local.get $var12 global.set $__stack_pointer local.get $var8 return ) ```
Here's my .cargo/config.toml ```toml [target.wasm32-unknown-emscripten] rustflags = [ "-C", "link-args=-sSIDE_MODULE=2", "-C", "link-args=-sASSERTIONS=2", "-C", "link-arg=-fwasm-exceptions", "-C", "link-args=-sSUPPORT_LONGJMP=wasm", "-C", "llvm-args=-wasm-enable-sjlj", # "-C", "link-args=-pthread", # Seems to cause issues with both chromium and firefox "-C", "target-feature=+atomics,+bulk-memory,+mutable-globals", "-C", "link-args=-sEXPORT_ALL=1", "-C", "link-arg=-O0", "-C", "link-arg=-g", "-C", "link-arg=-sDEMANGLE_SUPPORT=1", "-C", "llvm-args=-enable-emscripten-cxx-exceptions=0", "-C", "link-args=-sDISABLE_EXCEPTION_CATCHING=1", "-Zlink-native-libraries=no" ] ```
Bromeon commented 1 month ago

Getting a bizarre issue that I can't find a solution for. I've tried every single flag combination in this thread, but I keep getting "resolved is not a function".

Try disabling -Clink-arg=-sASSERTIONS=2.

However I also got some different problems, most notably

which I couldn't resolve. There's some discussion around it on Discord.

Rune580 commented 1 month ago

Disabling -Clink-arg=-sASSERTIONS=2 resulted in getting: Chromium: image Firefox: image

Rune580 commented 3 weeks ago

Figured out a solution for my problem, documenting it here for anyone else who encounters a similar problem.

Following the book for setting up a project for web export, the .carg/config.toml contains this flag "-C", "link-args=-pthread", but with this flag enabled I couldn't get my project to load in either Firefox or Chromium. So I tried adding in every flag I found in this issue thread, but no combinations ended up fixing my issue. So I thought to remove the flag "-C", "link-args=-pthread", this ended up with me getting the errors found from my previous comments.

After going through the 5 stages of grief over the course of 5 days, I decided to try everything I could think of, skipping past all of the attempts that didn't work, here's what did work:

  1. Followed the docs for exporting to web

  2. Using this for my .cargo/config.toml

    [target.wasm32-unknown-emscripten]
    rustflags = [
    "-C", "link-args=-sSIDE_MODULE=2",
    "-C", "link-args=-pthread", # was -sUSE_PTHREADS=1 in earlier emscripten versions
    "-C", "target-feature=+atomics,+bulk-memory,+mutable-globals",
    "-Z", "link-native-libraries=no",
    "-Z", "link-native-libraries=no",
    "-C", "link-arg=-fwasm-exceptions",
    "-C", "link-args=-sSUPPORT_LONGJMP=wasm",
    "-C", "llvm-args=-enable-emscripten-cxx-exceptions=0",
    "-C", "llvm-args=-wasm-enable-sjlj",
    "-C", "link-args=-sDISABLE_EXCEPTION_CATCHING=1",
    ]
  3. Used emcc 3.1.66

  4. Enabled Thread Support in the web export image

Now I can load my project in Firefox and Chromium. I don't know what specifically ended up fixing my problems, but everything works for me now.

Bromeon commented 3 weeks ago

Thanks a lot! What is really not great at the moment is that the .cargo/config.toml lives completely outside the godot-rust knowledge, however it really impacts how the emscripten compilation works. It would be much better if we could generate this (so that users don't have to) or at least validate some of the flags and see if they're compatible with our library.

For example, gdext has two features: experimental-wasm and experimental-wasm-nothreads, the latter runs in "no threads" mode and requires matching configuration in Godot's export settings as well as the .cargo/config.toml flags.

I'm not sure what's the best way to keep library options in sync with .cargo/config.toml and export settings... ideally better than looking for certain files in certain directories and trying to parse them?

Rune580 commented 3 weeks ago

Maybe a hacky solution could be to have a macro or function that is called from a user created build.rs file, that parses and modifies the .cargo/config.toml.

For now though the docs should be updated to indicate the experimental-wasm and experimental-wasm-nothreads features and the required export options.

Jaso333 commented 3 weeks ago

Figured out a solution for my problem, documenting it here for anyone else who encounters a similar problem.

Following the book for setting up a project for web export, the .carg/config.toml contains this flag "-C", "link-args=-pthread", but with this flag enabled I couldn't get my project to load in either Firefox or Chromium. So I tried adding in every flag I found in this issue thread, but no combinations ended up fixing my issue. So I thought to remove the flag "-C", "link-args=-pthread", this ended up with me getting the errors found from my previous comments.

After going through the 5 stages of grief over the course of 5 days, I decided to try everything I could think of, skipping past all of the attempts that didn't work, here's what did work:

  1. Followed the docs for exporting to web
  2. Using this for my .cargo/config.toml
[target.wasm32-unknown-emscripten]
rustflags = [
    "-C", "link-args=-sSIDE_MODULE=2",
    "-C", "link-args=-pthread", # was -sUSE_PTHREADS=1 in earlier emscripten versions
    "-C", "target-feature=+atomics,+bulk-memory,+mutable-globals",
    "-Z", "link-native-libraries=no",
    "-Z", "link-native-libraries=no",
    "-C", "link-arg=-fwasm-exceptions",
    "-C", "link-args=-sSUPPORT_LONGJMP=wasm",
    "-C", "llvm-args=-enable-emscripten-cxx-exceptions=0",
    "-C", "llvm-args=-wasm-enable-sjlj",
    "-C", "link-args=-sDISABLE_EXCEPTION_CATCHING=1",
]
  1. Used emcc 3.1.66
  2. Enabled Thread Support in the web export image

Now I can load my project in Firefox and Chromium. I don't know what specifically ended up fixing my problems, but everything works for me now.

thank you! I was having a whole raft of issue up to this point. Checking the "Thread Support" checkbox in Godot and using your cargo.toml make everything work all of a sudden!

MrZak-dev commented 1 week ago

@Rune580

Thanks a lot, i was having the same issue resolved is not a function with the same error

tmp_js_export.html:139 TypeError: resolved is not a function
    at stubs.<computed> (tmp_js_export.js:9:30850)
    at gdext_rust_init

and your solution worked for me , using emcc 3.1.39 and Godot 4.3.stable

PgBiel commented 1 week ago

Just to clarify, you don't have to enable Thread support if you remove the -pthread flag and add the experimental-wasm-nothreads feature to gdext. In fact, you can have two separate builds (one with and one without thread support) for the same extension so that the Thread support toggle in the editor works properly (which is the recommended approach, other than only building your extension for threaded environments). We will be adding this to the docs; see my comment in the relevant PR for instructions in the meantime: https://github.com/godot-rust/book/pull/56#discussion_r1782111773