Skip to content

Unify wasm-bindgen output under -sWASM_BINDGEN, add -sWASM_BINDGEN=auto#27208

Open
guybedford wants to merge 1 commit into
emscripten-core:mainfrom
guybedford:wasm-bindgen-unified-flow
Open

Unify wasm-bindgen output under -sWASM_BINDGEN, add -sWASM_BINDGEN=auto#27208
guybedford wants to merge 1 commit into
emscripten-core:mainfrom
guybedford:wasm-bindgen-unified-flow

Conversation

@guybedford

@guybedford guybedford commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

This unifies wasm-bindgen output generation under a single -sWASM_BINDGEN path and adds -sWASM_BINDGEN=auto, building on the wasm-bindgen side change in wasm-bindgen/wasm-bindgen#5210 (shipped in 0.2.126). It supersedes #27179.

Previously -sWASM_BINDGEN was effectively a staticlib-only flow where emcc had to discover and own the export set itself. With #5210, wasm-bindgen hoists its clean exported API (functions, classes, enums) into top-level library symbols that self-register into EXPORTED_FUNCTIONS via its --js-library. The user-facing API now comes from wasm-bindgen's own library in every flow, so emscripten no longer needs to guess or own exports — it just surfaces what wasm-bindgen registered. That lets one -sWASM_BINDGEN path serve both whether cargo/rustc drives the link (bin crate, emcc as the linker, rustc supplies -sEXPORTED_FUNCTIONS) or emcc drives it (staticlib, exports discovered locally).

The model for a public export is the union across both systems: wasm-bindgen's self-registered API, plus emscripten's own EMSCRIPTEN_KEEPALIVE exports — minus wasm-bindgen's internal expansion glue, which must not spill into the public surface.

What's implemented:

  • The exports the wasm-bindgen expansion reaches by name — the supplied EXPORTED_FUNCTIONS (method shims, the __wbindgen_* runtime, the marker, main) plus anything its expansion adds — are captured as internal glue and kept off every export layer: the ESM wrapper (user_requested_exports), the factory Module attachment (EXPORTED_FUNCTIONS, via should_export), and the keepalive pass in finalize_wasm. main still runs automatically on init; _main isn't surfaced.
  • A genuine EMSCRIPTEN_KEEPALIVE C/C++ export is not in that internal set and remains surfaced, so a hand-written native export composes with wasm-bindgen's API in the same module.
  • Placeholder symbols wasm-bindgen consumes (__wbindgen_describe*, __externref_*, ...) are stripped so they aren't reported as undefined exports.
  • nm-based export discovery only runs for explicit -sWASM_BINDGEN when no driver supplied EXPORTED_FUNCTIONS; the rustc-driven link already lists them exactly.
  • Imported JS is wired through: library_bindgen.extern-pre.js is fed as extern-pre-js and the snippets/ dir is copied next to the output so relative imports resolve.
  • The WASM_ESM_INTEGRATION wrapper re-exports the JS library symbols that were exported (MODULARIZE=instance), and provides wasmExports via a namespace import of the wasm so by-name export access works.

-sWASM_BINDGEN=auto (replacing #27179's implicit marker detection, per review feedback there): runs wasm-bindgen only when the linked wasm carries wasm-bindgen's __wasm_bindgen_emscripten_marker custom section — how cargo/rustc opts in when driving emcc as the linker. Otherwise it's a no-op and wasm-bindgen need not be installed, so rustc can always pass it via -Clink-arg without affecting ordinary builds.

if settings.WASM_BINDGEN == 'auto':
    settings.WASM_BINDGEN = 1 if building.is_wasm_bindgen_module(in_wasm) else 0

Both output modes then expose only the clean API (e.g. a Greeter class).

Testing:

  • End-to-end test parameterized over the ESM (-sWASM_ESM_INTEGRATION) and factory (-sMODULARIZE -sEXPORT_ES6) output modes, built via cargo build with -sWASM_BINDGEN=auto (marker auto-detected), asserting the clean Greeter API works, main runs on init, and none of the raw wasm exports leak.
  • A no-marker test asserting -sWASM_BINDGEN=auto is a no-op for an ordinary hello_world.c build (doesn't require wasm-bindgen installed).
  • The staticlib integration test is extended to assert an EMSCRIPTEN_KEEPALIVE native export survives alongside the wasm-bindgen API (fails under a blanket export wipe).
  • CI installs a pinned wasm-bindgen-cli alongside rust so the flow is always exercised.

Not in scope (follow-up): preserving human-supplied EXPORTED_FUNCTIONS through wasm-bindgen linkage (the rustc-supplied set is currently indistinguishable from glue, so explicit exports aren't surfaced).

wasm-bindgen 0.2.126 (wasm-bindgen/wasm-bindgen#5210) hoists the clean
exported API (functions, classes, enums) into top-level library symbols
that self-register into EXPORTED_FUNCTIONS via its `--js-library`. The
user-facing API now comes from wasm-bindgen's library in every flow, so
emscripten no longer needs to guess or own the export set.

This collapses the previous staticlib-only handling into a single
-sWASM_BINDGEN path that works whether cargo/rustc drives the link (bin
crate, emcc as the linker, rustc supplies -sEXPORTED_FUNCTIONS) or emcc
drives it (staticlib, exports discovered locally):

- The exports the wasm-bindgen expansion reaches by name - the supplied
  EXPORTED_FUNCTIONS (method shims, the __wbindgen_* runtime, the marker,
  main) plus anything its expansion adds - are internal glue, not a user
  API. They are captured and kept off every export layer: the ESM wrapper
  (user_requested_exports), the factory Module attachment
  (EXPORTED_FUNCTIONS, via should_export), and the keepalive pass in
  finalize_wasm. `main` still runs automatically on init; `_main` isn't
  surfaced.
- A genuine EMSCRIPTEN_KEEPALIVE C/C++ export is not in that internal set
  and remains surfaced, so a hand-written native export composes with
  wasm-bindgen's API in the same module. Human-supplied EXPORTED_FUNCTIONS
  are not preserved through wasm-bindgen linkage yet (the rustc-supplied
  set is indistinguishable from glue); that can be revisited later.
- Strip the placeholder symbols wasm-bindgen consumes
  (__wbindgen_describe*, __externref_*, ...) so they aren't reported as
  undefined exports.
- Only run nm-based export discovery for explicit -sWASM_BINDGEN when no
  driver supplied EXPORTED_FUNCTIONS; the rustc-driven link already lists
  them exactly.
- Wire imported JS: feed library_bindgen.extern-pre.js as extern-pre-js
  and copy the snippets/ dir next to the output so relative imports
  resolve.
- The WASM_ESM_INTEGRATION wrapper re-exports the JS library symbols that
  were exported (MODULARIZE=instance), and provides wasmExports via a
  namespace import of the wasm so by-name export access works.

Add -sWASM_BINDGEN=auto: run wasm-bindgen only when the linked wasm
carries wasm-bindgen's __wasm_bindgen_emscripten_marker custom section,
which is how cargo/rustc opts in when driving emcc as the linker
(addressing the request to replace implicit marker detection with an
explicit flag); otherwise it is a no-op and wasm-bindgen need not be
installed.

Both output modes then expose only the clean API (e.g. a `Greeter`
class). Add an end-to-end test parameterized over the ESM and factory
output modes (built via cargo with -sWASM_BINDGEN=auto), a no-marker
test asserting auto is a no-op for an ordinary build, extend the
staticlib integration test to assert an EMSCRIPTEN_KEEPALIVE export
survives alongside the wasm-bindgen API, and install a pinned
wasm-bindgen-cli alongside rust in CI so the flow is always exercised.
@guybedford guybedford force-pushed the wasm-bindgen-unified-flow branch from b7e9291 to c10cb62 Compare June 26, 2026 23:32
@guybedford guybedford changed the title Unify wasm-bindgen output under -sWASM_BINDGEN Unify wasm-bindgen output under -sWASM_BINDGEN, add -sWASM_BINDGEN=auto Jun 26, 2026
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.

1 participant