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

WebAssembly support #438

Open
Bromeon opened this issue Oct 1, 2023 · 18 comments
Open

WebAssembly support #438

Bromeon opened this issue Oct 1, 2023 · 18 comments
Labels
c: wasm WebAssembly export target feature Adds functionality to the library hard Opposite of "good first issue": needs deeper know-how and significant design work.

Comments

@Bromeon
Copy link
Member

Bromeon commented Oct 1, 2023

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.

@Bromeon Bromeon added feature Adds functionality to the library hard Opposite of "good first issue": needs deeper know-how and significant design work. labels Oct 1, 2023
@PgBiel
Copy link
Contributor

PgBiel commented Oct 2, 2023

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

(EDIT: results in this comment are outdated; see #438 (comment))

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 👀 - drop by and give their opinions as well 😉

@Esption
Copy link

Esption commented Oct 5, 2023

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

  • Making sure the emscripten version that rust uses, and that you compiled the web template with, is possibly necessary. It technically shouldn't be, but emscripten can sometimes break compatibility between releases, so YMMV.
  • Having -sEXPORT_ALL=1 is probably not necessary. Only exporting the gdext entry symbol is (probably?) necessary. But I would leave that there until you know it works.
  • Emscripten's dead code elimination can be unnecessarily aggressive on a release build. Make sure to try a debug build or lower -o if it seems like the .wasm file is strangely small. Probably related to a bug in gdext, but just a heads-up if you run into it.

@Esption
Copy link

Esption commented Nov 3, 2023

Newer info:

  • Most recent supported emcc versions for godot are emcc 3.1.39 for latest dev, and emcc 3.1.28 for godot 4.1.2 (and probably earlier). I haven't checked if the patch to push version up to emcc 3.1.39 got into godot 4.1.3
  • We probably need a way to manually register things.
  • We need to fix the UB as heck reading of a function pointer in compat_4_1.rs the quick fix is just to check and exit out if it's being ran on wasm.

@Esption
Copy link

Esption commented Nov 22, 2023

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
  1. 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.
  1. 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"
]
  1. 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.
6. 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"
  1. 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
  1. In godot, add a web target and ensure that the "Extensions Support" checkbox is ticked ON. Set it to export wherever you would like
  2. 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
Copy link
Contributor

zecozephyr commented Nov 25, 2023

Preliminary support has now been merged into master by #493.

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

@DeprecatedLuke
Copy link

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
Copy link
Member Author

Bromeon commented Apr 22, 2024

@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
Copy link
Member Author

Bromeon commented Jun 14, 2024

I wonder if godotengine/godot-cpp#1489 has relevance for us (panics might use the same mechanism as exceptions) 🤔

@PgBiel
Copy link
Contributor

PgBiel commented Jun 18, 2024

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 rust-lang/rust#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
Copy link
Contributor

PgBiel commented Jul 10, 2024

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:
    emscripten_preregistration();
  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.
    • In particular, I tried to inline the entire gdext_registration function instead of calling it, but it would still fail at CString::new and .expect. Replacing with a CStr literal (c"the JS code here") postponed the error to the function call the_cstr.as_ptr().
    • In other words, the condition warned against by the comment below is now manifesting itself:
      // Warning: It may be possible that in the process of executing the code leading up
      // to `emscripten_run_script` that we might trigger usage of one of the symbols we
      // wish to monkey patch? It seems fairly unlikely, especially as long as no i64 are
      // involved, but I don't know what guarantees we have here.
      //
      // We should keep an eye out for these sorts of failures!
  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_")) {:
    if (sym.startsWith("dynCall_")) {
  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 79edae3
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

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::<MyExtension>(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
Copy link
Contributor

PgBiel commented Jul 10, 2024

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)! 🎉

What happened?

Basically, since godotengine/godot#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: godotengine/godot-cpp#1489

I tried to add that linker flag directly, but that didn't fix it. After some googling, I got here rust-lang/rust#112195 (comment) . 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 #438 (comment) , 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
Copy link
Contributor

PgBiel commented Jul 10, 2024

Regarding single-threaded wasm builds

Godot 4.3's web export template can be compiled with threads=no to disable multi-threading (since godotengine/godot#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 godotengine/godot-cpp#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():

std::thread::current().id(),

Full logs

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

@Bromeon Bromeon added the c: wasm WebAssembly export target label Jul 10, 2024
@PgBiel
Copy link
Contributor

PgBiel commented Jul 14, 2024

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
Copy link
Member Author

Bromeon commented Sep 1, 2024

Note also some changes in flags like godotengine/godot-cpp#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
Copy link
Contributor

PgBiel commented Sep 2, 2024

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
Copy link

Rune580 commented Sep 9, 2024

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
image
image
image
image
image
image
image

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
[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
Copy link
Member Author

Bromeon commented Sep 9, 2024

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

  • Too much recursion (Chromium-based)
  • Maximum call stack size exceeded (Firefox)

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

@Rune580
Copy link

Rune580 commented Sep 9, 2024

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

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 hard Opposite of "good first issue": needs deeper know-how and significant design work.
Projects
None yet
Development

No branches or pull requests

6 participants