Skip to content

Use .init_array to call constructors in WASM instead of JS snippet#1476

Merged
Bromeon merged 2 commits intogodot-rust:masterfrom
SvizelPritula:wasm-init-array
Feb 6, 2026
Merged

Use .init_array to call constructors in WASM instead of JS snippet#1476
Bromeon merged 2 commits intogodot-rust:masterfrom
SvizelPritula:wasm-init-array

Conversation

@SvizelPritula
Copy link
Copy Markdown
Contributor

Currently, godot-rust uses some JavaScript to enumerate all constructors and call them in WASM build. However, Rust can nowadays leverage Emscripten's/wasm-lds support for global constructors using the .init_array section, just like on Unix platforms. This is what the rust-ctor and inventory crates do. An inventory pull request suggest that this should work in compilers newer than 1.85 or nightly-2024-12-18.

I've personally tested it with the Dodge the Creeps example game. It worked in both threaded and unthreaded release and debug builds. The build configuration in the example is currently broken, so I used this Makefile to build it.

Breaking changes

This PR currently introduces breaking changes, as it removes the ExtensionLibrary::override_wasm_binary function, as it's no longer needed or used.
This will of course break the build of anyone who overrides it. (Or anyone who calls it, but who does that?) This function is available even without the experimental-wasm feature.
An alternative would be to just mark the function as #[deprecated] until the next major release, or forever.

Checks

It's worth noting that inventory adds a static flag to each constructor on WebAssembly targets that makes the constructors idempotent by returning early on all but the first call.
This is because wasm-ld inserts a call to __wasm_call_ctors to the start of every exported function if it heuristically detects "command-style linkage". It will do so only if __wasm_call_ctors is neither called anywhere in the binary nor exported from the binary, i.e. if there is no chance that the constructors would be called otherwise. Emscripten will always mark __wasm_call_ctors as exported (except in standalone mode, which we're not using and is probably incompatible with side modules).
Since we're only supporting Emscripten, this should not be an issue for us. I added an assertion that emits a compile error when targeting non-Emscripten WebAssembly. It's worth noting that inventory only added their checks when adding support for non-Emscripten targets. rust-ctor doesn't seem to contain any checks.

We could of course still add some checks for repeated calls, but if we do, I think we should make repeated calls panic instead of doing nothing. If the built library doesn't call __wasm_call_ctors and doesn't export it either, then something must have gone wrong at build time. It's worth noting that if I manually call __wasm_call_ctors a second time, godot-rust already panics with Godot class `…` is defined multiple times in Rust; ….

Copy link
Copy Markdown
Contributor

@PgBiel PgBiel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly LGTM, let's try to wait for #1275 so we can at least make sure we aren't breaking anything important.

Big thanks for your research and PR!

This PR currently introduces breaking changes

I'd say it's unlikely anyone was using that in any sufficiently complicated way, and the fix is very straightforward (just remove the function definition, the wasm binary doesn't have to be renamed).

Of course, if policy mandates, we can always wait before 0.5 to land this, though I'm kinda leaning towards considering this as part of the wasm being experimental thing.

It's worth noting that if I manually call __wasm_call_ctors a second time, godot-rust already panics

Right. I think this is enough of a guarantee for me.

Comment on lines +67 to +68
#[cfg(all(target_family = "wasm", not(target_os = "emscripten")))]
compile_error!("Wasm targets not using Emscripten are not supported.");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can move this out of the macro, into the crate itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which crate? godot-ffi or godot?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe here:

gdext/godot/src/lib.rs

Lines 207 to 226 in 59c48e8

// ----------------------------------------------------------------------------------------------------------------------------------------------
// Validations
// Many validations are moved to godot-ffi. #[cfg]s are not emitted in this crate, so move checks for those up to godot-core.
#[cfg(all(target_family = "wasm", not(feature = "experimental-wasm")))]
compile_error!(
"Wasm target requires opt-in via `experimental-wasm` Cargo feature;\n\
keep in mind that this is work in progress."
);
// See also https://github.com/godotengine/godot/issues/86346.
// Could technically be moved to godot-codegen to reduce time-to-failure slightly, but would scatter validations even more.
#[cfg(all(
feature = "double-precision",
not(feature = "api-custom"),
not(feature = "api-custom-json")
))]
compile_error!("The feature `double-precision` currently requires `api-custom` or `api-custom-json` due to incompatibilities in the GDExtension API JSON. \
See: https://github.com/godotengine/godot/issues/86346");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Some checks are also in godot-ffi, I'm not sure why -- it seems to me like they could be here as well.

@Bromeon Bromeon added quality-of-life No new functionality, but improves ergonomics/internals c: wasm WebAssembly export target labels Jan 10, 2026
@SvizelPritula
Copy link
Copy Markdown
Contributor Author

I tried running tests locally, using check.sh from #1275. All tests pass in the threaded build. In the build without thread support, all the tests from godot_cell::tests::mock::blocking fail, as they fail to spawn a thread. (I wonder why. 🙂)

@GodotRust
Copy link
Copy Markdown

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1476

@SvizelPritula
Copy link
Copy Markdown
Contributor Author

Should I rebase?

@Bromeon
Copy link
Copy Markdown
Member

Bromeon commented Jan 18, 2026

As @PgBiel mentioned:

Mostly LGTM, let's try to wait for #1275 so we can at least make sure we aren't breaking anything important.

This may take some time depending on their availability -- maybe there are changes that can be directly contributed to that PR?

@PgBiel
Copy link
Copy Markdown
Contributor

PgBiel commented Jan 19, 2026

I believe we should merge the tests PR with only unit tests. Integration tests will take a while, as they are substantially more complicated, so they can be implemented in a separate PR.

So, we could test this PR with my hello-gdext-wasm project (for example) and make sure it passes the unit tests once merged, and, if that works out, this PR can likely be merged before integration tests.

@PgBiel PgBiel mentioned this pull request Jan 27, 2026
8 tasks
@Bromeon
Copy link
Copy Markdown
Member

Bromeon commented Feb 3, 2026

#1275 is now merged. Could you rebase and fix CI? 🙂

@SvizelPritula
Copy link
Copy Markdown
Contributor Author

Done. I ran the CI on my fork and it passed, but it got a compilation warning on wasm due to the translate_functional macro helper now being unused. I deleted it. (Or do you want to keep it around, in case it's useful in the future?) I also removed the now unneeded dependency of godot-ffi on godot-macros.

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

This is a beautiful cleanup, great job! 👍
I still need to test this with some projects.


[...] it got a compilation warning on wasm due to the translate_functional macro helper now being unused. I deleted it. (Or do you want to keep it around, in case it's useful in the future?)

Yes, that would be nice -- you can annotate it with #[expect(dead_code)] I think.


This PR currently introduces breaking changes, as it removes the ExtensionLibrary::override_wasm_binary function, as it's no longer needed or used.

Just to understand this a bit better -- this was introduced in #1093 to allow custom binary names. In particular, it allows coexisting binaries for threads and nothreads, see motivating example in that PR.

Why is this no longer needed? It seems orthogonal to the registration problem addressed here.


Eventually, you could also squash commits 🙂

godot/src/lib.rs Outdated
Comment on lines +228 to +233
// On non-Emscripen targets, wasm-ld will insert a call to __wasm_call_ctors
// (which calls all constructors) to the start all exported functions, if it
// detects that __wasm_call_ctors is never called and not exported.
// This could cause constructors to run multiple times. Emscripen should always
// export __wasm_call_ctors and call it at runtime.
// See https://github.com/godot-rust/gdext/pull/1476 for more info and links.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: please use roughly 120-145 chars line width.

@SvizelPritula
Copy link
Copy Markdown
Contributor Author

This PR currently introduces breaking changes, as it removes the ExtensionLibrary::override_wasm_binary function, as it's no longer needed or used.

Just to understand this a bit better -- this was introduced in #1093 to allow custom binary names. In particular, it allows coexisting binaries for threads and nothreads, see motivating example in that PR.

Why is this no longer needed? It seems orthogonal to the registration problem addressed here.

Because we no longer need to know the name of the binary, like on other platforms. The only reason the method existed was so that the JavaScript code would know which library to search for constructors to call. (Unless you planned for some other use, but I don't see what that would be.)

@Bromeon Bromeon enabled auto-merge February 5, 2026 22:04
auto-merge was automatically disabled February 6, 2026 09:03

Head branch was pushed to by a user without write access

@SvizelPritula
Copy link
Copy Markdown
Contributor Author

SvizelPritula commented Feb 6, 2026

I accidentally merged master into by this branch. I removed the merge commit, but it still cleared workflow status, even though workflows already ran on this exact commit hash. 😞 Sorry, I didn't mean to do that, it's my first time seeing Github's UI with merge queues...

At any rate, I think the PR is ready now, I've re-added translate_functional, re-line-wrapped the comment and squashed all commits into two.

@SvizelPritula SvizelPritula requested a review from Bromeon February 6, 2026 09:34
@Bromeon
Copy link
Copy Markdown
Member

Bromeon commented Feb 6, 2026

That's too bad, in the meantime GitHub broke merge queues and Godot broke our guaranteed_ordering test 🙈

This will have to wait a bit for a new opportunity window to merge... Thanks already for the great work nonetheless!

@Bromeon Bromeon enabled auto-merge February 6, 2026 16:57
@Bromeon Bromeon added this pull request to the merge queue Feb 6, 2026
Merged via the queue into godot-rust:master with commit a40eaab Feb 6, 2026
23 checks passed
@Bromeon
Copy link
Copy Markdown
Member

Bromeon commented Feb 6, 2026

Thanks a lot for your contribution! 😊

@PgBiel
Copy link
Copy Markdown
Contributor

PgBiel commented Feb 6, 2026

By the way, I tested this PR just now on hello-gdext-wasm, and all seemed to work fine. Impressive work!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: wasm WebAssembly export target quality-of-life No new functionality, but improves ergonomics/internals

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants