From d1c4c9d5ec36c6a4c06075539ad5e2e596d6d1f7 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 28 Jul 2026 14:06:08 -0700 Subject: [PATCH 1/2] Support JS libraries self-registering their exports via EXPORTED_FUNCTIONS JS library code can already mutate the compile-time EXPORTED_FUNCTIONS set at library load time, which under MODULARIZE=instance causes jsifier to emit the symbol with an `export` declaration. This makes that flow fully work by forwarding the final EXPORTED_FUNCTIONS set back from the JS compiler so the linker can derive which JS library symbols were exported, and have the WASM_ESM_INTEGRATION wrapper re-export them. This is used by binding layers (e.g. wasm-bindgen) that define a public JS API surface distinct from the wasm export names, registering it from their generated JS library. --- src/jsifier.mjs | 4 ++++ test/test_jslib.py | 31 ++++++++++++++++++++++++++++++- tools/building.py | 4 ++++ tools/emscripten.py | 8 ++++++++ tools/link.py | 4 ++++ 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/jsifier.mjs b/src/jsifier.mjs index 77a534648bb78..1f5bcd8335262 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -973,6 +973,10 @@ var proxiedFunctionTable = [ '//FORWARDED_DATA:' + JSON.stringify({ librarySymbols, + // The final EXPORTED_FUNCTIONS set, including any additions made by + // JS libraries at load time, so the caller can re-derive which + // library symbols were exported. + exportedFunctions: Array.from(EXPORTED_FUNCTIONS), nativeAliases, warnings: warningOccured(), asyncFuncs, diff --git a/test/test_jslib.py b/test/test_jslib.py index e8d26e1618087..aae4f7ec76053 100644 --- a/test/test_jslib.py +++ b/test/test_jslib.py @@ -6,7 +6,7 @@ from subprocess import PIPE from common import RunnerCore, copy_asset, create_file, read_file, test_file -from decorators import also_with_wasm64, also_without_bigint, parameterized +from decorators import also_with_wasm64, also_without_bigint, parameterized, requires_node_25 from tools.shared import EMCC from tools.utils import delete_file @@ -164,6 +164,35 @@ def test_jslib_exported(self): self.do_runf('src.c', 'c calling: 12\njs calling: 10.', cflags=['--js-library', 'lib.js', '-sEXPORTED_FUNCTIONS=_main,_jslibfunc']) + @parameterized({ + '': ([],), + 'esm_integration': (['-sWASM_ESM_INTEGRATION'],), + }) + @requires_node_25 + def test_jslib_self_export(self, args): + # A JS library can add its own symbols to EXPORTED_FUNCTIONS at load time + # (e.g. a binding layer registering the public API it defines), making them + # ES module exports under MODULARIZE=instance without the user needing to + # list them on the command line. + self.node_args += ['--no-warnings'] + create_file('lib.js', '''\ +EXPORTED_FUNCTIONS.add('libExport'); +addToLibrary({ + $libExport: () => 42, +}); +''') + create_file('main.c', 'int main() { return 0; }') + self.run_process([EMCC, 'main.c', '-sMODULARIZE=instance', '-Wno-experimental', + '--js-library', 'lib.js', '-o', 'mod.mjs'] + args + self.get_cflags()) + create_file('runner.mjs', ''' + import { strict as assert } from 'assert'; + import init, { libExport } from './mod.mjs'; + await init(); + assert(libExport() == 42); + console.log('ok'); + ''') + self.assertContained('ok', self.run_js('runner.mjs')) + def test_jslib_using_asm_lib(self): create_file('lib.js', r''' addToLibrary({ diff --git a/tools/building.py b/tools/building.py index ffc77781f965b..2edcc3e2dabbb 100644 --- a/tools/building.py +++ b/tools/building.py @@ -57,6 +57,10 @@ _is_ar_cache: dict[str, bool] = {} # the exports the user requested user_requested_exports: set[str] = set() +# JS library symbols that ended up in EXPORTED_FUNCTIONS (including additions +# made by JS libraries themselves at load time), derived from the JS compiler's +# forwarded data; the WASM_ESM_INTEGRATION wrapper re-exports them. +exported_js_library_symbols: 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`. diff --git a/tools/emscripten.py b/tools/emscripten.py index 54adaa8aa8756..efaead9bf4d3b 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -446,6 +446,14 @@ def emscript(in_wasm, out_wasm, outfile_js, js_syms, finalize=True, base_metadat report_missing_exports(forwarded_json['librarySymbols']) + # A JS library symbol is exported (MODULARIZE=instance) when it is in + # EXPORTED_FUNCTIONS; derive that set rather than tracking it separately. The + # forwarded EXPORTED_FUNCTIONS includes additions made by JS libraries + # themselves at load time. + exported_functions = set(forwarded_json['exportedFunctions']) + building.exported_js_library_symbols.update( + s for s in forwarded_json['librarySymbols'] if s in exported_functions) + asm_const_pairs = ['%s: %s' % (key, value) for key, value in asm_consts] if asm_const_pairs or settings.MAIN_MODULE: pre += 'var ASM_CONSTS = {\n ' + ', \n '.join(asm_const_pairs) + '\n};\n' diff --git a/tools/link.py b/tools/link.py index c287e1311c2e6..c7e2360503c6c 100644 --- a/tools/link.py +++ b/tools/link.py @@ -2191,6 +2191,10 @@ def node_detection_code(): def create_esm_wrapper(wrapper_file, support_target, wasm_target): js_exports = building.user_requested_exports.union(settings.EXPORTED_RUNTIME_METHODS) + # JS library symbols the support module exports at declaration (including + # any the libraries themselves added to EXPORTED_FUNCTIONS at load time); + # the wrapper must forward these too. + js_exports |= building.exported_js_library_symbols js_exports = ', '.join(sorted(js_exports)) wrapper = [] From 253216db7f8c4d62b49e46aa3bd3e76a7a8ff731 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 28 Jul 2026 15:28:16 -0700 Subject: [PATCH 2/2] Retrigger CI