Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [`test`](./commands/test.md)
- [`pack` and `publish`](./commands/pack-and-publish.md)
- [`init` (DEPRECATED)](./commands/init.md)
- [Emscripten target](./emscripten-target.md)
- [Tutorials](./tutorials/index.md)
- [Hybrid applications with Webpack](./tutorials/hybrid-applications-with-webpack/index.md)
- [Getting started](./tutorials/hybrid-applications-with-webpack/getting-started.md)
Expand Down
14 changes: 14 additions & 0 deletions docs/src/commands/new.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ template.

[`cargo-generate`]: https://github.com/ashleygwilliams/cargo-generate

## Emscripten target

Pass `--emscripten` to scaffold a project that builds for
`wasm32-unknown-emscripten` instead of the default `wasm32-unknown-unknown`:

```
wasm-pack new myproject --emscripten
```

This selects the `wasm-pack-emscripten-template` (a sibling of the default
template in the same repo). Requires [emsdk](https://emscripten.org/) to be
installed and `emcc` reachable on `PATH`. See the
[Emscripten target chapter](../emscripten-target.html) for details.

## Mode

The `wasm-pack new` command can be given an optional mode argument, e.g.:
Expand Down
179 changes: 179 additions & 0 deletions docs/src/emscripten-target.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Building for `wasm32-unknown-emscripten`

`wasm-pack` supports the `wasm32-unknown-emscripten` target as an alternative
to the default `wasm32-unknown-unknown`. The two produce wasm binaries that
behave differently at runtime: emscripten output includes a libc, file system
shims, POSIX-style APIs, and a richer JavaScript runtime around the wasm.

You generally want emscripten when:

- You need `std::time::{Instant, SystemTime}`.
- You need `std::env::{current_dir, vars}`.
- You need `std::fs::*` (backed by an in-memory MEMFS).
- You need `std::collections::HashMap` with default random state.
- You need `rand::random()` to Just Work (via `getentropy`).
- You're linking Rust against C/C++ sources via `bindgen`/`cxx`.

You generally want `wasm32-unknown-unknown` when:

- Your crate is pure Rust + `js-sys`/`web-sys`.
- You want the smallest possible runtime overhead.
- You care about cold-start time on the web.

Most projects don't need emscripten. The default target stays the right choice
for the common case.

## Prerequisites

`wasm-pack` invokes the Emscripten SDK (`emcc`) during the build. Install
[emsdk](https://emscripten.org/docs/getting_started/downloads.html) and
activate it before running `wasm-pack`:

```sh
git clone https://github.com/emscripten-core/emsdk.git ~/emsdk
cd ~/emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
```

`emcc --version` should print at least 3.1.60. Older versions lack
`-sSOURCE_PHASE_IMPORTS=1` and will fail the build with a clear error.

`wasm-pack` will auto-detect a `~/emsdk` install or honor the `$EMSDK`
environment variable if `emcc` isn't already on `PATH`. CI configurations
should `source emsdk_env.sh` before running `wasm-pack` to make `emcc`
available to child processes.

## Quick start

```sh
wasm-pack new my-pkg --emscripten
cd my-pkg
wasm-pack build
```

The first command scaffolds a project that's pre-configured for the
emscripten target. The second runs the full pipeline:

```
cargo build → emcc link → wasm-bindgen → emcc post-link
```

The result in `pkg/` matches the layout for other `wasm-pack` targets, with
two notable differences:

- The JS module has an `.mjs` extension (always ESM).
- There's no `<name>_bg.wasm`; the wasm file is just `<name>.wasm`.

## What the template generates

`wasm-pack new --emscripten <name>` produces a crate with this shape:

```
<name>/
Cargo.toml # crate-type = ["staticlib"]
.cargo/
config.toml # target = "wasm32-unknown-emscripten"
src/
lib.rs # a tiny `greet()` example
README.md
```

Two things are notable compared to the default template:

1. **`crate-type = ["staticlib"]`** — emcc consumes a static library and
links it into the final wasm itself. `cdylib` would skip emcc entirely.

2. **`.cargo/config.toml`** — selects the emscripten target and sets the
rustflags emcc expects:
- `-Cpanic=abort` — `panic=unwind` isn't supported across the wasm-bindgen
boundary on emscripten yet
([wasm-bindgen#5165](https://github.com/wasm-bindgen/wasm-bindgen/issues/5165)).
- `-Crelocation-model=static` — the staticlib is linked directly; PIC
isn't needed.
- `-Cllvm-args=-enable-emscripten-cxx-exceptions=0` — avoids pulling in
a C++ exception runtime we don't ship.

If you build your own crate (without using the template), make sure to
configure both of these.

## Build targets

The wasm-pack `--target` flag still selects the output shape:

| `--target` | Output |
|---|---|
| `bundler` (default) | `<name>.mjs` ESM async factory, env: `web,node` |
| `web` | same as `bundler` |
| `module` | `<name>.mjs` with `import source` for the wasm |
| `nodejs` | `<name>.mjs`, env: `node` |
| `deno` | `<name>.mjs`, env: `web,node` |
| `no-modules` | not supported — emcc produces module-shaped output only |

All shapes are ESM; emscripten doesn't emit CJS. The `nodejs` target is
distinguished only by the `-sENVIRONMENT=node` setting, which omits
browser-detection probes from the runtime.

The `module` target uses [source-phase imports][src-phase] for the wasm:

```js
import source wasmModule from './<name>.wasm';
```

This requires a host that understands the proposal (modern bundlers, Node 22+,
Deno).

[src-phase]: https://github.com/tc39/proposal-source-phase-imports

## Limitations

A few things differ from the default wasm-pack target:

- **`wasm-pack test` is not supported.** The `wasm-bindgen-test` runner
isn't currently wired up for emscripten; tests must run via
`cargo test` directly.
- **TypeScript declarations are wasm-bindgen-driven and wasm-pack-decorated.**
The `.d.ts` in `pkg/` is wasm-bindgen's output (covering every
`#[wasm_bindgen]` export and class), with a default-export factory
declaration appended so `import M from "./<name>.mjs"` type-checks
cleanly. The factory return type is the wasm-bindgen surface only.
emscripten runtime members (`HEAP*`, `_initialize`, `ccall`/`cwrap`,
`FS`) aren't attached to the factory return by default and are not
typed; if you need them you can extend the `.d.ts` in `pkg/` after the
build.
- **Optimization is capped at `-O2`.** emcc's `-O3` enables wasm-opt's
`--minify-imports-and-exports` pass, which renames wasm exports to
single letters — but wasm-bindgen's generated JS glue references the
original names. `-O2` produces near-equivalent output without the
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.

fix.

## How the build pipeline works

For curiosity:

1. **`cargo build`** — cargo invokes `rustc --target wasm32-unknown-emscripten`,
which under the hood links via emcc to produce a `librustworker.a`
staticlib.

2. **`emcc` link** — `wasm-pack` invokes `emcc --no-entry --oformat=bare`
to produce a bare `.wasm` from the staticlib. Symbols wasm-bindgen
needs are preserved via `-sEXPORTED_FUNCTIONS`.

3. **`wasm-bindgen`** — runs over the bare `.wasm` with `--keep-lld-exports`
(no `--target`; emscripten output mode is auto-detected from the
`__wasm_bindgen_emscripten_marker` custom section). Produces a
rewritten `_bg.wasm`, a `library_bindgen.js` (an emscripten JS
library), and a `.d.ts`.

4. **`emcc --post-link`** — combines the wasm-bindgen-rewritten wasm with
`library_bindgen.js` to emit the final `<name>.mjs` (ESM factory
function) and `<name>.wasm` (final, post-linked binary).

Each phase is a separate, self-contained tool invocation. Intermediate
artifacts (`linked.wasm`, `_bg.wasm`, `library_bindgen.js`) are not
shipped in `pkg/`.
25 changes: 21 additions & 4 deletions src/bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub fn wasm_bindgen_build(
reference_types: bool,
target: Target,
profile: BuildProfile,
emscripten: bool,
) -> Result<()> {
let out_dir = out_dir.to_str().unwrap();

Expand All @@ -47,11 +48,24 @@ pub fn wasm_bindgen_build(
cmd.arg("--reference-types");
}

let target_arg = build_target_arg(target, &bindgen_path)?;
if supports_dash_dash_target(&bindgen_path)? {
cmd.arg("--target").arg(target_arg);
if emscripten {
// wasm-bindgen's Emscripten output mode is auto-detected from the
// `__wasm_bindgen_emscripten_marker` custom section embedded in the
// wasm by the `wasm-bindgen` runtime. We must not pass `--target`
// here: passing one would force a non-Emscripten output mode and
// wasm-bindgen would fail to find the intrinsics it needs.
//
// `--keep-lld-exports` preserves the symbols `wasm-ld` exported
// (which emcc post-link expects); without it wasm-bindgen would
// strip them during its rewrite pass.
cmd.arg("--keep-lld-exports");
} else {
cmd.arg(target_arg);
let target_arg = build_target_arg(target, &bindgen_path)?;
if supports_dash_dash_target(&bindgen_path)? {
cmd.arg("--target").arg(target_arg);
} else {
cmd.arg(target_arg);
}
}

if let Some(value) = out_name {
Expand Down Expand Up @@ -121,6 +135,9 @@ fn build_target_arg_legacy(target: Target, cli_path: &Path) -> Result<String> {
}
Target::Bundler => "--browser",
Target::Deno => "--deno",
Target::Module => {
bail!("The 'module' target requires wasm-bindgen >= 0.2.99. Please update your project to a newer version of wasm-bindgen.")
}
};
Ok(target_arg.to_string())
}
110 changes: 110 additions & 0 deletions src/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,116 @@ pub fn cargo_build_wasm(
}
}

/// `cargo build --lib` variant that expects a staticlib (`.a`) artifact.
///
/// Used by the `wasm32-*-emscripten` pipeline: cargo produces a static
/// library which is then linked by emcc in a subsequent step. This is
/// otherwise identical to `cargo_build_wasm` — same flags, same profile
/// handling — but locates the `.a` filename in the artifact JSON instead
/// of `.wasm`.
pub fn cargo_build_staticlib(
path: &Path,
profile: BuildProfile,
extra_options: &[String],
target_triple: &str,
panic_unwind: bool,
) -> Result<String> {
let msg = format!("{}Compiling to staticlib...", emoji::CYCLONE);
PBAR.info(&msg);

let mut cmd = Command::new("cargo");
cmd.current_dir(path);
if panic_unwind {
cmd.arg("+nightly");
}
cmd.arg("build").arg("--lib");

if PBAR.quiet() {
cmd.arg("--quiet");
}

match profile {
BuildProfile::Profiling | BuildProfile::Release => {
cmd.arg("--release");
}
BuildProfile::Dev => {}
BuildProfile::Custom(arg) => {
cmd.arg("--profile").arg(arg);
}
}

cmd.env("CARGO_BUILD_TARGET", target_triple);

if panic_unwind {
cmd.arg("-Z").arg("build-std=std,panic_unwind");
let existing = std::env::var("RUSTFLAGS").unwrap_or_default();
let combined = if existing.is_empty() {
"-Cpanic=unwind".to_string()
} else {
format!("{existing} -Cpanic=unwind")
};
cmd.env("RUSTFLAGS", combined);
}

// Same relative→absolute path normalization as `cargo_build_wasm`.
let mut handle_path = false;
let extra_options_with_absolute_paths = extra_options
.iter()
.map(|option| -> Result<String> {
let value = if handle_path && Path::new(option).is_relative() {
std::env::current_dir()?
.join(option)
.to_str()
.ok_or_else(|| anyhow!("path contains non-UTF-8 characters"))?
.to_string()
} else {
option.to_string()
};
handle_path = matches!(&**option, "--target-dir" | "--out-dir" | "--manifest-path");
Ok(value)
})
.collect::<Result<Vec<_>>>()?;
cmd.args(extra_options_with_absolute_paths);
cmd.arg("--message-format=json");

let mut cargo_process = cmd.stdout(Stdio::piped()).spawn()?;
let final_artifact =
Message::parse_stream(BufReader::new(cargo_process.stdout.as_mut().unwrap()))
.filter_map(|msg| {
match msg {
Ok(Message::CompilerArtifact(artifact)) => return Some(artifact),
Ok(Message::CompilerMessage(msg)) => eprintln!("{msg}"),
Ok(Message::TextLine(text)) => eprintln!("{text}"),
Err(err) => log::error!("Couldn't parse cargo message: {err}"),
_ => {}
}
None
})
.last();

if !cargo_process
.wait()
.context("Failed to wait for cargo build process")?
.success()
{
bail!("`cargo build` failed, see the output above for details");
}

let lib_files: Vec<_> = final_artifact
.context("Expected at least one compiler artifact in the output of `cargo build`")?
.filenames
.into_iter()
.filter(|p| p.extension() == Some("a"))
.collect();

match <[_; 1]>::try_from(lib_files) {
Ok([filename]) => Ok(filename.into_string()),
Err(filenames) => bail!(
"Expected exactly one .a file from `cargo build` for the emscripten target, found {filenames:?}"
),
}
}

/// Runs `cargo build --tests` targeting `wasm32-unknown-unknown`.
///
/// This generates the `Cargo.lock` file that we use in order to know which version of
Expand Down
Loading
Loading