Skip to content

[DRAFT] add wasm-bindgen support #23493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 31 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e9bf8a0
first pass at a hack
walkingeyerobot Dec 23, 2024
135405f
oops
walkingeyerobot Dec 23, 2024
e5b67a7
wasm bindgen setting
walkingeyerobot Dec 23, 2024
6732eec
link in actual js output by wasm-bindgen
walkingeyerobot Jan 13, 2025
1708fdf
don't try to predict the wasm filename
walkingeyerobot Jan 29, 2025
5f3feae
address code review comments
walkingeyerobot Jan 29, 2025
8ff4580
switch to single file js for bindgen
walkingeyerobot Feb 13, 2025
44d0f55
Merge remote-tracking branch 'upstream/main' into wbg-walkingeyerobot
walkingeyerobot Feb 20, 2025
836fbe6
minor cleanup
walkingeyerobot Feb 21, 2025
2debab9
first test
walkingeyerobot Feb 24, 2025
ea4eab0
only avoid stripping if wasm bindgen is enabled
walkingeyerobot Mar 11, 2025
d7faa6b
remove irrelevant comment; going to solve this a different way
walkingeyerobot Mar 11, 2025
cb281cb
Make HEAP_DATA_VIEW available for wasm-bindgen regardless of endianness
google-yfyang Mar 12, 2025
2843e36
Automatically infer what symbols to export for wasm-bindgen
googlewalt Mar 13, 2025
26cd774
Remove hack not to export system libraries
googlewalt Mar 13, 2025
cfc9b11
Merge pull request #1 from googlewalt/linkargs
walkingeyerobot Mar 13, 2025
c2dfc91
wasm bindgen now uses the 'env' property instead of its own 'wbg'
walkingeyerobot Mar 17, 2025
4196290
with newer rustc, there appears to be a bunch of anon symbols that we…
walkingeyerobot Mar 24, 2025
8c08d8e
pass --keep-debug to wasm-bindgen always.
walkingeyerobot Apr 28, 2025
50e8795
Merge branch 'branch-405' into wbg-walkingeyerobot
walkingeyerobot May 21, 2025
09992ae
Merge branch 'branch-406' into wbg-walkingeyerobot
walkingeyerobot May 21, 2025
126f551
Merge branch 'branch-407' into wbg-walkingeyerobot
walkingeyerobot May 21, 2025
c779002
Merge branch 'branch-408' into wbg-walkingeyerobot
walkingeyerobot May 21, 2025
8e5fd0a
Merge branch 'branch-409' into wbg-walkingeyerobot
walkingeyerobot May 21, 2025
feaf9be
Merge remote-tracking branch 'upstream/main' into wbg-walkingeyerobot
walkingeyerobot May 21, 2025
50b600f
Merge remote-tracking branch 'upstream/main' into wbg-walkingeyerobot
walkingeyerobot May 21, 2025
50c4fe1
Get typescript with wasm-bindgen to work
google-yfyang Jun 4, 2025
24be666
Merge remote-tracking branch 'upstream/main' into wbg-walkingeyerobot
walkingeyerobot Jun 12, 2025
fe3db01
don't do big endian stuff with bindgen stuff unless explicitly asked to
walkingeyerobot Jun 24, 2025
6d1c5ff
make typescript work with emscripten (part2)
google-yfyang Jun 25, 2025
9c92122
Merge pull request #2 from walkingeyerobot/yfyang
walkingeyerobot Jun 25, 2025
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
2 changes: 1 addition & 1 deletion emcc.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,7 @@ def compile_source_file(input_file):
# Default to assuming the inputs are object files and pass them to the linker
pass

return [f.value for f in linker_args]
return linker_args


@ToolchainProfiler.profile()
Expand Down
6 changes: 4 additions & 2 deletions src/runtime_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ var
HEAPU64;
#endif

#if SUPPORT_BIG_ENDIAN
#if SUPPORT_BIG_ENDIAN || WASM_BINDGEN
/** @type {!DataView} */
var HEAP_DATA_VIEW;
#endif
Expand Down Expand Up @@ -145,8 +145,10 @@ function updateMemoryViews() {
{{{ maybeExportHeap('HEAP64') }}}HEAP64 = new BigInt64Array(b);
{{{ maybeExportHeap('HEAPU64') }}}HEAPU64 = new BigUint64Array(b);
#endif
#if SUPPORT_BIG_ENDIAN || WASM_BINDGEN
{{{ maybeExportHeap('HEAP_DATA_VIEW') }}}HEAP_DATA_VIEW = new DataView(b);
#endif
#if SUPPORT_BIG_ENDIAN
{{{ maybeExportHeap('HEAP_DATA_VIEW') }}} HEAP_DATA_VIEW = new DataView(b);
LE_HEAP_UPDATE();
#endif
}
Expand Down
4 changes: 4 additions & 0 deletions src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2164,6 +2164,10 @@ 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.
// [link]
var WASM_BINDGEN = 0;

// Experimental support for wasm source phase imports.
// This is only currently implemented in the pre-release/nightly version of node,
// and not yet supported by browsers.
Expand Down
2 changes: 2 additions & 0 deletions test/rust/basics/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-emscripten"
7 changes: 7 additions & 0 deletions test/rust/bindgen_integration/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[build]
target = "wasm32-unknown-emscripten"
rustflags = [
"-Cllvm-args=-enable-emscripten-cxx-exceptions=0",
"-Cpanic=abort",
"-Crelocation-model=static",
]
9 changes: 9 additions & 0 deletions test/rust/bindgen_integration/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "bindgen_integration"
edition = "2021"

[lib]
crate-type = ["staticlib"]

[dependencies]
wasm-bindgen = { path = "../../../wasm-bindgen" }
6 changes: 6 additions & 0 deletions test/rust/bindgen_integration/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn rs_add(a: i32, b: i32) -> i32 {
return a + b;
}
36 changes: 35 additions & 1 deletion test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -15943,7 +15943,7 @@ def test_embool(self):
@requires_rust
def test_rust_integration_basics(self):
copytree(test_file('rust/basics'), '.')
self.run_process(['cargo', 'build', '--target=wasm32-unknown-emscripten'])
self.run_process(['cargo', 'build'])
lib = 'target/wasm32-unknown-emscripten/debug/libbasics.a'
self.assertExists(lib)

Expand All @@ -15955,6 +15955,40 @@ def test_rust_integration_basics(self):
}''')
self.do_runf('main.cpp', 'Hello from rust!', emcc_args=[lib])

@requires_rust
def test_wasm_bindgen_integration(self):
copytree(test_file('rust/bindgen_integration'), '.')
self.run_process(['cargo', 'build'])
lib = 'target/wasm32-unknown-emscripten/debug/libbindgen_integration.a'
self.assertExists(lib)

create_file('main.cpp', '')
create_file('post.js', '''
Module.onRuntimeInitialized = () => {
out(Module.rs_add(17, 25));
};
''')
exported_funcs = [
'___wbindgen_describe_rs_add',
'_rs_add',
'___externref_drop_slice',
'___externref_heap_live_count',
'___externref_table_alloc',
'___externref_table_dealloc',
'___wbindgen_exn_store',
'___wbindgen_free',
'___wbindgen_malloc',
'___wbindgen_realloc']
emcc_args = [
lib,
'-sWASM_BINDGEN',
'--post-js',
'post.js',
'-Wno-undefined',
'-sEXPORTED_FUNCTIONS=' + ','.join(exported_funcs),
]
self.do_runf('main.cpp', '42', emcc_args=emcc_args)

def test_relative_em_cache(self):
with env_modify({'EM_CACHE': 'foo'}):
err = self.expect_fail([EMCC, '-c', test_file('hello_world.c')])
Expand Down
55 changes: 54 additions & 1 deletion tools/building.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,32 @@ def lld_flags_for_executable(external_symbols):
return cmd


def link_lld(args, target, external_symbols=None):
def get_wasm_bindgen_exported_symbols(input_files):
if not os.path.exists(LLVM_NM):
exit_with_error('llvm-nm not found in LLVM directory: %s', LLVM_NM)

nm_args = [
LLVM_NM,
'--defined-only',
'--extern-only',
'--format=just-symbols',
'--print-file-name',
'--quiet',
]

result = run_process(nm_args + input_files, stdout=subprocess.PIPE)
symbols = []
for line in result.stdout.splitlines():
(path, symbol) = line.split()
# Skip mangled (non-C) symbols
if symbol.startswith('_Z') or symbol.startswith('_R') or symbol.startswith('anon.'):
continue
symbols.append(symbol)

return symbols


def link_lld(args, target, external_symbols=None, linker_inputs=[]):
if not os.path.exists(WASM_LD):
exit_with_error('linker binary not found in LLVM directory: %s', WASM_LD)
# runs lld to link things.
Expand All @@ -263,6 +288,10 @@ def link_lld(args, target, external_symbols=None):
# 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-if-defined={e}' for e in exported_symbols)

# Emscripten currently expects linkable output (SIDE_MODULE/MAIN_MODULE) to
# include all archive contents.
if settings.LINKABLE:
Expand Down Expand Up @@ -1239,6 +1268,30 @@ def run_wasm_opt(infile, outfile=None, args=[], **kwargs): # noqa
return run_binaryen_command('wasm-opt', infile, outfile, args=args, **kwargs)


def run_wasm_bindgen(infile, outfile=None, args=[], **kwargs): # noqa
bindgen_out_dir = get_emscripten_temp_dir() + '/bindgen_out/'

cmd = config.WASM_BINDGEN + [
infile,
'--target',
'emscripten',
'--keep-lld-exports',
'--keep-debug',
'--out-dir',
bindgen_out_dir,
]
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 = list(filter(lambda x: x.endswith('.wasm'), all_output_files))[0]
if outfile == None:
outfile = infile

shutil.copyfile(bindgen_out_dir + new_wasm_file, outfile)


intermediate_counter = 0


Expand Down
4 changes: 4 additions & 0 deletions tools/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
CACHE = None
PORTS = None
COMPILER_WRAPPER = None
WASM_BINDGEN = None

# Set by init()
EM_CONFIG = None
Expand Down Expand Up @@ -61,6 +62,7 @@ def fix_js_engine(old, new):
def normalize_config_settings():
global CACHE, PORTS, LLVM_ADD_VERSION, CLANG_ADD_VERSION, CLOSURE_COMPILER
global NODE_JS, NODE_JS_TEST, V8_ENGINE, JS_ENGINES, SPIDERMONKEY_ENGINE, WASM_ENGINES
global WASM_BINDGEN

# EM_CONFIG stuff
if not JS_ENGINES:
Expand All @@ -73,6 +75,7 @@ def normalize_config_settings():
JS_ENGINES = [listify(engine) for engine in JS_ENGINES]
WASM_ENGINES = [listify(engine) for engine in WASM_ENGINES]
CLOSURE_COMPILER = listify(CLOSURE_COMPILER)
WASM_BINDGEN = listify(WASM_BINDGEN)
if not CACHE:
CACHE = path_from_root('cache')
if not PORTS:
Expand Down Expand Up @@ -122,6 +125,7 @@ def parse_config_file():
'CACHE',
'PORTS',
'COMPILER_WRAPPER',
'WASM_BINDGEN',
)

# Only propagate certain settings from the config file.
Expand Down
11 changes: 9 additions & 2 deletions tools/emscripten.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,8 @@ def finalize_wasm(infile, outfile, js_syms):
if not need_name_section:
strip_sections += ['name']

if strip_sections or not settings.GENERATE_DWARF:
# TODO(walkingeyerobot): make this work. it appears to not like the wasm file from wasm-bindgen
if not settings.WASM_BINDGEN and (strip_sections or not settings.GENERATE_DWARF):
building.save_intermediate(outfile, 'strip.wasm')
building.strip(infile, outfile, debug=not settings.GENERATE_DWARF,
sections=strip_sections)
Expand Down Expand Up @@ -643,7 +644,7 @@ def create_tsd_exported_runtime_methods(metadata):
return utils.read_file(tsc_output_file)


def create_tsd(metadata, embind_tsd):
def create_tsd(metadata, embind_tsd, bindgen_tsd = None):
out = '// TypeScript bindings for emscripten-generated code. Automatically generated at compile time.\n'
if settings.EXPORTED_RUNTIME_METHODS:
out += create_tsd_exported_runtime_methods(metadata)
Expand Down Expand Up @@ -673,6 +674,12 @@ def create_tsd(metadata, embind_tsd):
# Add in embind definitions.
if embind_tsd:
export_interfaces += ' & EmbindModule'
if settings.WASM_BINDGEN and bindgen_tsd:
for file_path in bindgen_tsd:
with open(file_path, 'r') as file:
for line in file:
out += f'{line}'
export_interfaces += ' & BindgenModule'
out += f'export type MainModule = {export_interfaces};\n'
if settings.MODULARIZE:
return_type = 'MainModule'
Expand Down
22 changes: 17 additions & 5 deletions tools/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from .shared import in_temp, safe_copy, do_replace
from .shared import DEBUG, WINDOWS, DYLIB_EXTENSIONS
from .shared import unsuffixed, unsuffixed_basename, get_file_suffix
from .shared import get_emscripten_temp_dir
from .settings import settings, default_setting, user_settings, JS_ONLY_SETTINGS, DEPRECATED_SETTINGS
from .minimal_runtime_shell import generate_minimal_runtime_html

Expand Down Expand Up @@ -1900,7 +1901,7 @@ def phase_calculate_system_libraries(options):


@ToolchainProfiler.profile_block('link')
def phase_link(linker_args, wasm_target, js_syms):
def phase_link(linker_args, linker_inputs, wasm_target, js_syms):
logger.debug(f'linking: {linker_args}')

# Make a final pass over settings.EXPORTED_FUNCTIONS to remove any
Expand All @@ -1922,11 +1923,12 @@ def phase_link(linker_args, 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)
building.link_lld(linker_args, wasm_target, external_symbols=js_syms,
linker_inputs=linker_inputs)
settings.LINKABLE = True
rtn = extract_metadata.extract_metadata(wasm_target)

building.link_lld(linker_args, wasm_target, external_symbols=js_syms)
building.link_lld(linker_args, wasm_target, external_symbols=js_syms, linker_inputs=linker_inputs)
return rtn


Expand All @@ -1948,6 +1950,10 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat

settings.TARGET_JS_NAME = os.path.basename(js_target)

if settings.WASM_BINDGEN:
building.run_wasm_bindgen(in_wasm)
settings.JS_LIBRARIES += [get_emscripten_temp_dir() + '/bindgen_out/library_bindgen.js']

metadata = phase_emscript(in_wasm, wasm_target, js_syms, base_metadata)

if settings.EMBIND_AOT:
Expand Down Expand Up @@ -2062,7 +2068,10 @@ def phase_emit_tsd(options, wasm_target, js_target, js_syms, metadata):
embind_tsd = ''
if settings.EMBIND:
embind_tsd = run_embind_gen(options, wasm_target, js_syms, {'EMBIND_AOT': False})
all_tsd = emscripten.create_tsd(metadata, embind_tsd)
bindgen_ts_files = glob.glob(get_emscripten_temp_dir() + "/bindgen_out/*.d.ts", recursive=False)
# This list comprehension then filters out any files that end with .wasm.d.ts.
bindgen_ts_files = [file for file in bindgen_ts_files if not file.endswith('.wasm.d.ts')]
all_tsd = emscripten.create_tsd(metadata, embind_tsd, bindgen_ts_files)
out_file = os.path.join(os.path.dirname(js_target), filename)
write_file(out_file, all_tsd)

Expand Down Expand Up @@ -3060,6 +3069,9 @@ def run(options, linker_args):
if not linker_args:
exit_with_error('no input files')

linker_inputs = [f.value for f in linker_args if f.is_file]
linker_args = [f.value for f in linker_args]

if options.output_file and options.output_file.startswith('-'):
exit_with_error(f'invalid output filename: `{options.output_file}`')

Expand Down Expand Up @@ -3109,7 +3121,7 @@ def add_js_deps(sym):
settings.ASYNCIFY_IMPORTS_EXCEPT_JS_LIBS = settings.ASYNCIFY_IMPORTS[:]
settings.ASYNCIFY_IMPORTS += ['*.' + x for x in js_info['asyncFuncs']]

base_metadata = phase_link(linker_args, wasm_target, js_syms)
base_metadata = phase_link(linker_args, linker_inputs, wasm_target, js_syms)

# Special handling for when the user passed '-Wl,--version'. In this case the linker
# does not create the output file, but just prints its version and exits with 0.
Expand Down
Loading