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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,17 @@ commands:
install-rust:
steps:
- run:
name: install rust
name: install rust and wasm-bindgen
# 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
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:
Expand Down
5 changes: 5 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ 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. 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)

6.0.9 - 09/01/26
----------------
Expand Down
12 changes: 11 additions & 1 deletion site/source/docs/tools_reference/settings_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3352,7 +3352,17 @@ 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.
Even with this setting enabled, wasm-bindgen processing is only performed
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.
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.

.. note:: This is an experimental setting

Expand Down
7 changes: 7 additions & 0 deletions src/postamble.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,14 @@ 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 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 }}}';
Comment thread
guybedford marked this conversation as resolved.
#else
var wasmExports;
#endif
#if SPLIT_MODULE
var wasmRawExports;
#endif
Expand Down
12 changes: 11 additions & 1 deletion src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2223,7 +2223,17 @@ 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.
// Even with this setting enabled, wasm-bindgen processing is only performed
// 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.
// 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.
// [link]
// [experimental]
var WASM_BINDGEN = 0;
Expand Down
13 changes: 13 additions & 0 deletions test/rust/bindgen_greeter/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
23 changes: 23 additions & 0 deletions test/rust/bindgen_greeter/src/main.rs
Original file line number Diff line number Diff line change
@@ -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");
}
93 changes: 86 additions & 7 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -15324,22 +15329,97 @@ 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.h>
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 ES6 MODULARIZE surface the clean wasm-bindgen API
# differently (named ESM exports vs `Module.<name>`). Both must expose exactly
# the `Greeter` class and none of the raw wasm exports rustc lists.
@requires_rust
@requires_wasm_bindgen
@parameterized({
'esm_integration': (['-sWASM_ESM_INTEGRATION'], '''
import init, * as mod from './bindgen_greeter.js';
await init();
'''),
'es6': (['-sMODULARIZE', '-sEXPORT_ES6'], '''
import Module from './bindgen_greeter.js';
const mod = await Module();
'''),
})
def test_wasm_bindgen_rustc_driven(self, ldflags, prelude):
# 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', '-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.
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);
''')
# 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),
# even though `_main` is not surfaced as a user-facing export.
self.assertContained('main ran', output)

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', '-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'), '.')
Expand All @@ -15350,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)
Expand Down
39 changes: 30 additions & 9 deletions tools/building.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
LLVM_DWARFDUMP,
LLVM_NM,
LLVM_OBJCOPY,
LLVM_OBJDUMP,
WASM_LD,
asmjs_mangle,
check_call,
Expand All @@ -62,6 +63,8 @@
user_requested_exports: set[str] = set()
# JS library symbols exported via the `__export` decorator.
extra_js_exports: set[str] = set()
# 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`)
# which reads it using `--detect-features`.
Expand Down Expand Up @@ -307,16 +310,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'}]

if settings.WASM_BINDGEN:
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):
Expand Down Expand Up @@ -345,7 +344,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)
Expand All @@ -354,7 +353,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)
Expand Down Expand Up @@ -1319,6 +1318,13 @@ def run_wasm_opt(infile, outfile=None, args=[], **kwargs): # ruff: ignore[mutab
return run_binaryen_command('wasm-opt', infile, outfile, args=args, **kwargs)


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):
bindgen_out_dir = os.path.join(get_emscripten_temp_dir(), 'bindgen_out')

Expand All @@ -1333,16 +1339,31 @@ 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]
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)}
removed_exports = exports_before - exports_after
added_exports = exports_after - exports_before

shutil.copyfile(new_wasm_path, infile)

shutil.copyfile(os.path.join(bindgen_out_dir, new_wasm_file), infile)
# 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
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')
return os.path.join(bindgen_out_dir, 'library_bindgen.js'), removed_exports, added_exports, extern_pre_js, snippets_dir


intermediate_counter = 0
Expand Down
3 changes: 2 additions & 1 deletion tools/emscripten.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -637,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 building.wasm_bindgen_internal_exports:
unexpected_exports.append('_main')

building.user_requested_exports.update(unexpected_exports)
Expand Down
Loading
Loading