Skip to content

Add wasm-bindgen support#23493

Open
walkingeyerobot wants to merge 87 commits into
emscripten-core:mainfrom
walkingeyerobot:wbg-walkingeyerobot
Open

Add wasm-bindgen support#23493
walkingeyerobot wants to merge 87 commits into
emscripten-core:mainfrom
walkingeyerobot:wbg-walkingeyerobot

Conversation

@walkingeyerobot
Copy link
Copy Markdown
Collaborator

This is an early draft PR for the purposes of gathering feedback early. There are also pending changes to wasm-bindgen.

How this works:

  1. Cargo builds Rust code targeting wasm32-unknown-emscripten into a .a file.
  2. Emscripten is invoked with any C++ sources and the just built Rust .a file.
  3. Emscripten builds C++ sources and then calls out to wasm-ld to link the C++ and Rust into a .wasm file.
  4. wasm-bindgen is run on that .wasm file, producing a new .wasm file, a library.js file, and a pre.js file.
  5. Emscripten constructs its own .js, integrating the wasm-bindgen .js files.

You can see a demo more easily at https://github.com/walkingeyerobot/cxx-rust-demo. library_wbg.js and pre.js are approximately what will be produced by wasm-bindgen for consumption by Emscripten.

Some TODOs:

  1. Figure out how to pass the exported symbols from the rust compiler to Emscripten. These are symbols that need to be passed to wasm-ld so they're not removed in the final .wasm but that may not necessarily be present after wasm-bindgen processes the .wasm. wasm-bindgen at compile time puts the information it needs to generate JS inside the .wasm file itself in the form of _describe functions. These functions are then removed after JS generation.
  2. Merge the .js files produced by wasm-bindgen. This shouldn't be that hard; I just haven't gotten around to it yet. This would simplify the code for both Emscripten and wasm-bindgen.
  3. Get wasm-bindgen tests to pass. Early efforts here have revealed some very odd compiler differences between -unknown and -emscripten that I'll have to fix.
  4. Have this work end-to-end via wasm-pack. I'll have a draft PR for this soon (tm).

I'm mostly looking for feedback on the first point about exported symbols and about the general addition of -sWASM_BINDGEN to Emscripten. Again, this is very early, but it's a pretty big feature, so I thought it best to start discussions now.

cc @daxpedda, who I've been working with on the wasm-bindgen side.

@kripken
Copy link
Copy Markdown
Member

kripken commented Jan 24, 2025

wasm-bindgen at compile time puts the information it needs to generate JS inside the .wasm file itself in the form of _describe functions.

Does rustc then read the wasm to find those function names, and pass those names to wasm-ld? (if not, how does it find those names?)

In general if we need to read metadata-type info from the wasm, then we have a minimal parser in tools/webassembly.py. If we need something more complex, a binaryen pass is an option.

@walkingeyerobot
Copy link
Copy Markdown
Collaborator Author

wasm-bindgen itself is two pieces: a library that allows you to annotate your rust code marking things to be exported, and a tool that consumes a .wasm file and reads those annotations to produce a companion js file. rustc knows about those function names because wasm-bindgen as a library provided the annotations. If rustc invokes the linker itself, it's able to pass that information along. However, because we need to also build C++, we're only using rustc to compile and not drive the whole process, so we need to have it output that information elsewhere.

One (very naive) possibility is to have rustc invoke a fake linker that just writes the -sEXPORTED_FUNCTIONS to a file for emscripten to read later.

Comment thread tools/link.py Outdated
Comment thread tools/link.py Outdated
Comment thread tools/building.py Outdated
Comment thread tools/building.py Outdated
Comment thread tools/building.py Outdated
Comment thread tools/building.py Outdated
gergelyvagujhelyi added a commit to gergelyvagujhelyi/flutter_rust_bridge that referenced this pull request Apr 22, 2026
…arget

`WorkerPool::spawn` calls `wasm_bindgen::module()` to forward the
current Wasm module handle to a newly-spawned Web Worker. That
intrinsic is only supported by wasm-bindgen-cli's OutputMode::{Web,
NoModules, Node, Module} (see `wasm-bindgen-cli-support/src/js/mod.rs`
`Intrinsic::Module`, which bails out for Bundler and Emscripten).

An emscripten fork with `-sWASM_BINDGEN` integration (e.g. the
`walkingeyerobot/emscripten` draft upstream of
emscripten-core/emscripten#23493) inserts the
`__wasm_bindgen_emscripten_marker` custom section, which forces
wasm-bindgen-cli into `OutputMode::Emscripten` unconditionally. In that
mode the `__wbindgen_module` intrinsic bails, so post-link fails with:

    failed to generates bindings for import of
    `__wbindgen_placeholder__::__wbg___wbindgen_module_<hash>`

This happens even when the caller never spawns a worker — the mere
reference in the dep graph is enough. Web Workers aren't usable from
the emscripten target regardless (single-threaded model), so fall back
to a null module handle when `target_os = "emscripten"`. Any runtime
call to `spawn` would have failed on that target anyway; this just
keeps the import out of the wasm so wasm-bindgen post-link can complete.

Verified on `wasm32-unknown-emscripten` with wasm-bindgen-cli 0.2.118 +
the walkingeyerobot emscripten fork: `cargo build --release --bin
nobodywho_flutter_web --target wasm32-unknown-emscripten` now produces
a ~15 MB .wasm + matching JS glue that loads and instantiates in Node.
gergelyvagujhelyi added a commit to gergelyvagujhelyi/flutter_rust_bridge that referenced this pull request Apr 23, 2026
…arget

`WorkerPool::spawn` calls `wasm_bindgen::module()` to forward the
current Wasm module handle to a newly-spawned Web Worker. That
intrinsic is only supported by wasm-bindgen-cli's OutputMode::{Web,
NoModules, Node, Module} (see `wasm-bindgen-cli-support/src/js/mod.rs`
`Intrinsic::Module`, which bails out for Bundler and Emscripten).

An emscripten fork with `-sWASM_BINDGEN` integration (e.g. the
`walkingeyerobot/emscripten` draft upstream of
emscripten-core/emscripten#23493) inserts the
`__wasm_bindgen_emscripten_marker` custom section, which forces
wasm-bindgen-cli into `OutputMode::Emscripten` unconditionally. In that
mode the `__wbindgen_module` intrinsic bails, so post-link fails with:

    failed to generates bindings for import of
    `__wbindgen_placeholder__::__wbg___wbindgen_module_<hash>`

This happens even when the caller never spawns a worker — the mere
reference in the dep graph is enough. Web Workers aren't usable from
the emscripten target regardless (single-threaded model), so fall back
to a null module handle when `target_os = "emscripten"`. Any runtime
call to `spawn` would have failed on that target anyway; this just
keeps the import out of the wasm so wasm-bindgen post-link can complete.

Verified on `wasm32-unknown-emscripten` with wasm-bindgen-cli 0.2.118 +
the walkingeyerobot emscripten fork: `cargo build --release --bin
nobodywho_flutter_web --target wasm32-unknown-emscripten` now produces
a ~15 MB .wasm + matching JS glue that loads and instantiates in Node.
@walkingeyerobot walkingeyerobot marked this pull request as ready for review April 27, 2026 19:40
@walkingeyerobot
Copy link
Copy Markdown
Collaborator Author

I believe this is ready for review! :D

@walkingeyerobot walkingeyerobot changed the title [DRAFT] add wasm-bindgen support add wasm-bindgen support Apr 29, 2026
@sbc100 sbc100 changed the title add wasm-bindgen support Add wasm-bindgen support May 15, 2026
@sbc100
Copy link
Copy Markdown
Collaborator

sbc100 commented May 15, 2026

Sorry for the delay reviewing this. Just getting to it now.

Is the PR description up-to-date with current state of things?

Comment thread tools/config.py
'CACHE',
'PORTS',
'COMPILER_WRAPPER',
'WASM_BINDGEN',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we avoid the new config setting completely and just rely on wasm-bindgen being in the PATH when a user added -sWASM_BINDGEN.

This is what we do for tsc I believe. I'm loath to add more config setting if we can possibly avoid it.

Comment thread .circleci/config.yml
# We use an out-of-tree cache directory so it can be part of the
# persisted workspace (see below).
echo "CACHE = os.path.expanduser('~/cache')" >> .emscripten
echo "WASM_BINDGEN = os.path.expanduser('~/.cargo/bin/wasm-bindgen')" >> .emscripten
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we just add ~/.cargo/bin to the PATH instead? (see commend in config.py)

Comment thread tools/emscripten.py
for file_path in bindgen_tsd:
with open(file_path, encoding='utf-8') as file:
for line in file:
out += f'{line}'
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can these 3 lines be replaced with just out += read_file(file_path)?

Comment thread tools/link.py

if settings.WASM_BINDGEN:
building.run_wasm_bindgen(in_wasm)
settings.JS_LIBRARIES += [get_emscripten_temp_dir() + '/bindgen_out/library_bindgen.js']
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can run_wasm_bindgen return the filename here?

bindgen_jslib = building.run_wasm_bindgen(in_wasm)
settings.JS_LIBRARIES.append(bindgen_jslib)

Comment thread tools/utils.py
"""

value: str
is_file: int
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Perhaps putting this in cmdline.py would make more sense?

I'm still not clear why we need to move this, but trying to understand now.

Comment thread test/test_other.py
def test_rust_integration_basics(self):
copytree(test_file('rust/basics'), '.')
self.run_process(['cargo', 'build', '--target=wasm32-unknown-emscripten'])
self.run_process(['cargo', 'build'])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why isn't --target needed here anymore? .. Oh I see its in the cargo.toml file now.

Maybe just revert these changes to test_rust_integration_basics ? Since they seem unrelated?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants