Skip to content

feat: support wasm32-unknown-emscripten target#1583

Open
guybedford wants to merge 7 commits into
wasm-bindgen:masterfrom
guybedford:feat/emscripten-target
Open

feat: support wasm32-unknown-emscripten target#1583
guybedford wants to merge 7 commits into
wasm-bindgen:masterfrom
guybedford:feat/emscripten-target

Conversation

@guybedford
Copy link
Copy Markdown
Contributor

@guybedford guybedford commented May 16, 2026

This adds support for the wasm32-unknown-emscripten target alongside the existing wasm32-unknown-unknown flow. The emscripten target makes std-library APIs that aren't available under wasm32-unknown-unknown usable from wasm-pack-built crates: SystemTime, std::env, std::fs (via MEMFS), HashMap default random state, rand::random via getentropy, and POSIX-style I/O.

The build runs four discrete phases, each a self-contained tool invocation:

  1. cargo buildlibrustworker.a (staticlib)
  2. emcc link → bare .wasm (no JS, just wasm-ld)
  3. wasm-bindgen → rewritten .wasm + library_bindgen.js
  4. emcc --post-link → final .mjs ESM + .wasm

Included:

  • target detection via --target arg, CARGO_BUILD_TARGET env var, and a workspace-aware .cargo/config.toml walk (matches cargo's own precedence)
  • emcc auto-discovery via PATH / $EMSDK / ~/emsdk plus a soft version check (warns below 3.1.60 — the floor for -sSOURCE_PHASE_IMPORTS)
  • per---target mapping to coherent emcc settings (bundler / web / module / nodejs / deno; rejects no-modules)
  • source-phase imports (import source wasmModule from './foo.wasm') for --target module
  • -O2 optimization (capped to avoid wasm-opt's export minifier renaming the wasm-bindgen-required exports)
  • crate-type diagnostics: emscripten requires staticlib, default targets require cdylib; both surface clear errors when misconfigured instead of cryptic downstream failures
  • wasm-pack new --emscripten template alongside the existing one
  • docs at docs/src/emscripten-target.md
  • 8 integration tests + 5 unit tests

Three supporting changes came along while building this and apply to the default flow too:

  • WASM_BINDGEN_BIN env var support, for pointing wasm-pack at a locally-built wasm-bindgen during iteration
  • wasm-opt invocation now passes the full Wasm 3.0 feature flag set so it handles any standard wasm feature instead of erroring on exception-handling, bulk-memory, etc.
  • the wasm-pack publish interactive prompt now offers deno and module alongside the older targets

wasm-pack test is rejected for the emscripten target with a clear message. wasm-bindgen-test's runner isn't wired for emscripten and the integration is non-trivial.

The integration-test fixture depends on wasm-bindgen = "=0.2.122" (which includes the emscripten output-mode fixes) directly from crates.io. wasm-pack reads the fixture's lockfile and downloads the matching CLI on its own; no [patch.crates-io] block or sibling-checkout plumbing is needed. The wasm-pack new --emscripten template also pins wasm-bindgen = "0.2.122" as its floor so scaffolded projects pick up the fixes by default.

One remaining upstream item, with the corresponding workaround commented with a TODO link and removable once it lands:

  • emscripten — acorn-import-phases plugin so emcc's JS optimizer can parse source-phase imports. Has landed on emscripten main, awaiting a tagged release. Today --target module builds run unoptimized (--no-opt); once the next emscripten release ships they can use -O2 like everything else.
  • emscripten — TSD_SKIP_EXPORTS setting so --emit-tsd doesn't assert on wasm-bindgen-style multi-value-return exports. The .d.ts merge plumbing in step_emcc_post_link is in place but parked behind emscripten_dts: Option<PathBuf> = None; until that lands.

Tracking issue for panic=unwind on emscripten via wasm-bindgen linked from the docs; the template ships with panic=abort until that combination is supported.

Pre-merge review

A self-review pass surfaced and addressed (commit 6282abf):

  • Windows correctnessemcc is invoked via the full path resolved by which::which("emcc") rather than the bare string. Rust's process spawner doesn't honour PATHEXT for bare names, so Command::new("emcc") would have failed to find emcc.bat. Stashed on Build in step_check_for_emcc and threaded through emcc_link/emcc_post_link. Works on Windows since Rust 1.77's CVE-2024-24576 fix added explicit .bat handling for full paths.
  • Test PATH separatorprepend_path helper uses std::env::join_paths instead of a hardcoded ":".
  • Node driver path safetymake_node_driver JSON-encodes the .mjs URL so Windows-style backslash paths can't break out of the JS string.
  • Stale comment — corrected docs that referenced a since-removed EmscriptenRuntime interface in the .d.ts decoration.
  • Match arm consolidationTarget::Bundler | Web | Deno collapsed into a single arm; they emit identical post-link settings.
  • Cargo.lock churn — restored to a clean minimal diff (only +4 lines for the dirs crate's chain).

Testing:

source emsdk_env.sh
cargo test --test all emscripten::

The 8 integration tests exercise each --target variant end-to-end (each one drives a Node-based smoke test through the produced .mjs that covers string passing, closures, typed arrays, Result, classes, and Rust→JS imports), plus rejection cases for invalid combinations. 5 unit tests cover the version parser and the workspace-aware .cargo/config.toml walk.

Ready to merge once CI is green; will land after the next emscripten tagged release so --target module builds can drop --no-opt.

@guybedford guybedford force-pushed the feat/emscripten-target branch 2 times, most recently from 77b5eca to 4f67a44 Compare May 16, 2026 01:19
Copy link
Copy Markdown
Contributor

@walkingeyerobot walkingeyerobot left a comment

Choose a reason for hiding this comment

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

This looks awesome, thanks!

I think this is awesome overall, but I did want to ask: do we need to have emcc invoke wasm-ld? What are the pros and cons vs just invoking wasm-ld directly here? I think this is a pretty minor point and you could reasonably ignore this question.

Comment thread docs/src/emscripten-target.md Outdated
renaming.
- **`--target module` builds run unoptimized today.** emcc's bundled JS
optimizer (`acorn-optimizer`) can't parse `import source` syntax. Pass
`--no-opt` for `--target module` builds, or wait for the upstream emcc
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.

a linked issue / PR here would be helpful. then once the upstream fix is in, readers could see the PR was merged and would know this is no longer the case.

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.

The upstream fix has now landed for this one, I will merge it in here and reenable after the next emscripten release.

guybedford added a commit to guybedford/wasm-pack that referenced this pull request May 22, 2026
Per review feedback on wasm-bindgen#1583, link to the upstream tracking issue at
wasm-bindgen#5165 so readers can see when this limitation is lifted.
@guybedford guybedford marked this pull request as ready for review May 22, 2026 22:29
guybedford added a commit to guybedford/wasm-pack that referenced this pull request May 22, 2026
Per review feedback on wasm-bindgen#1583, link to the upstream tracking issue at
wasm-bindgen#5165 so readers can see when this limitation is lifted.
@guybedford guybedford force-pushed the feat/emscripten-target branch from 2d77d3d to 6282abf Compare May 22, 2026 22:48
Adds a complete build pipeline for the wasm32-unknown-emscripten target
alongside the existing wasm32-unknown-unknown flow. The emscripten target
unlocks std-library APIs that aren't available under wasm32-unknown-unknown:
SystemTime, std::env, std::fs (MEMFS), HashMap default random state,
rand::random via getentropy, and POSIX-style I/O.

Resolves #<TBD>.

The emscripten pipeline runs four discrete phases, each one a self-
contained tool invocation:

  1. cargo build → librustworker.a (staticlib)
  2. emcc link → bare .wasm (no JS, just wasm-ld)
  3. wasm-bindgen → rewritten .wasm + library_bindgen.js
  4. emcc --post-link → final .mjs ESM + .wasm

* target detection via --target arg, CARGO_BUILD_TARGET env var, and
  workspace-aware .cargo/config.toml walk (matches cargo's own precedence)
* emcc auto-discovery via PATH / $EMSDK / ~/emsdk plus a soft version
  check (warns below 3.1.60 which is the floor for -sSOURCE_PHASE_IMPORTS)
* per --target mapping to coherent emcc settings (bundler/web/module/
  nodejs/deno; rejects no-modules)
* source-phase imports (`import source` syntax) for --target module
* -O2 optimization (capped to avoid wasm-opt's export minifier renaming
  wasm-bindgen-required exports)
* crate-type diagnostics: emscripten requires staticlib, default targets
  require cdylib, both produce clear errors when misconfigured
* a `wasm-pack new --emscripten` template alongside the existing template
* docs page at docs/src/emscripten-target.md
* 8 integration tests + 5 unit tests covering the matrix

Supporting changes that also benefit the default target:

* WASM_BINDGEN_BIN env var: lets developers point wasm-pack at a local
  wasm-bindgen build for iteration
* wasm-opt invocation passes the full Wasm 3.0 feature flag set so it
  can handle modules using any standard wasm feature
* publish prompt now offers deno + module alongside the older targets

`wasm-pack test` is rejected for emscripten with a clear message — the
wasm-bindgen-test runner isn't wired up for emscripten yet.

Two upstream fixes are in flight and will let us drop temporary
workarounds once they land:

* emscripten PR: acorn-import-phases plugin to parse source-phase
  imports in the JS optimizer (currently --target module builds run
  unoptimized to dodge this)
* emscripten PR: TSD_SKIP_EXPORTS setting so emcc's --emit-tsd respects
  wasm-bindgen-owned exports (TSD merge plumbing is in place but parked
  until this lands)

The wasm-bindgen "emscripten-output-fixes" branch is pinned via
[patch.crates-io] in the integration-test fixture; it'll be dropped
once the fixes land in a wasm-bindgen release.
Replaces the parked emcc --emit-tsd + textual-merge plumbing with a
direct decoration approach. wasm-pack appends a small EmscriptenRuntime
interface, a `MainModule = EmscriptenRuntime & BindgenModule`
intersection, and a default-exported factory declaration to the
wasm-bindgen-generated .d.ts.

The decoration makes `import M from "./<name>.mjs"` type-check cleanly
without depending on emcc's TSD generator (which currently asserts on
wasm-bindgen-style multi-value returns) or any upstream emcc PR. For a
pure-Rust wasm-pack package the TS surface is fully knowable to us:
wasm-bindgen owns the exports, emcc's runtime is a small curated set,
and there's no user C/C++ to type. Users who need additional emscripten
runtime members typed can extend the .d.ts after the build.

Drops emcc_post_link's emit_tsd parameter, the merge_emscripten_and_bindgen_dts
helper, and the parked code path in step_emcc_post_link. Adds an
integration test that asserts the decoration's shape.
Adds three new exports to the integration-test fixture:

* `rs_hostname` via `#[wasm_bindgen(module = "node:os")]` — exercises the
  ESM-import path. wasm-bindgen now emits these to a sidecar
  `library_bindgen.extern-pre.js` that wasm-pack passes to emcc via
  `--extern-pre-js`, so the imports land at module top-level.
* `rs_log` via `js_namespace = console` — single-level namespace on a
  global. The test driver stubs `console.log` to capture the call.
* `rs_path_posix_join` via `js_namespace = posix` + `module = "node:path"` —
  namespace on an ESM-imported binding (not on globalThis).
* `Calc` class with `js_namespace = ["app", "math"]` — exercises a
  namespaced *export*. The class attaches at `Module.app.math.Calc`.

Threads the `library_bindgen.extern-pre.js` sidecar through
`step_emcc_post_link`: detects the file, passes it to emcc via
`--extern-pre-js`, then cleans it up alongside the other intermediates.
The emscripten output-mode fixes (#5156) landed in wasm-bindgen 0.2.122,
so the integration-test fixture can drop the `[patch.crates-io]` block
pinned at the `emscripten-output-fixes` branch and instead depend on
`wasm-bindgen = "=0.2.122"` from crates.io.

With the released crate now the source of truth, the
sibling-wasm-bindgen-checkout auto-discovery in the test driver is no
longer needed: `wasm-pack build` reads the fixture's lockfile and
downloads the matching CLI from crates.io's release artifacts on its
own. The `install_local_wasm_bindgen` fallback was hard-pinned at
0.2.100 and would have version-mismatched against the macro anyway.

Kept as future debugging hooks (no current consumers): the existing
`WASM_BINDGEN_BIN` env var (plumbed through src/install/mod.rs) and
a new `patched_emcc_bin` + `prepend_path` pair driven by
`EMSCRIPTEN_BIN` for testing future emcc fixes against the current
wasm-pack.
Per review feedback on wasm-bindgen#1583, link to the upstream tracking issue at
wasm-bindgen#5165 so readers can see when this limitation is lifted.
Working through reviewer-flagged items before landing:

* Windows: `emcc` is invoked via the full path resolved by
  `which::which("emcc")` instead of the bare string "emcc". Stashed
  on the Build struct in `step_check_for_emcc` and threaded through
  `emcc_link` and `emcc_post_link`. The Rust process spawner doesn't
  honour PATHEXT for bare names, so `Command::new("emcc")` couldn't
  find `emcc.bat` on Windows; using the full path works since Rust
  1.77's CVE-2024-24576 fix added explicit .bat handling for full paths.

* tests: `prepend_path` (the `EMSCRIPTEN_BIN` companion helper, kept
  as a future debugging hook) now uses `std::env::join_paths` instead
  of a hardcoded `:` separator. Was a trap for the same Windows port.

* tests: `make_node_driver` JSON-encodes the .mjs URL when interpolating
  it into the JS source, so Windows-style paths with backslashes can't
  break out of the JS string literal.

* template: bumped `wasm-bindgen = "0.2"` to `"0.2.122"` so new
  projects scaffolded via `wasm-pack new --emscripten` pick up the
  emscripten output-mode fixes (wasm-bindgen#5156). Older versions
  produce broken JS/wasm under the emscripten target.

* docs: corrected stale comment that referenced an "EmscriptenRuntime"
  interface decoration. The decoration was deliberately scaled back to
  not claim runtime members (`HEAP*`, `FS`, `ccall`/`cwrap`) on
  the factory return; the comment now reflects what the code actually
  does.

* style: collapsed `Target::Bundler`, `Web`, and `Deno` post-link
  settings into a single match arm with an explanatory comment. They
  were three identical struct literals.
@guybedford guybedford force-pushed the feat/emscripten-target branch from 6282abf to 4d2253c Compare May 22, 2026 23:12
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.

2 participants