From a0be7dba643bd28eca6535a1fd78ff5dc2ca9ad3 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 26 Jun 2026 15:47:15 -0700 Subject: [PATCH 01/14] Unify wasm-bindgen output under -sWASM_BINDGEN, add -sWASM_BINDGEN=auto wasm-bindgen (wasm-bindgen/wasm-bindgen#5210, updated for the __export/ __force symbol attributes from #27436 in wasm-bindgen/wasm-bindgen#5253) hoists the clean exported API (functions, classes, enums) into top-level library symbols that self-register as exports 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. The wasm-bindgen library and CLI are pinned to the same wasm-bindgen main rev (the attribute support is not yet in a release); they must match exactly, and can move to a version pin once released. --- .circleci/config.yml | 9 ++- ChangeLog.md | 7 ++ .../tools_reference/settings_reference.rst | 7 +- src/postamble.js | 8 ++ src/settings.js | 7 +- test/rust/bindgen_greeter/.cargo/config.toml | 10 +++ test/rust/bindgen_greeter/Cargo.toml | 13 +++ test/rust/bindgen_greeter/src/main.rs | 23 ++++++ test/test_other.py | 80 +++++++++++++++++-- tools/building.py | 49 ++++++++++-- tools/emscripten.py | 14 +++- tools/link.py | 31 ++++++- tools/settings.py | 2 +- 13 files changed, 243 insertions(+), 17 deletions(-) create mode 100644 test/rust/bindgen_greeter/.cargo/config.toml create mode 100644 test/rust/bindgen_greeter/Cargo.toml create mode 100644 test/rust/bindgen_greeter/src/main.rs diff --git a/.circleci/config.yml b/.circleci/config.yml index b1436f4666ab2..b124315e45efc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -76,12 +76,19 @@ commands: install-rust: steps: - run: - name: install rust + name: install rust and wasm-bindgen + # rust and wasm-bindgen are always installed together so there is no + # CI environment with one but not the other. The wasm-bindgen-cli + # version is pinned to match the library the test crates depend on; + # wasm-bindgen requires the CLI and the library to be the exact same + # version. 0.2.127 is the first release with the emscripten + # __export/__force attribute support (wasm-bindgen/wasm-bindgen#5253). command: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y export PATH=${HOME}/.cargo/bin:${PATH} rustup target add wasm32-unknown-emscripten echo "export PATH=\"\$HOME/.cargo/bin:\$PATH\"" >> $BASH_ENV + cargo install wasm-bindgen-cli --version 0.2.127 --locked install-node-version: description: "install a specific version of node" parameters: diff --git a/ChangeLog.md b/ChangeLog.md index 74ede3362cf3d..3909ec6a8da05 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -72,6 +72,13 @@ See docs/process.md for more on how version tagging works. (`JS_BIGINT_INTEGRATION`) are universally available across all supported engines, and removes legacy JS polyfills and Binaryen lowering passes. (#27542) +- `-sWASM_BINDGEN` now runs wasm-bindgen (0.2.127 or later) as a post-link + step, unifying its output with emscripten's: wasm-bindgen's bindings are + surfaced as the user-facing API across the standard output modes (including + `-sMODULARIZE` and `-sWASM_ESM_INTEGRATION`) and its raw wasm exports are no + longer leaked. `-sWASM_BINDGEN=auto` enables this automatically when the + linked wasm was built with wasm-bindgen, so cargo/rustc builds that link via + emcc work without extra flags. (#27208) 6.0.6 - 08/05/26 ---------------- diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 185aa492a06a3..9e1089bbd2d14 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3352,7 +3352,12 @@ Default value: [] WASM_BINDGEN ============ -Run wasm-bindgen and integrate the rust-exported symbols into the rest of Emscripten's JS output. +Run wasm-bindgen and integrate the rust-exported symbols into the rest of +Emscripten's JS output. +Set to 1 to always run wasm-bindgen (e.g. a C/C++ build linking a Rust +staticlib). Set to 'auto' to run it only when the linked wasm carries +wasm-bindgen's marker section, which is how cargo/rustc opts in when driving +emcc as the linker; otherwise 'auto' is a no-op. .. note:: This is an experimental setting diff --git a/src/postamble.js b/src/postamble.js index a474aa57280f3..6c6b1cf5330c0 100644 --- a/src/postamble.js +++ b/src/postamble.js @@ -238,7 +238,15 @@ function checkUnflushedContent() { #endif // EXIT_RUNTIME #endif // ASSERTIONS +#if WASM_ESM_INTEGRATION && WASM_BINDGEN +// wasm-bindgen's glue reaches the wasm exports by name on an aggregate exports +// object, so provide it via a namespace import. Only under WASM_BINDGEN - +// plain ESM integration keeps per-symbol named imports so bundlers can +// tree-shake unused wasm exports. +import * as wasmExports from './{{{ WASM_BINARY_FILE }}}'; +#elif !WASM_ESM_INTEGRATION var wasmExports; +#endif #if SPLIT_MODULE var wasmRawExports; #endif diff --git a/src/settings.js b/src/settings.js index 3ce85156f714d..8b456f66acad8 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2223,7 +2223,12 @@ var LEGACY_RUNTIME = false; // [link] var SIGNATURE_CONVERSIONS = []; -// Run wasm-bindgen and integrate the rust-exported symbols into the rest of Emscripten's JS output. +// Run wasm-bindgen and integrate the rust-exported symbols into the rest of +// Emscripten's JS output. +// Set to 1 to always run wasm-bindgen (e.g. a C/C++ build linking a Rust +// staticlib). Set to 'auto' to run it only when the linked wasm carries +// wasm-bindgen's marker section, which is how cargo/rustc opts in when driving +// emcc as the linker; otherwise 'auto' is a no-op. // [link] // [experimental] var WASM_BINDGEN = 0; diff --git a/test/rust/bindgen_greeter/.cargo/config.toml b/test/rust/bindgen_greeter/.cargo/config.toml new file mode 100644 index 0000000000000..0a1071f986ec9 --- /dev/null +++ b/test/rust/bindgen_greeter/.cargo/config.toml @@ -0,0 +1,10 @@ +[build] +target = "wasm32-unknown-emscripten" +rustflags = [ + "-Cllvm-args=-enable-emscripten-cxx-exceptions=0", + "-Cpanic=abort", + "-Crelocation-model=static", + # rust's precompiled std requires emscripten's C++ runtime libraries, but + # rustc links via emcc rather than em++. + "-Clink-arg=-sDEFAULT_TO_CXX", +] diff --git a/test/rust/bindgen_greeter/Cargo.toml b/test/rust/bindgen_greeter/Cargo.toml new file mode 100644 index 0000000000000..966fd3b838570 --- /dev/null +++ b/test/rust/bindgen_greeter/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "bindgen_greeter" +edition = "2021" + +[[bin]] +name = "bindgen_greeter" +path = "src/main.rs" + +[dependencies] +# 0.2.127 is the first release with the emscripten __export/__force attribute +# support (wasm-bindgen/wasm-bindgen#5253); must match the wasm-bindgen-cli +# version exactly. +wasm-bindgen = "0.2.127" diff --git a/test/rust/bindgen_greeter/src/main.rs b/test/rust/bindgen_greeter/src/main.rs new file mode 100644 index 0000000000000..e4afb889b0c19 --- /dev/null +++ b/test/rust/bindgen_greeter/src/main.rs @@ -0,0 +1,23 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct Greeter { + greeting: String, +} + +#[wasm_bindgen] +impl Greeter { + #[wasm_bindgen(constructor)] + pub fn new(greeting: String) -> Greeter { + Greeter { greeting } + } + + pub fn greet(&self, name: String) -> String { + format!("{}, {}!", self.greeting, name) + } +} + +fn main() { + // Matches the emscripten idiom: main runs automatically on init. + println!("main ran"); +} diff --git a/test/test_other.py b/test/test_other.py index a4a2fedaee15c..b54fc8a8b70a9 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -272,6 +272,11 @@ def requires_rust(func): return requires_tool('cargo', 'RUST')(func) +def requires_wasm_bindgen(func): + assert callable(func) + return requires_tool('wasm-bindgen', 'WASM_BINDGEN')(func) + + def requires_pkg_config(func): assert callable(func) @@ -15324,20 +15329,85 @@ def test_rust_integration_basics(self): self.do_runf('main.cpp', 'Hello from rust!', cflags=[lib]) @requires_rust + @requires_wasm_bindgen def test_wasm_bindgen_integration(self): copytree(test_file('rust/bindgen_integration'), '.') - self.run_process(['cargo', 'add', 'wasm-bindgen']) + # Pin the library to the (managed) wasm-bindgen-cli version on PATH; + # wasm-bindgen requires the CLI and the library to match exactly. + self.run_process(['cargo', 'add', 'wasm-bindgen@0.2.127']) self.run_process(['cargo', 'build']) lib = 'target/wasm32-unknown-emscripten/debug/libbindgen_integration.a' self.assertExists(lib) - create_file('empty.c', '') + # A hand-written EMSCRIPTEN_KEEPALIVE C export must remain surfaced + # alongside wasm-bindgen's self-registered API; the wasm-bindgen glue + # suppression must not drop it. + create_file('native.c', ''' + #include + EMSCRIPTEN_KEEPALIVE int em_double(int x) { return x * 2; } + ''') create_file('post.js', ''' - Module.onRuntimeInitialized = () => out(Module.rs_add(17, 25)); + Module.onRuntimeInitialized = () => { + out('rs_add=' + Module.rs_add(17, 25)); + out('em_double=' + Module._em_double(20)); + }; ''') - self.run_process(['cargo', 'install', 'wasm-bindgen-cli']) - self.do_runf('empty.c', '42', cflags=[lib, '-sWASM_BINDGEN', '-Wno-experimental', '--post-js=post.js', '-lexports.js']) + output = self.do_runf('native.c', cflags=[lib, '-sWASM_BINDGEN', '-Wno-experimental', '--post-js=post.js', '-lexports.js']) + self.assertContained('rs_add=42', output) + self.assertContained('em_double=40', output) + + # ESM-integration and factory (MODULARIZE) surface the clean wasm-bindgen API + # differently (named ESM exports vs `Module.`). Both must expose exactly + # the `Greeter` class and none of the raw wasm exports rustc lists. + @requires_rust + @requires_wasm_bindgen + @parameterized({ + 'esm': (['-sWASM_ESM_INTEGRATION'], ''' + import init, * as mod from './bindgen_greeter.js'; + await init(); + '''), + 'factory': (['-sMODULARIZE', '-sEXPORT_ES6'], ''' + import Module from './bindgen_greeter.js'; + const mod = await Module(); + '''), + }) + def test_wasm_bindgen_rustc_driven(self, cflags, prelude): + # cargo/rustc links via emcc; pass -sWASM_BINDGEN=auto (plus the output-mode + # settings) through so emcc detects wasm-bindgen's marker section in the + # linked wasm and runs wasm-bindgen as a post-link step. + copytree(test_file('rust/bindgen_greeter'), '.') + # rustc invokes emcc as the linker; ensure it uses *this* emcc and pass the + # link settings through. + with env_modify({'CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_LINKER': EMCC, + 'EMCC_CFLAGS': ' '.join(['-sWASM_BINDGEN=auto', '-Wno-experimental'] + cflags)}): + self.run_process(['cargo', 'build']) + + # cargo copies only the .js and .wasm; the ESM support module and snippets + # stay in deps/, so run from there. + out_dir = 'target/wasm32-unknown-emscripten/debug/deps' + create_file(os.path.join(out_dir, 'run.mjs'), prelude + ''' + const greeting = new mod.Greeter('Hello').greet('world'); + if (greeting !== 'Hello, world!') throw new Error('unexpected greeting: ' + greeting); + // None of the raw wasm exports leak into the user-facing API. + for (const name of ['_main', 'greeter_greet', '_greeter_greet', + '__wbindgen_malloc', '___wbindgen_malloc']) { + if (mod[name] !== undefined) throw new Error('leaked export: ' + name); + } + console.log(greeting); + ''') + self.node_args += ['--experimental-wasm-modules', '--no-warnings'] + output = self.run_js(os.path.join(out_dir, 'run.mjs')) + self.assertContained('Hello, world!', output) + # `main` runs automatically on init (matching the emscripten C++ idiom), + # even though `_main` is not surfaced as a user-facing export. + self.assertContained('main ran', output) + + def test_wasm_bindgen_auto_no_marker(self): + # -sWASM_BINDGEN=auto is a no-op for an ordinary build with no wasm-bindgen + # marker section: wasm-bindgen is never invoked (so it need not be installed) + # and the program builds and runs normally. + self.do_runf('hello_world.c', 'Hello, world!', cflags=['-sWASM_BINDGEN=auto', '-Wno-experimental']) @requires_rust @requires_dev_dependency('typescript') diff --git a/tools/building.py b/tools/building.py index 684146752d89d..ca609ddf82b35 100644 --- a/tools/building.py +++ b/tools/building.py @@ -27,7 +27,7 @@ utils, webassembly, ) -from .settings import settings +from .settings import settings, user_settings from .shared import ( CLANG_CC, CLANG_CXX, @@ -62,6 +62,10 @@ user_requested_exports: set[str] = set() # JS library symbols exported via the `__export` decorator. extra_js_exports: set[str] = set() +# The raw wasm exports wasm-bindgen's generated bindings reach by name (the +# supplied/expansion glue), mangled. These are suppressed from the public +# surface; EMSCRIPTEN_KEEPALIVE exports are not in this set and remain. +wasm_bindgen_internal_exports: set[str] = set() # A list of feature flags to pass to each binaryen invocation (like `wasm-opt`, # etc.). This is received by the first call to binaryen (e.g. `wasm-emscripten-finalize`) # which reads it using `--detect-features`. @@ -313,7 +317,12 @@ def lld_flags(args, linker_inputs=None): # grouping. args = [a for a in args if a not in {'--start-group', '--end-group'}] - if settings.WASM_BINDGEN: + # Retain the wasm exports wasm-bindgen's glue reaches by name. This is the + # emcc-driven staticlib flow (explicit -sWASM_BINDGEN), where nobody else + # computed the export set, so we discover it here. The cargo/rustc-driven flow + # (-sWASM_BINDGEN=auto, still unresolved at link time) supplies EXPORTED_FUNCTIONS + # itself and never needs this. + if settings.WASM_BINDGEN == 1 and 'EXPORTED_FUNCTIONS' not in user_settings: exported_symbols = get_wasm_bindgen_exported_symbols(linker_inputs) args.extend(f'--export={e}' for e in exported_symbols) @@ -1319,6 +1328,14 @@ def run_wasm_opt(infile, outfile=None, args=[], **kwargs): # ruff: ignore[mutab return run_binaryen_command('wasm-opt', infile, outfile, args=args, **kwargs) +def is_wasm_bindgen_module(wasm_file): + # wasm-bindgen marks modules built for the emscripten target with this custom + # section so emcc, when used as the linker (e.g. by cargo/rustc), can detect + # under `-sWASM_BINDGEN=auto` that wasm-bindgen needs to run as a post-link step. + with webassembly.Module(wasm_file) as module: + return module.get_custom_section('__wasm_bindgen_emscripten_marker') is not None + + def run_wasm_bindgen(infile): bindgen_out_dir = os.path.join(get_emscripten_temp_dir(), 'bindgen_out') @@ -1333,16 +1350,36 @@ def run_wasm_bindgen(infile): '--out-dir', bindgen_out_dir, ] + exports_before = {e.name for e in webassembly.get_exports(infile)} + check_call(cmd) # Don't try to predict the .wasm filename that wasm-bindgen outputs. Instead # just grab the .wasm file itself. all_output_files = os.listdir(bindgen_out_dir) new_wasm_file = [x for x in all_output_files if x.endswith('.wasm')][0] - - shutil.copyfile(os.path.join(bindgen_out_dir, new_wasm_file), infile) - - return os.path.join(bindgen_out_dir, 'library_bindgen.js') + new_wasm_path = os.path.join(bindgen_out_dir, new_wasm_file) + + exports_after = {e.name for e in webassembly.get_exports(new_wasm_path)} + # Report which placeholder exports wasm-bindgen consumed so the caller can + # drop them from EXPORTED_FUNCTIONS, and which exports its expansion added so + # the caller can keep them off the public surface. + removed_exports = exports_before - exports_after + added_exports = exports_after - exports_before + + shutil.copyfile(new_wasm_path, infile) + + # wasm-bindgen emits imported JS snippets into `snippets/` and the `import` + # statements referencing them into `library_bindgen.extern-pre.js`, only when + # the crate actually imports JS. + extern_pre_js = os.path.join(bindgen_out_dir, 'library_bindgen.extern-pre.js') + if not os.path.exists(extern_pre_js): + extern_pre_js = None + snippets_dir = os.path.join(bindgen_out_dir, 'snippets') + if not os.path.isdir(snippets_dir): + snippets_dir = None + + return os.path.join(bindgen_out_dir, 'library_bindgen.js'), removed_exports, added_exports, extern_pre_js, snippets_dir intermediate_counter = 0 diff --git a/tools/emscripten.py b/tools/emscripten.py index a9f9f878e72de..bc2db307fcd52 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -625,7 +625,12 @@ def finalize_wasm(infile, outfile, js_syms): unexpected_exports = [e for e in unexpected_exports if e not in expected_exports] if (not settings.STANDALONE_WASM and 'main' in metadata.all_exports) or '__main_argc_argv' in metadata.all_exports: - if 'EXPORTED_FUNCTIONS' in user_settings and '_main' not in settings.USER_EXPORTS: + if settings.WASM_BINDGEN: + # Under WASM_BINDGEN, `main` stays a wasm export so it runs automatically + # on init, but `_main` is internal (part of wasm-bindgen's internal export + # set, see below) and is not surfaced as a public export. + pass + elif 'EXPORTED_FUNCTIONS' in user_settings and '_main' not in settings.USER_EXPORTS: # If `_main` was unexpectedly exported we assume it was added to # EXPORT_IF_DEFINED by `phase_linker_setup` in order that we can detect # it and report this warning. After reporting the warning we explicitly @@ -640,6 +645,13 @@ def finalize_wasm(infile, outfile, js_syms): else: unexpected_exports.append('_main') + # Keep wasm-bindgen's internal glue exports (the raw symbols its generated + # bindings reach by name, including `_main`) off the public surface. Genuine + # EMSCRIPTEN_KEEPALIVE exports are not in this set and remain. + if settings.WASM_BINDGEN: + unexpected_exports = [e for e in unexpected_exports + if e not in building.wasm_bindgen_internal_exports] + building.user_requested_exports.update(unexpected_exports) settings.EXPORTED_FUNCTIONS.extend(unexpected_exports) diff --git a/tools/link.py b/tools/link.py index 5e97948b4be4e..3dd3ea3bee1b8 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1917,9 +1917,38 @@ def phase_post_link(in_wasm, wasm_target, target, js_syms, base_metadata=None): settings.TARGET_JS_NAME = os.path.basename(js_target) + # -sWASM_BINDGEN=auto runs wasm-bindgen only when the linked wasm carries + # wasm-bindgen's marker section (cargo/rustc linking via emcc); otherwise it is + # a no-op. This lets rustc opt in without emcc guessing for ordinary builds. + if settings.WASM_BINDGEN == 'auto': + settings.WASM_BINDGEN = 1 if building.is_wasm_bindgen_module(in_wasm) else 0 + if settings.WASM_BINDGEN: - bindgen_jslib = building.run_wasm_bindgen(in_wasm) + bindgen_jslib, removed_exports, added_exports, extern_pre_js, snippets_dir = building.run_wasm_bindgen(in_wasm) settings.JS_LIBRARIES.append(bindgen_jslib) + # 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 added) are internal, not a user-facing + # API: wasm-bindgen self-registers the real API via its JS library. Capture + # that set so it can be 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. A genuine + # EMSCRIPTEN_KEEPALIVE C/C++ export is not in this set and is still surfaced. + removed = {shared.asmjs_mangle(e) for e in removed_exports} + building.wasm_bindgen_internal_exports = ( + set(settings.USER_EXPORTS) | {shared.asmjs_mangle(e) for e in added_exports}) + # Also drop the placeholder symbols wasm-bindgen consumed (__wbindgen_describe*, + # __externref_*, ...) so they aren't reported as undefined exports. + drop = removed | building.wasm_bindgen_internal_exports + settings.EXPORTED_FUNCTIONS = [e for e in settings.EXPORTED_FUNCTIONS if e not in drop] + settings.USER_EXPORTS = [] + building.user_requested_exports.clear() + # Imported JS: emit wasm-bindgen's `import` statements as extern-pre-js and + # place the snippet files alongside the output so relative imports resolve. + if extern_pre_js: + options.extern_pre_js.append(extern_pre_js) + if snippets_dir: + shutil.copytree(snippets_dir, os.path.join(os.path.dirname(js_target), 'snippets'), dirs_exist_ok=True) metadata = phase_emscript(in_wasm, wasm_target, js_syms, base_metadata) diff --git a/tools/settings.py b/tools/settings.py index 699e65615d436..b227f8516e455 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -415,7 +415,7 @@ def __setattr__(self, name, value): def check_type(self, name, value): # These settings have a variable type so cannot be easily type checked. - if name in {'EXECUTABLE', 'SUPPORT_LONGJMP', 'PTHREAD_POOL_SIZE', 'SEPARATE_DWARF', 'LTO', 'MODULARIZE'}: + if name in {'EXECUTABLE', 'SUPPORT_LONGJMP', 'PTHREAD_POOL_SIZE', 'SEPARATE_DWARF', 'LTO', 'MODULARIZE', 'WASM_BINDGEN'}: return expected_type = self.types.get(name) if not expected_type: From b902aee411b5f2bc0efb790db615b76904990812 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Thu, 3 Sep 2026 14:24:32 -0700 Subject: [PATCH 02/14] Address review feedback - Drop the greeter test's .cargo/config.toml: -sDEFAULT_TO_CXX and the exceptions/panic flags are no longer needed since #27496, and the link args are now passed via -Clink-arg rustflags in a config written by the test instead of through EMCC_CFLAGS. - Only pass --experimental-wasm-modules to node < 25. - Rename the test parameterizations to esm_integration/es6. - Clarify in the WASM_BINDGEN setting docs when the linked wasm carries the marker section and why the staticlib flow still needs =1. - Merge the wasm-bindgen comments in finalize_wasm. - Note in postamble.js that the wasmExports namespace import is only needed for wasm-bindgen 0.2.127 and can go once the minimum version includes wasm-bindgen/wasm-bindgen#5270. --- .../tools_reference/settings_reference.rst | 13 ++++--- src/postamble.js | 11 +++--- src/settings.js | 13 ++++--- test/rust/bindgen_greeter/.cargo/config.toml | 10 ------ test/test_other.py | 34 ++++++++++++------- tools/emscripten.py | 13 +++---- 6 files changed, 54 insertions(+), 40 deletions(-) delete mode 100644 test/rust/bindgen_greeter/.cargo/config.toml diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 9e1089bbd2d14..21a7edbcb5521 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3354,10 +3354,15 @@ WASM_BINDGEN Run wasm-bindgen and integrate the rust-exported symbols into the rest of Emscripten's JS output. -Set to 1 to always run wasm-bindgen (e.g. a C/C++ build linking a Rust -staticlib). Set to 'auto' to run it only when the linked wasm carries -wasm-bindgen's marker section, which is how cargo/rustc opts in when driving -emcc as the linker; otherwise 'auto' is a no-op. +Set to 1 to always run wasm-bindgen. This is required when a C/C++ build +links a Rust staticlib: nothing references wasm-bindgen's objects in the +archive, so emcc must force-export them for wasm-bindgen to find. +Set to 'auto' to run wasm-bindgen only when the linked wasm carries the +custom section wasm-bindgen emits for this target. That is the case when +cargo/rustc drives the link using emcc as the linker (rustc exports +wasm-bindgen's symbols itself), so 'auto' can be passed unconditionally via +`-Clink-arg`; for any other link it is a no-op and wasm-bindgen need not be +installed. .. note:: This is an experimental setting diff --git a/src/postamble.js b/src/postamble.js index 6c6b1cf5330c0..9e71309759bc6 100644 --- a/src/postamble.js +++ b/src/postamble.js @@ -239,10 +239,13 @@ function checkUnflushedContent() { #endif // ASSERTIONS #if WASM_ESM_INTEGRATION && WASM_BINDGEN -// wasm-bindgen's glue reaches the wasm exports by name on an aggregate exports -// object, so provide it via a namespace import. Only under WASM_BINDGEN - -// plain ESM integration keeps per-symbol named imports so bundlers can -// tree-shake unused wasm exports. +// wasm-bindgen 0.2.127's glue reaches the wasm exports by name on an aggregate +// exports object, so provide it via a namespace import. Only under +// WASM_BINDGEN - plain ESM integration keeps per-symbol named imports so +// bundlers can tree-shake unused wasm exports. +// TODO: Remove once the minimum wasm-bindgen version references the per-export +// receiving bindings instead (wasm-bindgen/wasm-bindgen#5270, unreleased), +// which no longer needs this. import * as wasmExports from './{{{ WASM_BINARY_FILE }}}'; #elif !WASM_ESM_INTEGRATION var wasmExports; diff --git a/src/settings.js b/src/settings.js index 8b456f66acad8..e6a1682e05ff6 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2225,10 +2225,15 @@ var SIGNATURE_CONVERSIONS = []; // Run wasm-bindgen and integrate the rust-exported symbols into the rest of // Emscripten's JS output. -// Set to 1 to always run wasm-bindgen (e.g. a C/C++ build linking a Rust -// staticlib). Set to 'auto' to run it only when the linked wasm carries -// wasm-bindgen's marker section, which is how cargo/rustc opts in when driving -// emcc as the linker; otherwise 'auto' is a no-op. +// Set to 1 to always run wasm-bindgen. This is required when a C/C++ build +// links a Rust staticlib: nothing references wasm-bindgen's objects in the +// archive, so emcc must force-export them for wasm-bindgen to find. +// Set to 'auto' to run wasm-bindgen only when the linked wasm carries the +// custom section wasm-bindgen emits for this target. That is the case when +// cargo/rustc drives the link using emcc as the linker (rustc exports +// wasm-bindgen's symbols itself), so 'auto' can be passed unconditionally via +// `-Clink-arg`; for any other link it is a no-op and wasm-bindgen need not be +// installed. // [link] // [experimental] var WASM_BINDGEN = 0; diff --git a/test/rust/bindgen_greeter/.cargo/config.toml b/test/rust/bindgen_greeter/.cargo/config.toml deleted file mode 100644 index 0a1071f986ec9..0000000000000 --- a/test/rust/bindgen_greeter/.cargo/config.toml +++ /dev/null @@ -1,10 +0,0 @@ -[build] -target = "wasm32-unknown-emscripten" -rustflags = [ - "-Cllvm-args=-enable-emscripten-cxx-exceptions=0", - "-Cpanic=abort", - "-Crelocation-model=static", - # rust's precompiled std requires emscripten's C++ runtime libraries, but - # rustc links via emcc rather than em++. - "-Clink-arg=-sDEFAULT_TO_CXX", -] diff --git a/test/test_other.py b/test/test_other.py index b54fc8a8b70a9..aca08a6155f95 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15357,31 +15357,38 @@ def test_wasm_bindgen_integration(self): self.assertContained('rs_add=42', output) self.assertContained('em_double=40', output) - # ESM-integration and factory (MODULARIZE) surface the clean wasm-bindgen API + # ESM integration and ES6 MODULARIZE surface the clean wasm-bindgen API # differently (named ESM exports vs `Module.`). Both must expose exactly # the `Greeter` class and none of the raw wasm exports rustc lists. @requires_rust @requires_wasm_bindgen @parameterized({ - 'esm': (['-sWASM_ESM_INTEGRATION'], ''' + 'esm_integration': (['-sWASM_ESM_INTEGRATION'], ''' import init, * as mod from './bindgen_greeter.js'; await init(); '''), - 'factory': (['-sMODULARIZE', '-sEXPORT_ES6'], ''' + 'es6': (['-sMODULARIZE', '-sEXPORT_ES6'], ''' import Module from './bindgen_greeter.js'; const mod = await Module(); '''), }) - def test_wasm_bindgen_rustc_driven(self, cflags, prelude): + def test_wasm_bindgen_rustc_driven(self, ldflags, prelude): # cargo/rustc links via emcc; pass -sWASM_BINDGEN=auto (plus the output-mode - # settings) through so emcc detects wasm-bindgen's marker section in the - # linked wasm and runs wasm-bindgen as a post-link step. + # settings) through as link args so emcc detects wasm-bindgen's marker + # section in the linked wasm and runs wasm-bindgen as a post-link step. copytree(test_file('rust/bindgen_greeter'), '.') - # rustc invokes emcc as the linker; ensure it uses *this* emcc and pass the - # link settings through. - with env_modify({'CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_LINKER': EMCC, - 'EMCC_CFLAGS': ' '.join(['-sWASM_BINDGEN=auto', '-Wno-experimental'] + cflags)}): - self.run_process(['cargo', 'build']) + link_args = ['-sWASM_BINDGEN=auto', '-Wno-experimental'] + ldflags + rustflags = ', '.join(f'"-Clink-arg={a}"' for a in link_args) + ensure_dir('.cargo') + create_file('.cargo/config.toml', f''' + [build] + target = "wasm32-unknown-emscripten" + rustflags = [{rustflags}] + + [target.wasm32-unknown-emscripten] + linker = "{EMCC}" + ''') + self.run_process(['cargo', 'build']) # cargo copies only the .js and .wasm; the ESM support module and snippets # stay in deps/, so run from there. @@ -15396,7 +15403,10 @@ def test_wasm_bindgen_rustc_driven(self, cflags, prelude): } console.log(greeting); ''') - self.node_args += ['--experimental-wasm-modules', '--no-warnings'] + # Importing wasm modules is stable from node 25. + if not self.try_require_node_version(25): + self.node_args += ['--experimental-wasm-modules'] + self.node_args += ['--no-warnings'] output = self.run_js(os.path.join(out_dir, 'run.mjs')) self.assertContained('Hello, world!', output) # `main` runs automatically on init (matching the emscripten C++ idiom), diff --git a/tools/emscripten.py b/tools/emscripten.py index bc2db307fcd52..a477524e229b5 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -626,9 +626,9 @@ def finalize_wasm(infile, outfile, js_syms): if (not settings.STANDALONE_WASM and 'main' in metadata.all_exports) or '__main_argc_argv' in metadata.all_exports: if settings.WASM_BINDGEN: - # Under WASM_BINDGEN, `main` stays a wasm export so it runs automatically - # on init, but `_main` is internal (part of wasm-bindgen's internal export - # set, see below) and is not surfaced as a public export. + # rustc drives the link with an EXPORTED_FUNCTIONS list that never + # includes `_main`, so skip the unused-main handling: `main` stays a wasm + # export and runs on init. It is kept off the public surface below. pass elif 'EXPORTED_FUNCTIONS' in user_settings and '_main' not in settings.USER_EXPORTS: # If `_main` was unexpectedly exported we assume it was added to @@ -645,9 +645,10 @@ def finalize_wasm(infile, outfile, js_syms): else: unexpected_exports.append('_main') - # Keep wasm-bindgen's internal glue exports (the raw symbols its generated - # bindings reach by name, including `_main`) off the public surface. Genuine - # EMSCRIPTEN_KEEPALIVE exports are not in this set and remain. + # Under WASM_BINDGEN, keep wasm-bindgen's internal glue exports (the raw + # symbols its generated bindings reach by name, and `_main`) off the public + # surface; wasm-bindgen surfaces the user-facing API itself via its JS + # library. Genuine EMSCRIPTEN_KEEPALIVE exports are not in this set and remain. if settings.WASM_BINDGEN: unexpected_exports = [e for e in unexpected_exports if e not in building.wasm_bindgen_internal_exports] From 733d0b1c53645e5f291e8895c04b42b80388efde Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Thu, 3 Sep 2026 14:46:27 -0700 Subject: [PATCH 03/14] Resolve WASM_BINDGEN mode and export detection after the initial link The wasm-bindgen export detection previously lived inside lld_flags, gated on WASM_BINDGEN == 1 because 'auto' cannot be resolved until a linked module exists. Restructure so that the two concerns are decided in phase_link, on the initial link's output: - WASM_BINDGEN=auto is resolved against the linked wasm's marker section (moved here from phase_post_link). - Then, whenever wasm-bindgen mode is on and the link driver did not supply EXPORTED_FUNCTIONS, the exports wasm-bindgen reaches by name are discovered from the linker inputs and the module is re-linked to retain them. They are passed straight to the linker so they are not mistaken for user-requested exports. This drops the linker_inputs plumbing through link_lld/lld_flags and the WASM_BINDGEN special case in lld_flags. --- .../tools_reference/settings_reference.rst | 19 ++++++------ src/settings.js | 19 ++++++------ tools/building.py | 17 +++-------- tools/link.py | 29 +++++++++++++------ 4 files changed, 44 insertions(+), 40 deletions(-) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 21a7edbcb5521..d4d4c823776d7 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3354,15 +3354,16 @@ WASM_BINDGEN Run wasm-bindgen and integrate the rust-exported symbols into the rest of Emscripten's JS output. -Set to 1 to always run wasm-bindgen. This is required when a C/C++ build -links a Rust staticlib: nothing references wasm-bindgen's objects in the -archive, so emcc must force-export them for wasm-bindgen to find. -Set to 'auto' to run wasm-bindgen only when the linked wasm carries the -custom section wasm-bindgen emits for this target. That is the case when -cargo/rustc drives the link using emcc as the linker (rustc exports -wasm-bindgen's symbols itself), so 'auto' can be passed unconditionally via -`-Clink-arg`; for any other link it is a no-op and wasm-bindgen need not be -installed. +Set to 1 to always run wasm-bindgen, or to 'auto' to run it only when the +linked wasm carries the custom section wasm-bindgen emits for this target, +which makes it a no-op for any other link (so wasm-bindgen need not be +installed). cargo/rustc builds that use emcc as the linker can pass 'auto' +unconditionally via `-Clink-arg`. When a C/C++ build links a Rust +staticlib, nothing guarantees the object carrying that section is pulled +into the link, so use 1 there. +Unless the link driver supplies EXPORTED_FUNCTIONS (as rustc does when +driving the link), the wasm exports wasm-bindgen reaches by name are +discovered from the linker inputs and the module is re-linked to retain them. .. note:: This is an experimental setting diff --git a/src/settings.js b/src/settings.js index e6a1682e05ff6..21eec607f2c40 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2225,15 +2225,16 @@ var SIGNATURE_CONVERSIONS = []; // Run wasm-bindgen and integrate the rust-exported symbols into the rest of // Emscripten's JS output. -// Set to 1 to always run wasm-bindgen. This is required when a C/C++ build -// links a Rust staticlib: nothing references wasm-bindgen's objects in the -// archive, so emcc must force-export them for wasm-bindgen to find. -// Set to 'auto' to run wasm-bindgen only when the linked wasm carries the -// custom section wasm-bindgen emits for this target. That is the case when -// cargo/rustc drives the link using emcc as the linker (rustc exports -// wasm-bindgen's symbols itself), so 'auto' can be passed unconditionally via -// `-Clink-arg`; for any other link it is a no-op and wasm-bindgen need not be -// installed. +// Set to 1 to always run wasm-bindgen, or to 'auto' to run it only when the +// linked wasm carries the custom section wasm-bindgen emits for this target, +// which makes it a no-op for any other link (so wasm-bindgen need not be +// installed). cargo/rustc builds that use emcc as the linker can pass 'auto' +// unconditionally via `-Clink-arg`. When a C/C++ build links a Rust +// staticlib, nothing guarantees the object carrying that section is pulled +// into the link, so use 1 there. +// Unless the link driver supplies EXPORTED_FUNCTIONS (as rustc does when +// driving the link), the wasm exports wasm-bindgen reaches by name are +// discovered from the linker inputs and the module is re-linked to retain them. // [link] // [experimental] var WASM_BINDGEN = 0; diff --git a/tools/building.py b/tools/building.py index ca609ddf82b35..6df98a8c06379 100644 --- a/tools/building.py +++ b/tools/building.py @@ -27,7 +27,7 @@ utils, webassembly, ) -from .settings import settings, user_settings +from .settings import settings from .shared import ( CLANG_CC, CLANG_CXX, @@ -311,21 +311,12 @@ def get_wasm_bindgen_exported_symbols(input_files): return symbols -def lld_flags(args, linker_inputs=None): +def lld_flags(args): # lld doesn't currently support --start-group/--end-group since the # semantics are more like the windows linker where there is no need for # grouping. args = [a for a in args if a not in {'--start-group', '--end-group'}] - # Retain the wasm exports wasm-bindgen's glue reaches by name. This is the - # emcc-driven staticlib flow (explicit -sWASM_BINDGEN), where nobody else - # computed the export set, so we discover it here. The cargo/rustc-driven flow - # (-sWASM_BINDGEN=auto, still unresolved at link time) supplies EXPORTED_FUNCTIONS - # itself and never needs this. - if settings.WASM_BINDGEN == 1 and 'EXPORTED_FUNCTIONS' not in user_settings: - exported_symbols = get_wasm_bindgen_exported_symbols(linker_inputs) - args.extend(f'--export={e}' for e in exported_symbols) - # Emscripten currently expects linkable output (SIDE_MODULE/MAIN_MODULE) to # include all archive contents. if settings.LINKABLE and (settings.FAKE_DYLIBS or not settings.SIDE_MODULE): @@ -354,7 +345,7 @@ def lld_flags(args, linker_inputs=None): return args -def link_lld(args, target, external_symbols=None, linker_inputs=None): +def link_lld(args, target, external_symbols=None): # runs lld to link things. if not os.path.exists(WASM_LD): exit_with_error('linker binary not found in LLVM directory: %s', WASM_LD) @@ -363,7 +354,7 @@ def link_lld(args, target, external_symbols=None, linker_inputs=None): # normal linker flags that are used when building and executable if '--relocatable' not in args and '-r' not in args: cmd += lld_flags_for_executable(external_symbols) - cmd += lld_flags(args, linker_inputs) + cmd += lld_flags(args) cmd = get_command_with_possible_response_file(cmd) if settings.LINK_AS_CXX: check_call(cmd) diff --git a/tools/link.py b/tools/link.py index 3dd3ea3bee1b8..66fedb39f3e0c 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1890,12 +1890,29 @@ def phase_link(linker_args, linker_inputs, wasm_target, js_syms): # TODO(sbc): Remove this double execution of wasm-ld if we ever find a way to # distinguish EMSCRIPTEN_KEEPALIVE exports from `--export-dynamic` exports. settings.LINKABLE = False - building.link_lld(linker_args, wasm_target, external_symbols=js_syms, - linker_inputs=linker_inputs) + building.link_lld(linker_args, wasm_target, external_symbols=js_syms) settings.LINKABLE = True rtn = extract_metadata.extract_metadata(wasm_target) - building.link_lld(linker_args, wasm_target, external_symbols=js_syms, linker_inputs=linker_inputs) + building.link_lld(linker_args, wasm_target, external_symbols=js_syms) + + # -sWASM_BINDGEN=auto runs wasm-bindgen only when the linked wasm carries + # wasm-bindgen's marker section (e.g. cargo/rustc linking via emcc); + # otherwise it is a no-op. This lets rustc opt in without emcc guessing for + # ordinary builds. + if settings.WASM_BINDGEN == 'auto': + settings.WASM_BINDGEN = 1 if building.is_wasm_bindgen_module(wasm_target) else 0 + + # wasm-bindgen reaches its exports by name, so they all have to be retained. + # When the link driver supplied EXPORTED_FUNCTIONS (rustc lists them all + # when driving the link) that is authoritative. Otherwise (e.g. a C/C++ build + # linking a Rust staticlib) discover them from the linker inputs and re-link. + # They are passed straight to the linker rather than via EXPORTED_FUNCTIONS + # so they are not mistaken for user-requested exports. + if settings.WASM_BINDGEN and 'EXPORTED_FUNCTIONS' not in user_settings: + exports = building.get_wasm_bindgen_exported_symbols(linker_inputs) + building.link_lld(linker_args + [f'--export={e}' for e in exports], wasm_target, external_symbols=js_syms) + return rtn @@ -1917,12 +1934,6 @@ def phase_post_link(in_wasm, wasm_target, target, js_syms, base_metadata=None): settings.TARGET_JS_NAME = os.path.basename(js_target) - # -sWASM_BINDGEN=auto runs wasm-bindgen only when the linked wasm carries - # wasm-bindgen's marker section (cargo/rustc linking via emcc); otherwise it is - # a no-op. This lets rustc opt in without emcc guessing for ordinary builds. - if settings.WASM_BINDGEN == 'auto': - settings.WASM_BINDGEN = 1 if building.is_wasm_bindgen_module(in_wasm) else 0 - if settings.WASM_BINDGEN: bindgen_jslib, removed_exports, added_exports, extern_pre_js, snippets_dir = building.run_wasm_bindgen(in_wasm) settings.JS_LIBRARIES.append(bindgen_jslib) From 012105e36a38c225d70d539d4217412c5dfdae48 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Thu, 3 Sep 2026 16:15:24 -0700 Subject: [PATCH 04/14] Simplify wasm-bindgen export handling - Keep USER_EXPORTS truthful after the wasm-bindgen step (only drop the placeholder exports it consumed) instead of clearing it, so the existing unused-main handling in finalize_wasm applies as-is and the WASM_BINDGEN special case there can go. - Validate WASM_BINDGEN values. - Install the prebuilt wasm-bindgen CLI in CI instead of building it. - Trim comments. --- .circleci/config.yml | 12 +++---- .../tools_reference/settings_reference.rst | 7 ++-- src/postamble.js | 12 +++---- src/settings.js | 7 ++-- tools/building.py | 14 ++------ tools/emscripten.py | 11 +----- tools/link.py | 36 +++++++------------ 7 files changed, 33 insertions(+), 66 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b124315e45efc..1243a7372f1b4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -77,18 +77,16 @@ commands: steps: - run: name: install rust and wasm-bindgen - # rust and wasm-bindgen are always installed together so there is no - # CI environment with one but not the other. The wasm-bindgen-cli - # version is pinned to match the library the test crates depend on; - # wasm-bindgen requires the CLI and the library to be the exact same - # version. 0.2.127 is the first release with the emscripten - # __export/__force attribute support (wasm-bindgen/wasm-bindgen#5253). + # The wasm-bindgen CLI must exactly match the library version the + # test crates pin. command: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y export PATH=${HOME}/.cargo/bin:${PATH} rustup target add wasm32-unknown-emscripten echo "export PATH=\"\$HOME/.cargo/bin:\$PATH\"" >> $BASH_ENV - cargo install wasm-bindgen-cli --version 0.2.127 --locked + WB=wasm-bindgen-0.2.127-x86_64-unknown-linux-musl + curl -sSfL https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.127/$WB.tar.gz | tar xz -C /tmp + mv /tmp/$WB/wasm-bindgen ${HOME}/.cargo/bin/ install-node-version: description: "install a specific version of node" parameters: diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index d4d4c823776d7..5a4597f490971 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3361,9 +3361,10 @@ installed). cargo/rustc builds that use emcc as the linker can pass 'auto' unconditionally via `-Clink-arg`. When a C/C++ build links a Rust staticlib, nothing guarantees the object carrying that section is pulled into the link, so use 1 there. -Unless the link driver supplies EXPORTED_FUNCTIONS (as rustc does when -driving the link), the wasm exports wasm-bindgen reaches by name are -discovered from the linker inputs and the module is re-linked to retain them. +If EXPORTED_FUNCTIONS is set it is taken as the complete export list and +must include every export wasm-bindgen reaches by name (rustc supplies this +when driving the link). Otherwise those exports are discovered from the +linker inputs and the module is re-linked to retain them. .. note:: This is an experimental setting diff --git a/src/postamble.js b/src/postamble.js index 9e71309759bc6..54ab9bbe3997c 100644 --- a/src/postamble.js +++ b/src/postamble.js @@ -239,15 +239,11 @@ function checkUnflushedContent() { #endif // ASSERTIONS #if WASM_ESM_INTEGRATION && WASM_BINDGEN -// wasm-bindgen 0.2.127's glue reaches the wasm exports by name on an aggregate -// exports object, so provide it via a namespace import. Only under -// WASM_BINDGEN - plain ESM integration keeps per-symbol named imports so -// bundlers can tree-shake unused wasm exports. -// TODO: Remove once the minimum wasm-bindgen version references the per-export -// receiving bindings instead (wasm-bindgen/wasm-bindgen#5270, unreleased), -// which no longer needs this. +// wasm-bindgen's glue reaches the wasm exports by name on an aggregate object. +// TODO: Remove once the minimum wasm-bindgen version uses the per-export +// receiving bindings instead (wasm-bindgen/wasm-bindgen#5270). import * as wasmExports from './{{{ WASM_BINARY_FILE }}}'; -#elif !WASM_ESM_INTEGRATION +#else var wasmExports; #endif #if SPLIT_MODULE diff --git a/src/settings.js b/src/settings.js index 21eec607f2c40..e7c72737588ea 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2232,9 +2232,10 @@ var SIGNATURE_CONVERSIONS = []; // unconditionally via `-Clink-arg`. When a C/C++ build links a Rust // staticlib, nothing guarantees the object carrying that section is pulled // into the link, so use 1 there. -// Unless the link driver supplies EXPORTED_FUNCTIONS (as rustc does when -// driving the link), the wasm exports wasm-bindgen reaches by name are -// discovered from the linker inputs and the module is re-linked to retain them. +// If EXPORTED_FUNCTIONS is set it is taken as the complete export list and +// must include every export wasm-bindgen reaches by name (rustc supplies this +// when driving the link). Otherwise those exports are discovered from the +// linker inputs and the module is re-linked to retain them. // [link] // [experimental] var WASM_BINDGEN = 0; diff --git a/tools/building.py b/tools/building.py index 6df98a8c06379..a917e62135311 100644 --- a/tools/building.py +++ b/tools/building.py @@ -62,9 +62,7 @@ user_requested_exports: set[str] = set() # JS library symbols exported via the `__export` decorator. extra_js_exports: set[str] = set() -# The raw wasm exports wasm-bindgen's generated bindings reach by name (the -# supplied/expansion glue), mangled. These are suppressed from the public -# surface; EMSCRIPTEN_KEEPALIVE exports are not in this set and remain. +# Mangled wasm exports wasm-bindgen's glue reaches by name, kept off the public surface. wasm_bindgen_internal_exports: set[str] = set() # A list of feature flags to pass to each binaryen invocation (like `wasm-opt`, # etc.). This is received by the first call to binaryen (e.g. `wasm-emscripten-finalize`) @@ -1320,9 +1318,6 @@ def run_wasm_opt(infile, outfile=None, args=[], **kwargs): # ruff: ignore[mutab def is_wasm_bindgen_module(wasm_file): - # wasm-bindgen marks modules built for the emscripten target with this custom - # section so emcc, when used as the linker (e.g. by cargo/rustc), can detect - # under `-sWASM_BINDGEN=auto` that wasm-bindgen needs to run as a post-link step. with webassembly.Module(wasm_file) as module: return module.get_custom_section('__wasm_bindgen_emscripten_marker') is not None @@ -1352,17 +1347,12 @@ def run_wasm_bindgen(infile): new_wasm_path = os.path.join(bindgen_out_dir, new_wasm_file) exports_after = {e.name for e in webassembly.get_exports(new_wasm_path)} - # Report which placeholder exports wasm-bindgen consumed so the caller can - # drop them from EXPORTED_FUNCTIONS, and which exports its expansion added so - # the caller can keep them off the public surface. removed_exports = exports_before - exports_after added_exports = exports_after - exports_before shutil.copyfile(new_wasm_path, infile) - # wasm-bindgen emits imported JS snippets into `snippets/` and the `import` - # statements referencing them into `library_bindgen.extern-pre.js`, only when - # the crate actually imports JS. + # Only emitted when the crate imports JS snippets. extern_pre_js = os.path.join(bindgen_out_dir, 'library_bindgen.extern-pre.js') if not os.path.exists(extern_pre_js): extern_pre_js = None diff --git a/tools/emscripten.py b/tools/emscripten.py index a477524e229b5..3bb104e8ccc67 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -625,12 +625,7 @@ def finalize_wasm(infile, outfile, js_syms): unexpected_exports = [e for e in unexpected_exports if e not in expected_exports] if (not settings.STANDALONE_WASM and 'main' in metadata.all_exports) or '__main_argc_argv' in metadata.all_exports: - if settings.WASM_BINDGEN: - # rustc drives the link with an EXPORTED_FUNCTIONS list that never - # includes `_main`, so skip the unused-main handling: `main` stays a wasm - # export and runs on init. It is kept off the public surface below. - pass - elif 'EXPORTED_FUNCTIONS' in user_settings and '_main' not in settings.USER_EXPORTS: + if 'EXPORTED_FUNCTIONS' in user_settings and '_main' not in settings.USER_EXPORTS: # If `_main` was unexpectedly exported we assume it was added to # EXPORT_IF_DEFINED by `phase_linker_setup` in order that we can detect # it and report this warning. After reporting the warning we explicitly @@ -645,10 +640,6 @@ def finalize_wasm(infile, outfile, js_syms): else: unexpected_exports.append('_main') - # Under WASM_BINDGEN, keep wasm-bindgen's internal glue exports (the raw - # symbols its generated bindings reach by name, and `_main`) off the public - # surface; wasm-bindgen surfaces the user-facing API itself via its JS - # library. Genuine EMSCRIPTEN_KEEPALIVE exports are not in this set and remain. if settings.WASM_BINDGEN: unexpected_exports = [e for e in unexpected_exports if e not in building.wasm_bindgen_internal_exports] diff --git a/tools/link.py b/tools/link.py index 66fedb39f3e0c..57d4452c4b277 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1000,6 +1000,9 @@ def phase_linker_setup(linker_args): # ruff: ignore[complex-structure, too-many if settings.MODULARIZE and settings.MODULARIZE not in {1, 'instance'}: exit_with_error(f'Invalid setting "{settings.MODULARIZE}" for MODULARIZE.') + if settings.WASM_BINDGEN not in {0, 1, 'auto'}: + exit_with_error(f'Invalid setting "{settings.WASM_BINDGEN}" for WASM_BINDGEN.') + def limit_incoming_module_api(): if options.oformat == OFormat.HTML and options.shell_html == DEFAULT_SHELL_HTML: # Our default shell.html file has minimal set of INCOMING_MODULE_JS_API elements that it expects @@ -1896,19 +1899,13 @@ def phase_link(linker_args, linker_inputs, wasm_target, js_syms): building.link_lld(linker_args, wasm_target, external_symbols=js_syms) - # -sWASM_BINDGEN=auto runs wasm-bindgen only when the linked wasm carries - # wasm-bindgen's marker section (e.g. cargo/rustc linking via emcc); - # otherwise it is a no-op. This lets rustc opt in without emcc guessing for - # ordinary builds. if settings.WASM_BINDGEN == 'auto': settings.WASM_BINDGEN = 1 if building.is_wasm_bindgen_module(wasm_target) else 0 - # wasm-bindgen reaches its exports by name, so they all have to be retained. - # When the link driver supplied EXPORTED_FUNCTIONS (rustc lists them all - # when driving the link) that is authoritative. Otherwise (e.g. a C/C++ build - # linking a Rust staticlib) discover them from the linker inputs and re-link. - # They are passed straight to the linker rather than via EXPORTED_FUNCTIONS - # so they are not mistaken for user-requested exports. + # wasm-bindgen reaches its exports by name. A supplied EXPORTED_FUNCTIONS + # lists them (rustc does this when driving the link); otherwise discover them + # from the linker inputs and re-link, passing them straight to the linker so + # they are not mistaken for user-requested exports. if settings.WASM_BINDGEN and 'EXPORTED_FUNCTIONS' not in user_settings: exports = building.get_wasm_bindgen_exported_symbols(linker_inputs) building.link_lld(linker_args + [f'--export={e}' for e in exports], wasm_target, external_symbols=js_syms) @@ -1937,25 +1934,18 @@ def phase_post_link(in_wasm, wasm_target, target, js_syms, base_metadata=None): if settings.WASM_BINDGEN: bindgen_jslib, removed_exports, added_exports, extern_pre_js, snippets_dir = building.run_wasm_bindgen(in_wasm) settings.JS_LIBRARIES.append(bindgen_jslib) - # 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 added) are internal, not a user-facing - # API: wasm-bindgen self-registers the real API via its JS library. Capture - # that set so it can be 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. A genuine - # EMSCRIPTEN_KEEPALIVE C/C++ export is not in this set and is still surfaced. + # The exports wasm-bindgen reaches by name (the supplied EXPORTED_FUNCTIONS + # plus anything its expansion added) are internal glue; wasm-bindgen's JS + # library registers the user-facing API itself. Keep them off every export + # layer, and drop the placeholder exports it consumed (__wbindgen_describe*, + # ...) so they aren't reported as undefined. removed = {shared.asmjs_mangle(e) for e in removed_exports} building.wasm_bindgen_internal_exports = ( set(settings.USER_EXPORTS) | {shared.asmjs_mangle(e) for e in added_exports}) - # Also drop the placeholder symbols wasm-bindgen consumed (__wbindgen_describe*, - # __externref_*, ...) so they aren't reported as undefined exports. drop = removed | building.wasm_bindgen_internal_exports settings.EXPORTED_FUNCTIONS = [e for e in settings.EXPORTED_FUNCTIONS if e not in drop] - settings.USER_EXPORTS = [] + settings.USER_EXPORTS = [e for e in settings.USER_EXPORTS if e not in removed] building.user_requested_exports.clear() - # Imported JS: emit wasm-bindgen's `import` statements as extern-pre-js and - # place the snippet files alongside the output so relative imports resolve. if extern_pre_js: options.extern_pre_js.append(extern_pre_js) if snippets_dir: From 47424edb8cf49fd24483d98871b4e263c487a211 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Thu, 3 Sep 2026 16:28:04 -0700 Subject: [PATCH 05/14] rewordings --- ChangeLog.md | 11 ++++------- .../docs/tools_reference/settings_reference.rst | 12 ++++++------ src/settings.js | 12 ++++++------ tools/link.py | 15 +++++++-------- 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 3909ec6a8da05..117469631d48e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -24,6 +24,10 @@ See docs/process.md for more on how version tagging works. diagnostic warning has been removed. (#27646) - `WASM=0` and `WASM=2` (wasm2js) were marked as deprecated. (See #27608) - mimalloc was updated to 3.5.1. (#27662) +- `-sWASM_BINDGEN` supports emcc usage as a post-link step, where + `EXPORTED_FUNCTIONS` is authoritative. `-sWASM_BINDGEN=auto` also supports + automatically determining `WASM_BINDGEN` mode handling based on detecting + the wasm-bindgen marker section. (#27208) 6.0.9 - 09/01/26 ---------------- @@ -72,13 +76,6 @@ See docs/process.md for more on how version tagging works. (`JS_BIGINT_INTEGRATION`) are universally available across all supported engines, and removes legacy JS polyfills and Binaryen lowering passes. (#27542) -- `-sWASM_BINDGEN` now runs wasm-bindgen (0.2.127 or later) as a post-link - step, unifying its output with emscripten's: wasm-bindgen's bindings are - surfaced as the user-facing API across the standard output modes (including - `-sMODULARIZE` and `-sWASM_ESM_INTEGRATION`) and its raw wasm exports are no - longer leaked. `-sWASM_BINDGEN=auto` enables this automatically when the - linked wasm was built with wasm-bindgen, so cargo/rustc builds that link via - emcc work without extra flags. (#27208) 6.0.6 - 08/05/26 ---------------- diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 5a4597f490971..6dbc5c04d592b 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3355,12 +3355,12 @@ WASM_BINDGEN Run wasm-bindgen and integrate the rust-exported symbols into the rest of Emscripten's JS output. Set to 1 to always run wasm-bindgen, or to 'auto' to run it only when the -linked wasm carries the custom section wasm-bindgen emits for this target, -which makes it a no-op for any other link (so wasm-bindgen need not be -installed). cargo/rustc builds that use emcc as the linker can pass 'auto' -unconditionally via `-Clink-arg`. When a C/C++ build links a Rust -staticlib, nothing guarantees the object carrying that section is pulled -into the link, so use 1 there. +linked Wasm carries the wasm-bindgen marker section, making it a no-op for +non wasm-bindgen binaries. +When linking Rust code, passing 'auto' allows wasm-bindgen processing to +be applied automatically, only when it is needed. +When a C/C++ build links a Rust staticlib, it is not definitely guaranteed +that the object carrying that section is pulled into the link, so use 1 there. If EXPORTED_FUNCTIONS is set it is taken as the complete export list and must include every export wasm-bindgen reaches by name (rustc supplies this when driving the link). Otherwise those exports are discovered from the diff --git a/src/settings.js b/src/settings.js index e7c72737588ea..e3788aef6da45 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2226,12 +2226,12 @@ var SIGNATURE_CONVERSIONS = []; // Run wasm-bindgen and integrate the rust-exported symbols into the rest of // Emscripten's JS output. // Set to 1 to always run wasm-bindgen, or to 'auto' to run it only when the -// linked wasm carries the custom section wasm-bindgen emits for this target, -// which makes it a no-op for any other link (so wasm-bindgen need not be -// installed). cargo/rustc builds that use emcc as the linker can pass 'auto' -// unconditionally via `-Clink-arg`. When a C/C++ build links a Rust -// staticlib, nothing guarantees the object carrying that section is pulled -// into the link, so use 1 there. +// linked Wasm carries the wasm-bindgen marker section, making it a no-op for +// non wasm-bindgen binaries. +// When linking Rust code, passing 'auto' allows wasm-bindgen processing to +// be applied automatically, only when it is needed. +// When a C/C++ build links a Rust staticlib, it is not definitely guaranteed +// that the object carrying that section is pulled into the link, so use 1 there. // If EXPORTED_FUNCTIONS is set it is taken as the complete export list and // must include every export wasm-bindgen reaches by name (rustc supplies this // when driving the link). Otherwise those exports are discovered from the diff --git a/tools/link.py b/tools/link.py index 57d4452c4b277..ffe04c5f563bf 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1902,10 +1902,9 @@ def phase_link(linker_args, linker_inputs, wasm_target, js_syms): if settings.WASM_BINDGEN == 'auto': settings.WASM_BINDGEN = 1 if building.is_wasm_bindgen_module(wasm_target) else 0 - # wasm-bindgen reaches its exports by name. A supplied EXPORTED_FUNCTIONS - # lists them (rustc does this when driving the link); otherwise discover them - # from the linker inputs and re-link, passing them straight to the linker so - # they are not mistaken for user-requested exports. + # If EXPORTED_FUNCTIONS is provided for WASM_BINDGEN, it forms the authoritative + # list of exports of the Wasm module (per rustc linking semantics). + # Otherwise, discover the symbols directly if not set for e.g. static linking Rust. if settings.WASM_BINDGEN and 'EXPORTED_FUNCTIONS' not in user_settings: exports = building.get_wasm_bindgen_exported_symbols(linker_inputs) building.link_lld(linker_args + [f'--export={e}' for e in exports], wasm_target, external_symbols=js_syms) @@ -1935,10 +1934,10 @@ def phase_post_link(in_wasm, wasm_target, target, js_syms, base_metadata=None): bindgen_jslib, removed_exports, added_exports, extern_pre_js, snippets_dir = building.run_wasm_bindgen(in_wasm) settings.JS_LIBRARIES.append(bindgen_jslib) # The exports wasm-bindgen reaches by name (the supplied EXPORTED_FUNCTIONS - # plus anything its expansion added) are internal glue; wasm-bindgen's JS - # library registers the user-facing API itself. Keep them off every export - # layer, and drop the placeholder exports it consumed (__wbindgen_describe*, - # ...) so they aren't reported as undefined. + # plus anything its expansion added) are internal glue only on the Wasm module, + # while wasm-bindgen's JS library registers the final user-facing API itself. + # Keep EXPORTED_FUNCTIONS off every export layer, and drop the placeholder exports it consumed + # (__wbindgen_describe*, etc.) so they aren't reported as undefined. removed = {shared.asmjs_mangle(e) for e in removed_exports} building.wasm_bindgen_internal_exports = ( set(settings.USER_EXPORTS) | {shared.asmjs_mangle(e) for e in added_exports}) From 99fe8d69a92bb02bfed83667297e57c5a1e12100 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 4 Sep 2026 11:40:42 -0700 Subject: [PATCH 06/14] Fold wasm_bindgen_internal_exports into expected_exports --- tools/emscripten.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tools/emscripten.py b/tools/emscripten.py index 3bb104e8ccc67..1277160fc743f 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -610,6 +610,7 @@ def finalize_wasm(infile, outfile, js_syms): expected_exports = set(settings.EXPORTED_FUNCTIONS) expected_exports.update(asmjs_mangle(s) for s in settings.REQUIRED_EXPORTS) expected_exports.update(asmjs_mangle(s) for s in settings.EXPORT_IF_DEFINED) + expected_exports.update(building.wasm_bindgen_internal_exports) # Assume that when JS symbol dependencies are exported it is because they # are needed by by a JS symbol and are not being explicitly exported due # to EMSCRIPTEN_KEEPALIVE (llvm.used). @@ -640,10 +641,6 @@ def finalize_wasm(infile, outfile, js_syms): else: unexpected_exports.append('_main') - if settings.WASM_BINDGEN: - unexpected_exports = [e for e in unexpected_exports - if e not in building.wasm_bindgen_internal_exports] - building.user_requested_exports.update(unexpected_exports) settings.EXPORTED_FUNCTIONS.extend(unexpected_exports) From e8081bd0717599331b3383186f07f0643d181d82 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 4 Sep 2026 11:42:41 -0700 Subject: [PATCH 07/14] Guard _main append on expected_exports --- tools/emscripten.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/emscripten.py b/tools/emscripten.py index 1277160fc743f..66ce83d9a5d46 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -638,7 +638,7 @@ def finalize_wasm(infile, outfile, js_syms): metadata.all_exports.remove('main') else: metadata.all_exports.remove('__main_argc_argv') - else: + elif '_main' not in expected_exports: unexpected_exports.append('_main') building.user_requested_exports.update(unexpected_exports) From f764bc9962efedf7157de8063d51b7e3cd405371 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 4 Sep 2026 11:51:41 -0700 Subject: [PATCH 08/14] Make WASM_BINDGEN always detect the marker section, removing the auto mode --- ChangeLog.md | 3 +++ .../tools_reference/settings_reference.rst | 9 ++------ src/settings.js | 9 ++------ test/test_other.py | 13 +++++------ tools/link.py | 22 +++++++++---------- tools/settings.py | 2 +- 6 files changed, 25 insertions(+), 33 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 117469631d48e..8e108e09eac23 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -28,6 +28,9 @@ See docs/process.md for more on how version tagging works. `EXPORTED_FUNCTIONS` is authoritative. `-sWASM_BINDGEN=auto` also supports automatically determining `WASM_BINDGEN` mode handling based on detecting the wasm-bindgen marker section. (#27208) + `EXPORTED_FUNCTIONS` is authoritative. It is now a no-op unless the linked + Wasm carries the wasm-bindgen marker section. (#27208) +- mimalloc was updated to 3.5.1. (#27662) 6.0.9 - 09/01/26 ---------------- diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 6dbc5c04d592b..9dfd69d5396ca 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3354,13 +3354,8 @@ WASM_BINDGEN Run wasm-bindgen and integrate the rust-exported symbols into the rest of Emscripten's JS output. -Set to 1 to always run wasm-bindgen, or to 'auto' to run it only when the -linked Wasm carries the wasm-bindgen marker section, making it a no-op for -non wasm-bindgen binaries. -When linking Rust code, passing 'auto' allows wasm-bindgen processing to -be applied automatically, only when it is needed. -When a C/C++ build links a Rust staticlib, it is not definitely guaranteed -that the object carrying that section is pulled into the link, so use 1 there. +wasm-bindgen is only run when the linked Wasm carries the wasm-bindgen +marker section, so this is a no-op for non wasm-bindgen binaries. If EXPORTED_FUNCTIONS is set it is taken as the complete export list and must include every export wasm-bindgen reaches by name (rustc supplies this when driving the link). Otherwise those exports are discovered from the diff --git a/src/settings.js b/src/settings.js index e3788aef6da45..67aaab94047f6 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2225,13 +2225,8 @@ var SIGNATURE_CONVERSIONS = []; // Run wasm-bindgen and integrate the rust-exported symbols into the rest of // Emscripten's JS output. -// Set to 1 to always run wasm-bindgen, or to 'auto' to run it only when the -// linked Wasm carries the wasm-bindgen marker section, making it a no-op for -// non wasm-bindgen binaries. -// When linking Rust code, passing 'auto' allows wasm-bindgen processing to -// be applied automatically, only when it is needed. -// When a C/C++ build links a Rust staticlib, it is not definitely guaranteed -// that the object carrying that section is pulled into the link, so use 1 there. +// wasm-bindgen is only run when the linked Wasm carries the wasm-bindgen +// marker section, so this is a no-op for non wasm-bindgen binaries. // If EXPORTED_FUNCTIONS is set it is taken as the complete export list and // must include every export wasm-bindgen reaches by name (rustc supplies this // when driving the link). Otherwise those exports are discovered from the diff --git a/test/test_other.py b/test/test_other.py index aca08a6155f95..54b9350912c8c 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15373,11 +15373,10 @@ def test_wasm_bindgen_integration(self): '''), }) def test_wasm_bindgen_rustc_driven(self, ldflags, prelude): - # cargo/rustc links via emcc; pass -sWASM_BINDGEN=auto (plus the output-mode - # settings) through as link args so emcc detects wasm-bindgen's marker - # section in the linked wasm and runs wasm-bindgen as a post-link step. + # cargo/rustc links via emcc; pass -sWASM_BINDGEN (plus the output-mode + # settings) through as link args so emcc runs wasm-bindgen as a post-link step. copytree(test_file('rust/bindgen_greeter'), '.') - link_args = ['-sWASM_BINDGEN=auto', '-Wno-experimental'] + ldflags + link_args = ['-sWASM_BINDGEN', '-Wno-experimental'] + ldflags rustflags = ', '.join(f'"-Clink-arg={a}"' for a in link_args) ensure_dir('.cargo') create_file('.cargo/config.toml', f''' @@ -15413,11 +15412,11 @@ def test_wasm_bindgen_rustc_driven(self, ldflags, prelude): # even though `_main` is not surfaced as a user-facing export. self.assertContained('main ran', output) - def test_wasm_bindgen_auto_no_marker(self): - # -sWASM_BINDGEN=auto is a no-op for an ordinary build with no wasm-bindgen + def test_wasm_bindgen_no_marker(self): + # -sWASM_BINDGEN is a no-op for an ordinary build with no wasm-bindgen # marker section: wasm-bindgen is never invoked (so it need not be installed) # and the program builds and runs normally. - self.do_runf('hello_world.c', 'Hello, world!', cflags=['-sWASM_BINDGEN=auto', '-Wno-experimental']) + self.do_runf('hello_world.c', 'Hello, world!', cflags=['-sWASM_BINDGEN', '-Wno-experimental']) @requires_rust @requires_dev_dependency('typescript') diff --git a/tools/link.py b/tools/link.py index ffe04c5f563bf..a72334a61fd58 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1000,9 +1000,6 @@ def phase_linker_setup(linker_args): # ruff: ignore[complex-structure, too-many if settings.MODULARIZE and settings.MODULARIZE not in {1, 'instance'}: exit_with_error(f'Invalid setting "{settings.MODULARIZE}" for MODULARIZE.') - if settings.WASM_BINDGEN not in {0, 1, 'auto'}: - exit_with_error(f'Invalid setting "{settings.WASM_BINDGEN}" for WASM_BINDGEN.') - def limit_incoming_module_api(): if options.oformat == OFormat.HTML and options.shell_html == DEFAULT_SHELL_HTML: # Our default shell.html file has minimal set of INCOMING_MODULE_JS_API elements that it expects @@ -1897,17 +1894,20 @@ def phase_link(linker_args, linker_inputs, wasm_target, js_syms): settings.LINKABLE = True rtn = extract_metadata.extract_metadata(wasm_target) - building.link_lld(linker_args, wasm_target, external_symbols=js_syms) - - if settings.WASM_BINDGEN == 'auto': - settings.WASM_BINDGEN = 1 if building.is_wasm_bindgen_module(wasm_target) else 0 - # If EXPORTED_FUNCTIONS is provided for WASM_BINDGEN, it forms the authoritative # list of exports of the Wasm module (per rustc linking semantics). - # Otherwise, discover the symbols directly if not set for e.g. static linking Rust. + # Otherwise, discover the symbols directly from the linker inputs for e.g. static + # linking Rust. This also pulls in wasm-bindgen's marker object from a staticlib. + bindgen_exports = [] if settings.WASM_BINDGEN and 'EXPORTED_FUNCTIONS' not in user_settings: - exports = building.get_wasm_bindgen_exported_symbols(linker_inputs) - building.link_lld(linker_args + [f'--export={e}' for e in exports], wasm_target, external_symbols=js_syms) + bindgen_exports = [f'--export={e}' for e in building.get_wasm_bindgen_exported_symbols(linker_inputs)] + + building.link_lld(linker_args + bindgen_exports, wasm_target, external_symbols=js_syms) + + if settings.WASM_BINDGEN and not building.is_wasm_bindgen_module(wasm_target): + settings.WASM_BINDGEN = 0 + if bindgen_exports: + building.link_lld(linker_args, wasm_target, external_symbols=js_syms) return rtn diff --git a/tools/settings.py b/tools/settings.py index b227f8516e455..699e65615d436 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -415,7 +415,7 @@ def __setattr__(self, name, value): def check_type(self, name, value): # These settings have a variable type so cannot be easily type checked. - if name in {'EXECUTABLE', 'SUPPORT_LONGJMP', 'PTHREAD_POOL_SIZE', 'SEPARATE_DWARF', 'LTO', 'MODULARIZE', 'WASM_BINDGEN'}: + if name in {'EXECUTABLE', 'SUPPORT_LONGJMP', 'PTHREAD_POOL_SIZE', 'SEPARATE_DWARF', 'LTO', 'MODULARIZE'}: return expected_type = self.types.get(name) if not expected_type: From da5af2fdb6bbb257f4dcf52610122f21fa5a3e10 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 4 Sep 2026 11:55:39 -0700 Subject: [PATCH 09/14] Clarify WASM_BINDGEN marker section docs --- ChangeLog.md | 4 ++++ site/source/docs/tools_reference/settings_reference.rst | 9 ++++++--- src/settings.js | 9 ++++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 8e108e09eac23..5e582d58caaee 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -30,6 +30,10 @@ See docs/process.md for more on how version tagging works. the wasm-bindgen marker section. (#27208) `EXPORTED_FUNCTIONS` is authoritative. It is now a no-op unless the linked Wasm carries the wasm-bindgen marker section. (#27208) + `EXPORTED_FUNCTIONS` is authoritative. wasm-bindgen processing is only + performed when the linked Wasm carries the wasm-bindgen Emscripten marker + section, so `-sWASM_BINDGEN` can safely be passed to non-wasm-bindgen builds. + (#27208) - mimalloc was updated to 3.5.1. (#27662) 6.0.9 - 09/01/26 diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 9dfd69d5396ca..677907290b068 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3354,12 +3354,15 @@ WASM_BINDGEN Run wasm-bindgen and integrate the rust-exported symbols into the rest of Emscripten's JS output. -wasm-bindgen is only run when the linked Wasm carries the wasm-bindgen -marker section, so this is a no-op for non wasm-bindgen binaries. +Even with this setting enabled, wasm-bindgen processing is only performed +when the linked Wasm carries the wasm-bindgen Emscripten marker section +(emitted by the wasm-bindgen crate). When the marker is absent the build is +unchanged, so -sWASM_BINDGEN can safely be passed unconditionally to +non-wasm-bindgen builds, and by toolchains that link via emcc. If EXPORTED_FUNCTIONS is set it is taken as the complete export list and must include every export wasm-bindgen reaches by name (rustc supplies this when driving the link). Otherwise those exports are discovered from the -linker inputs and the module is re-linked to retain them. +linker inputs. .. note:: This is an experimental setting diff --git a/src/settings.js b/src/settings.js index 67aaab94047f6..d26b414f5b374 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2225,12 +2225,15 @@ var SIGNATURE_CONVERSIONS = []; // Run wasm-bindgen and integrate the rust-exported symbols into the rest of // Emscripten's JS output. -// wasm-bindgen is only run when the linked Wasm carries the wasm-bindgen -// marker section, so this is a no-op for non wasm-bindgen binaries. +// Even with this setting enabled, wasm-bindgen processing is only performed +// when the linked Wasm carries the wasm-bindgen Emscripten marker section +// (emitted by the wasm-bindgen crate). When the marker is absent the build is +// unchanged, so -sWASM_BINDGEN can safely be passed unconditionally to +// non-wasm-bindgen builds, and by toolchains that link via emcc. // If EXPORTED_FUNCTIONS is set it is taken as the complete export list and // must include every export wasm-bindgen reaches by name (rustc supplies this // when driving the link). Otherwise those exports are discovered from the -// linker inputs and the module is re-linked to retain them. +// linker inputs. // [link] // [experimental] var WASM_BINDGEN = 0; From d0336e57c76d923ebbf957e499ef129af1291798 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 4 Sep 2026 13:59:59 -0700 Subject: [PATCH 10/14] Detect the wasm-bindgen marker from linker inputs to avoid relinking --- ChangeLog.md | 2 +- .../docs/tools_reference/settings_reference.rst | 2 +- src/settings.js | 2 +- .../test_codesize_hello_dylink_all.json | 4 ++-- tools/building.py | 9 ++++++--- tools/link.py | 17 +++++++---------- tools/shared.py | 1 + 7 files changed, 19 insertions(+), 18 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 5e582d58caaee..a0653ba8d184b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -31,7 +31,7 @@ See docs/process.md for more on how version tagging works. `EXPORTED_FUNCTIONS` is authoritative. It is now a no-op unless the linked Wasm carries the wasm-bindgen marker section. (#27208) `EXPORTED_FUNCTIONS` is authoritative. wasm-bindgen processing is only - performed when the linked Wasm carries the wasm-bindgen Emscripten marker + performed when the linker inputs carry the wasm-bindgen Emscripten marker section, so `-sWASM_BINDGEN` can safely be passed to non-wasm-bindgen builds. (#27208) - mimalloc was updated to 3.5.1. (#27662) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 677907290b068..3decfb5b6f220 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3355,7 +3355,7 @@ WASM_BINDGEN Run wasm-bindgen and integrate the rust-exported symbols into the rest of Emscripten's JS output. Even with this setting enabled, wasm-bindgen processing is only performed -when the linked Wasm carries the wasm-bindgen Emscripten marker section +when the linker inputs carry the wasm-bindgen Emscripten marker section (emitted by the wasm-bindgen crate). When the marker is absent the build is unchanged, so -sWASM_BINDGEN can safely be passed unconditionally to non-wasm-bindgen builds, and by toolchains that link via emcc. diff --git a/src/settings.js b/src/settings.js index d26b414f5b374..667a51862e4fe 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2226,7 +2226,7 @@ var SIGNATURE_CONVERSIONS = []; // Run wasm-bindgen and integrate the rust-exported symbols into the rest of // Emscripten's JS output. // Even with this setting enabled, wasm-bindgen processing is only performed -// when the linked Wasm carries the wasm-bindgen Emscripten marker section +// when the linker inputs carry the wasm-bindgen Emscripten marker section // (emitted by the wasm-bindgen crate). When the marker is absent the build is // unchanged, so -sWASM_BINDGEN can safely be passed unconditionally to // non-wasm-bindgen builds, and by toolchains that link via emcc. diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 99fa97702becc..cc7cb5914cb50 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 270584, + "a.out.js": 270569, "a.out.nodebug.wasm": 588324, - "total": 858908, + "total": 858893, "sent": [ "IMG_Init", "IMG_Load", diff --git a/tools/building.py b/tools/building.py index a917e62135311..5b2e0fc2a8bdf 100644 --- a/tools/building.py +++ b/tools/building.py @@ -39,6 +39,7 @@ LLVM_DWARFDUMP, LLVM_NM, LLVM_OBJCOPY, + LLVM_OBJDUMP, WASM_LD, asmjs_mangle, check_call, @@ -1317,9 +1318,11 @@ def run_wasm_opt(infile, outfile=None, args=[], **kwargs): # ruff: ignore[mutab return run_binaryen_command('wasm-opt', infile, outfile, args=args, **kwargs) -def is_wasm_bindgen_module(wasm_file): - with webassembly.Module(wasm_file) as module: - return module.get_custom_section('__wasm_bindgen_emscripten_marker') is not None +def has_wasm_bindgen_marker(input_files): + if not input_files: + return False + result = check_call([LLVM_OBJDUMP, '--section-headers'] + input_files, stdout=PIPE) + return '__wasm_bindgen_emscripten_marker' in result.stdout def run_wasm_bindgen(infile): diff --git a/tools/link.py b/tools/link.py index a72334a61fd58..1c54130491718 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1894,21 +1894,18 @@ def phase_link(linker_args, linker_inputs, wasm_target, js_syms): settings.LINKABLE = True rtn = extract_metadata.extract_metadata(wasm_target) + # WASM_BINDGEN is a no-op unless the inputs carry the wasm-bindgen marker section. + if settings.WASM_BINDGEN and not building.has_wasm_bindgen_marker(linker_inputs): + settings.WASM_BINDGEN = 0 + # If EXPORTED_FUNCTIONS is provided for WASM_BINDGEN, it forms the authoritative # list of exports of the Wasm module (per rustc linking semantics). # Otherwise, discover the symbols directly from the linker inputs for e.g. static - # linking Rust. This also pulls in wasm-bindgen's marker object from a staticlib. - bindgen_exports = [] + # linking Rust. if settings.WASM_BINDGEN and 'EXPORTED_FUNCTIONS' not in user_settings: - bindgen_exports = [f'--export={e}' for e in building.get_wasm_bindgen_exported_symbols(linker_inputs)] - - building.link_lld(linker_args + bindgen_exports, wasm_target, external_symbols=js_syms) - - if settings.WASM_BINDGEN and not building.is_wasm_bindgen_module(wasm_target): - settings.WASM_BINDGEN = 0 - if bindgen_exports: - building.link_lld(linker_args, wasm_target, external_symbols=js_syms) + linker_args = linker_args + [f'--export={e}' for e in building.get_wasm_bindgen_exported_symbols(linker_inputs)] + building.link_lld(linker_args, wasm_target, external_symbols=js_syms) return rtn diff --git a/tools/shared.py b/tools/shared.py index d20ea3742b195..f56786022e9a9 100644 --- a/tools/shared.py +++ b/tools/shared.py @@ -636,6 +636,7 @@ def init(): LLVM_NM = llvm_tool_path('llvm-nm') LLVM_DWARFDUMP = llvm_tool_path('llvm-dwarfdump') LLVM_OBJCOPY = llvm_tool_path('llvm-objcopy') +LLVM_OBJDUMP = llvm_tool_path('llvm-objdump') WASM_LD = llvm_tool_path('wasm-ld') LLVM_PROFDATA = llvm_tool_path('llvm-profdata') LLVM_COV = llvm_tool_path('llvm-cov') From d60eb0ee7e15864607602c23286ee3af7934ba35 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 4 Sep 2026 14:02:53 -0700 Subject: [PATCH 11/14] Fix ruff lints --- tools/building.py | 2 +- tools/link.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/building.py b/tools/building.py index 5b2e0fc2a8bdf..06967d97e6a55 100644 --- a/tools/building.py +++ b/tools/building.py @@ -1321,7 +1321,7 @@ def run_wasm_opt(infile, outfile=None, args=[], **kwargs): # ruff: ignore[mutab def has_wasm_bindgen_marker(input_files): if not input_files: return False - result = check_call([LLVM_OBJDUMP, '--section-headers'] + input_files, stdout=PIPE) + result = check_call([LLVM_OBJDUMP, '--section-headers', *input_files], stdout=PIPE) return '__wasm_bindgen_emscripten_marker' in result.stdout diff --git a/tools/link.py b/tools/link.py index 1c54130491718..dfa16c4220179 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1903,7 +1903,7 @@ def phase_link(linker_args, linker_inputs, wasm_target, js_syms): # Otherwise, discover the symbols directly from the linker inputs for e.g. static # linking Rust. if settings.WASM_BINDGEN and 'EXPORTED_FUNCTIONS' not in user_settings: - linker_args = linker_args + [f'--export={e}' for e in building.get_wasm_bindgen_exported_symbols(linker_inputs)] + linker_args += [f'--export={e}' for e in building.get_wasm_bindgen_exported_symbols(linker_inputs)] building.link_lld(linker_args, wasm_target, external_symbols=js_syms) return rtn From 55adabab5ff83cf31e28a7c870dd07e3b8d775f6 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 4 Sep 2026 14:27:30 -0700 Subject: [PATCH 12/14] rebaseline fix --- test/codesize/test_codesize_hello_dylink_all.json | 4 ++-- tools/emscripten.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index cc7cb5914cb50..99fa97702becc 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 270569, + "a.out.js": 270584, "a.out.nodebug.wasm": 588324, - "total": 858893, + "total": 858908, "sent": [ "IMG_Init", "IMG_Load", diff --git a/tools/emscripten.py b/tools/emscripten.py index 66ce83d9a5d46..0651dfaaee4b0 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -638,7 +638,7 @@ def finalize_wasm(infile, outfile, js_syms): metadata.all_exports.remove('main') else: metadata.all_exports.remove('__main_argc_argv') - elif '_main' not in expected_exports: + elif '_main' not in building.wasm_bindgen_internal_exports: unexpected_exports.append('_main') building.user_requested_exports.update(unexpected_exports) From 449f097356f9e5431b7d82c25f156781dce98c11 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 4 Sep 2026 14:49:54 -0700 Subject: [PATCH 13/14] Fix ChangeLog entry --- ChangeLog.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index a0653ba8d184b..e09084933ccd9 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -25,16 +25,10 @@ See docs/process.md for more on how version tagging works. - `WASM=0` and `WASM=2` (wasm2js) were marked as deprecated. (See #27608) - mimalloc was updated to 3.5.1. (#27662) - `-sWASM_BINDGEN` supports emcc usage as a post-link step, where - `EXPORTED_FUNCTIONS` is authoritative. `-sWASM_BINDGEN=auto` also supports - automatically determining `WASM_BINDGEN` mode handling based on detecting - the wasm-bindgen marker section. (#27208) - `EXPORTED_FUNCTIONS` is authoritative. It is now a no-op unless the linked - Wasm carries the wasm-bindgen marker section. (#27208) `EXPORTED_FUNCTIONS` is authoritative. wasm-bindgen processing is only performed when the linker inputs carry the wasm-bindgen Emscripten marker section, so `-sWASM_BINDGEN` can safely be passed to non-wasm-bindgen builds. (#27208) -- mimalloc was updated to 3.5.1. (#27662) 6.0.9 - 09/01/26 ---------------- From ad4ec9342939ab36faaba51d1556e7a5f8fa4095 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 4 Sep 2026 16:10:36 -0700 Subject: [PATCH 14/14] Use the managed wasm-bindgen CLI in test_wasm_bindgen_tsd_multi_return --- test/test_other.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index 54b9350912c8c..18bfcb63c1290 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15419,6 +15419,7 @@ def test_wasm_bindgen_no_marker(self): self.do_runf('hello_world.c', 'Hello, world!', cflags=['-sWASM_BINDGEN', '-Wno-experimental']) @requires_rust + @requires_wasm_bindgen @requires_dev_dependency('typescript') def test_wasm_bindgen_tsd_multi_return(self): copytree(test_file('rust/bindgen_integration'), '.') @@ -15429,11 +15430,10 @@ def test_wasm_bindgen_tsd_multi_return(self): Ok(42) } ''') - self.run_process(['cargo', 'add', 'wasm-bindgen']) + self.run_process(['cargo', 'add', 'wasm-bindgen@0.2.127']) self.run_process(['cargo', 'build']) lib = 'target/wasm32-unknown-emscripten/debug/libbindgen_integration.a' create_file('empty.c', '') - self.run_process(['cargo', 'install', 'wasm-bindgen-cli']) self.run_process([EMCC, 'empty.c', '--emit-tsd', 'test_multi.d.ts', '-sWASM_BINDGEN', '-Wno-experimental', '-o', 'test_multi.js'] + [lib] + self.get_cflags()) actual = read_file('test_multi.d.ts') self.assertContained("multi_value_return(): [number, number, number];", actual)