feat: support wasm32-unknown-emscripten target#1583
Open
guybedford wants to merge 7 commits into
Open
Conversation
77b5eca to
4f67a44
Compare
walkingeyerobot
approved these changes
May 18, 2026
Contributor
walkingeyerobot
left a comment
There was a problem hiding this comment.
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.
| 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 |
Contributor
There was a problem hiding this comment.
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.
Contributor
Author
There was a problem hiding this comment.
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
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.
2d77d3d to
6282abf
Compare
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.
6282abf to
4d2253c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This adds support for the
wasm32-unknown-emscriptentarget alongside the existingwasm32-unknown-unknownflow. The emscripten target makesstd-library APIs that aren't available underwasm32-unknown-unknownusable from wasm-pack-built crates:SystemTime,std::env,std::fs(via MEMFS),HashMapdefault random state,rand::randomviagetentropy, and POSIX-style I/O.The build runs four discrete phases, each a self-contained tool invocation:
cargo build→librustworker.a(staticlib)emcclink → bare.wasm(no JS, just wasm-ld)wasm-bindgen→ rewritten.wasm+library_bindgen.jsemcc --post-link→ final.mjsESM +.wasmIncluded:
--targetarg,CARGO_BUILD_TARGETenv var, and a workspace-aware.cargo/config.tomlwalk (matches cargo's own precedence)PATH/$EMSDK/~/emsdkplus a soft version check (warns below 3.1.60 — the floor for-sSOURCE_PHASE_IMPORTS)--targetmapping to coherent emcc settings (bundler / web / module / nodejs / deno; rejects no-modules)import source wasmModule from './foo.wasm') for--target module-O2optimization (capped to avoid wasm-opt's export minifier renaming the wasm-bindgen-required exports)staticlib, default targets requirecdylib; both surface clear errors when misconfigured instead of cryptic downstream failureswasm-pack new --emscriptentemplate alongside the existing onedocs/src/emscripten-target.mdThree supporting changes came along while building this and apply to the default flow too:
WASM_BINDGEN_BINenv var support, for pointing wasm-pack at a locally-built wasm-bindgen during iterationwasm-optinvocation now passes the full Wasm 3.0 feature flag set so it handles any standard wasm feature instead of erroring onexception-handling,bulk-memory, etc.wasm-pack publishinteractive prompt now offersdenoandmodulealongside the older targetswasm-pack testis 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. Thewasm-pack new --emscriptentemplate also pinswasm-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:
acorn-import-phasesplugin so emcc's JS optimizer can parse source-phase imports. Has landed on emscriptenmain, awaiting a tagged release. Today--target modulebuilds run unoptimized (--no-opt); once the next emscripten release ships they can use-O2like everything else.TSD_SKIP_EXPORTSsetting so--emit-tsddoesn't assert on wasm-bindgen-style multi-value-return exports. The.d.tsmerge plumbing instep_emcc_post_linkis in place but parked behindemscripten_dts: Option<PathBuf> = None;until that lands.Tracking issue for
panic=unwindon emscripten via wasm-bindgen linked from the docs; the template ships withpanic=abortuntil that combination is supported.Pre-merge review
A self-review pass surfaced and addressed (commit
6282abf):emccis invoked via the full path resolved bywhich::which("emcc")rather than the bare string. Rust's process spawner doesn't honourPATHEXTfor bare names, soCommand::new("emcc")would have failed to findemcc.bat. Stashed onBuildinstep_check_for_emccand threaded throughemcc_link/emcc_post_link. Works on Windows since Rust 1.77's CVE-2024-24576 fix added explicit.bathandling for full paths.prepend_pathhelper usesstd::env::join_pathsinstead of a hardcoded":".make_node_driverJSON-encodes the .mjs URL so Windows-style backslash paths can't break out of the JS string.EmscriptenRuntimeinterface in the.d.tsdecoration.Target::Bundler | Web | Denocollapsed into a single arm; they emit identical post-link settings.dirscrate's chain).Testing:
The 8 integration tests exercise each
--targetvariant end-to-end (each one drives a Node-based smoke test through the produced.mjsthat 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.tomlwalk.Ready to merge once CI is green; will land after the next emscripten tagged release so
--target modulebuilds can drop--no-opt.