From 7cd0bf58d8dd5217e9ee016f98a426b9f89c932d Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Wed, 8 Oct 2025 15:10:52 -0700 Subject: [PATCH] Export mutable wasm globals as JS objects. NFC For immutable wasm globals we currently export them as simple JS numbers at build time. This is because data symbol addresses are exported as immutable globals. However, its also useful to export real wasm globals. For now we treat mutable globals and immutable global differently here but we should look into perhaps changing that in the future. --- ChangeLog.md | 4 ++ src/lib/libdylink.js | 61 +++++++++++++------ src/settings_internal.js | 4 +- test/codesize/test_codesize_hello_dylink.json | 8 +-- .../test_codesize_hello_dylink_all.json | 4 +- test/core/test_wasm_global.c | 43 +++++++++++++ test/core/test_wasm_global.out | 4 ++ test/test_core.py | 13 ++++ tools/building.py | 2 +- tools/emscripten.py | 16 ++--- tools/extract_metadata.py | 25 +++++--- 11 files changed, 140 insertions(+), 44 deletions(-) create mode 100644 test/core/test_wasm_global.c create mode 100644 test/core/test_wasm_global.out diff --git a/ChangeLog.md b/ChangeLog.md index b968afb3e0c2c..5f4c1d91f3d0e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,10 @@ See docs/process.md for more on how version tagging works. 4.0.17 (in development) ----------------------- +- Mutable Wasm globals can now be exported from native code. Currently these + cannot be declared in C/C++ but can be defined and exported in assembly code. + This currently only works for mutable globals since immutables are already + (and continue to be) exported as plain JS numbers. (#25530) - Minimum Firefox version was bumped up to Firefox 68 ESR, since older Firefox versions are not able to run the parallel browser harness: (#25493) - Firefox: v65 -> v68 diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index 11e2a854e0510..221e9e1d5fd4c 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -230,24 +230,33 @@ var LibraryDylink = { } #endif - GOT[symName] ||= new WebAssembly.Global({'value': '{{{ POINTER_WASM_TYPE }}}', 'mutable': true}); - if (replace || GOT[symName].value == 0) { + + var existingEntry = GOT[symName] && GOT[symName].value != 0; + if (replace || !existingEntry) { #if DYLINK_DEBUG == 2 dbg(`updateGOT: before: ${symName} : ${GOT[symName].value}`); #endif + var newValue; if (typeof value == 'function') { - GOT[symName].value = {{{ to64('addFunction(value)') }}}; + newValue = {{{ to64('addFunction(value)') }}}; #if DYLINK_DEBUG == 2 dbg(`updateGOT: FUNC: ${symName} : ${GOT[symName].value}`); #endif } else if (typeof value == {{{ POINTER_JS_TYPE }}}) { - GOT[symName].value = value; + newValue = value; } else { - err(`unhandled export type for '${symName}': ${typeof value}`); + // The GOT can only contain addresses (i.e data addresses or function + // addresses so we currently ignore other types export here. +#if DYLINK_DEBUG + dbg(`updateGOT: ignoring ${symName} due to its type: ${typeof value}`); +#endif + continue; } #if DYLINK_DEBUG == 2 - dbg(`updateGOT: after: ${symName} : ${GOT[symName].value} (${value})`); + dbg(`updateGOT: after: ${symName} : ${newValue} (${value})`); #endif + GOT[symName] ||= new WebAssembly.Global({'value': '{{{ POINTER_WASM_TYPE }}}', 'mutable': true}); + GOT[symName].value = newValue; } #if DYLINK_DEBUG else if (GOT[symName].value != value) { @@ -260,29 +269,43 @@ var LibraryDylink = { #endif }, + $isImmutableGlobal__internal: true, + $isImmutableGlobal: (val) => { + if (val instanceof WebAssembly.Global) { + try { + val.value = val.value; + } catch { + return true; + } + } + return false; + }, + // Applies relocations to exported things. $relocateExports__internal: true, - $relocateExports__deps: ['$updateGOT'], + $relocateExports__deps: ['$updateGOT', '$isImmutableGlobal'], $relocateExports__docs: '/** @param {boolean=} replace */', $relocateExports: (exports, memoryBase, replace) => { - var relocated = {}; - - for (var e in exports) { - var value = exports[e]; + function relocateExport(name, value) { #if SPLIT_MODULE // Do not modify exports synthesized by wasm-split - if (e.startsWith('%')) { - relocated[e] = value - continue; + if (name.startsWith('%')) { + return value; } #endif - // Detect wasm global exports. These represent data addresses + // Detect immuable wasm global exports. These represent data addresses // which are relative to `memoryBase` - if (value instanceof WebAssembly.Global) { - value = value.value; - value += {{{ to64('memoryBase') }}}; + if (isImmutableGlobal(value)) { + return value.value + {{{ to64('memoryBase') }}}; } - relocated[e] = value; + + // Return unmodified value (no relocation required). + return value; + } + + var relocated = {}; + for (var e in exports) { + relocated[e] = relocateExport(e, exports[e]) } updateGOT(relocated, replace); return relocated; diff --git a/src/settings_internal.js b/src/settings_internal.js index 78d2d797d7a63..ae89b4b194730 100644 --- a/src/settings_internal.js +++ b/src/settings_internal.js @@ -16,8 +16,8 @@ // underscore. var WASM_EXPORTS = []; -// Similar to above but only includes the global/data symbols. -var WASM_GLOBAL_EXPORTS = []; +// Similar to above but only includes the data symbols (address exports). +var DATA_EXPORTS = []; // An array of all symbols exported from all the side modules specified on the // command line. diff --git a/test/codesize/test_codesize_hello_dylink.json b/test/codesize/test_codesize_hello_dylink.json index 2019f35e71567..6fcb6be251727 100644 --- a/test/codesize/test_codesize_hello_dylink.json +++ b/test/codesize/test_codesize_hello_dylink.json @@ -1,10 +1,10 @@ { - "a.out.js": 26912, - "a.out.js.gz": 11470, + "a.out.js": 26919, + "a.out.js.gz": 11469, "a.out.nodebug.wasm": 18567, "a.out.nodebug.wasm.gz": 9199, - "total": 45479, - "total_gz": 20669, + "total": 45486, + "total_gz": 20668, "sent": [ "__heap_base", "__indirect_function_table", diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 4a915b0e5af66..52fa7287b0211 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": 245822, + "a.out.js": 245829, "a.out.nodebug.wasm": 597746, - "total": 843568, + "total": 843575, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/core/test_wasm_global.c b/test/core/test_wasm_global.c new file mode 100644 index 0000000000000..f1b57e9856c2c --- /dev/null +++ b/test/core/test_wasm_global.c @@ -0,0 +1,43 @@ +#include +#include +#include + +__asm__( +".section .data.my_global,\"\",@\n" +".globl my_global\n" +".globaltype my_global, i32\n" +"my_global:\n" +); + +int get_global() { + int val; + // Without volatile here this test fails in O1 and above. + __asm__ volatile ("global.get my_global\n" + "local.set %0\n" : "=r" (val)); + return val; +} + +void set_global(int val) { + __asm__("local.get %0\n" + "global.set my_global\n" : : "r" (val)); +} + +int main() { + printf("in main: %d\n", get_global()); + set_global(42); + printf("new value: %d\n", get_global()); + EM_ASM({ + // With the ESM integration, the Wasm global be exported as a regular + // number. Otherwise it will be a WebAssembly.Global object. +#ifdef ESM_INTEGRATION + assert(typeof _my_global == 'number', typeof _my_global); + out('from js:', _my_global); + _my_global += 1 +#else + assert(typeof _my_global == 'object', typeof _my_global); + out('from js:', _my_global.value); + _my_global.value += 1 +#endif + }); + printf("done: %d\n", get_global()); +} diff --git a/test/core/test_wasm_global.out b/test/core/test_wasm_global.out new file mode 100644 index 0000000000000..694715d642acf --- /dev/null +++ b/test/core/test_wasm_global.out @@ -0,0 +1,4 @@ +in main: 0 +new value: 42 +from js: 42 +done: 43 diff --git a/test/test_core.py b/test/test_core.py index 0c9611728978a..6f838cb3575dd 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -9677,6 +9677,19 @@ def test_externref_emjs(self, dynlink): self.set_setting('MAIN_MODULE', 2) self.do_core_test('test_externref_emjs.c') + @parameterized({ + '': [False], + 'dylink': [True], + }) + @no_esm_integration('https://github.com/emscripten-core/emscripten/issues/25543') + def test_wasm_global(self, dynlink): + if dynlink: + self.check_dylink() + self.set_setting('MAIN_MODULE', 2) + if self.get_setting('WASM_ESM_INTEGRATION'): + self.cflags.append('-DESM_INTEGRATION') + self.do_core_test('test_wasm_global.c', cflags=['-sEXPORTED_FUNCTIONS=_main,_my_global']) + def test_syscall_intercept(self): self.do_core_test('test_syscall_intercept.c') diff --git a/tools/building.py b/tools/building.py index 3c95993c84c56..7d52b016d4f0d 100644 --- a/tools/building.py +++ b/tools/building.py @@ -810,7 +810,7 @@ def metadce(js_file, wasm_file, debug_info, last): exports = settings.WASM_EXPORTS else: # Ignore exported wasm globals. Those get inlined directly into the JS code. - exports = sorted(set(settings.WASM_EXPORTS) - set(settings.WASM_GLOBAL_EXPORTS)) + exports = sorted(set(settings.WASM_EXPORTS) - set(settings.DATA_EXPORTS)) extra_info = '{ "exports": [' + ','.join(f'["{asmjs_mangle(x)}", "{x}"]' for x in exports) + ']}' diff --git a/tools/emscripten.py b/tools/emscripten.py index 4533ac5fe40ce..47faa276d6e8a 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -111,7 +111,7 @@ def update_settings_glue(wasm_file, metadata, base_metadata): settings.WASM_EXPORTS = base_metadata.all_exports else: settings.WASM_EXPORTS = metadata.all_exports - settings.WASM_GLOBAL_EXPORTS = list(metadata.global_exports.keys()) + settings.DATA_EXPORTS = list(metadata.data_exports.keys()) settings.HAVE_EM_ASM = bool(settings.MAIN_MODULE or len(metadata.em_asm_consts) != 0) # start with the MVP features, and add any detected features. @@ -269,9 +269,9 @@ def trim_asm_const_body(body): return body -def create_global_exports(global_exports): +def create_data_exports(data_exports): lines = [] - for k, v in global_exports.items(): + for k, v in data_exports.items(): if shared.is_internal_global(k): continue @@ -408,11 +408,11 @@ def emscript(in_wasm, out_wasm, outfile_js, js_syms, finalize=True, base_metadat other_exports = base_metadata.other_exports # We want the real values from the final metadata but we only want to # include names from the base_metadata. See phase_link() in link.py. - global_exports = {k: v for k, v in metadata.global_exports.items() if k in base_metadata.global_exports} + data_exports = {k: v for k, v in metadata.data_exports.items() if k in base_metadata.data_exports} else: function_exports = metadata.function_exports other_exports = metadata.other_exports - global_exports = metadata.global_exports + data_exports = metadata.data_exports if settings.ASYNCIFY == 1: function_exports['asyncify_start_unwind'] = webassembly.FuncType([webassembly.Type.I32], []) @@ -421,7 +421,7 @@ def emscript(in_wasm, out_wasm, outfile_js, js_syms, finalize=True, base_metadat function_exports['asyncify_stop_rewind'] = webassembly.FuncType([], []) parts = [pre] - parts += create_module(metadata, function_exports, global_exports, other_exports, + parts += create_module(metadata, function_exports, data_exports, other_exports, forwarded_json['librarySymbols'], forwarded_json['nativeAliases']) parts.append(post) settings.ALIASES = list(forwarded_json['nativeAliases'].keys()) @@ -1008,10 +1008,10 @@ def create_receiving(function_exports, other_exports, library_symbols, aliases): return '\n'.join(receiving) -def create_module(metadata, function_exports, global_exports, other_exports, library_symbols, aliases): +def create_module(metadata, function_exports, data_exports, other_exports, library_symbols, aliases): module = [] module.append(create_receiving(function_exports, other_exports, library_symbols, aliases)) - module.append(create_global_exports(global_exports)) + module.append(create_data_exports(data_exports)) sending = create_sending(metadata, library_symbols) if settings.WASM_ESM_INTEGRATION: diff --git a/tools/extract_metadata.py b/tools/extract_metadata.py index c0e799c89abd5..de2ccbf9ef7fc 100644 --- a/tools/extract_metadata.py +++ b/tools/extract_metadata.py @@ -240,13 +240,16 @@ def get_main_reads_params(module, export_map): return True -def get_global_exports(module, exports): - global_exports = {} +def get_data_exports(module, exports): + data_exports = {} for export in exports: if export.kind == webassembly.ExternType.GLOBAL: g = module.get_global(export.index) - global_exports[export.name] = str(get_global_value(g)) - return global_exports + # Data symbols (addresses) are exported as immutable Wasm globals. + # mutable globals are handled via get_other_exports + if not g.mutable: + data_exports[export.name] = str(get_global_value(g)) + return data_exports def get_function_exports(module): @@ -260,8 +263,14 @@ def get_function_exports(module): def get_other_exports(module): rtn = [] for e in module.get_exports(): - if e.kind not in (webassembly.ExternType.FUNC, webassembly.ExternType.GLOBAL): - rtn.append(e.name) + if e.kind == webassembly.ExternType.FUNC: + continue + if e.kind == webassembly.ExternType.GLOBAL: + g = module.get_global(e.index) + # Immutable globals are handled specially. See get_data_exports + if not g.mutable: + continue + rtn.append(e.name) return rtn @@ -311,7 +320,7 @@ class Metadata: features: List[str] invoke_funcs: List[str] main_reads_params: bool - global_exports: Dict[str, str] + data_exports: Dict[str, str] function_exports: Dict[str, webassembly.FuncType] other_exports: List[str] all_exports: List[str] @@ -348,7 +357,7 @@ def extract_metadata(filename): metadata.em_js_funcs = em_js_funcs metadata.features = features metadata.main_reads_params = get_main_reads_params(module, export_map) - metadata.global_exports = get_global_exports(module, exports) + metadata.data_exports = get_data_exports(module, exports) read_module_imports(module, metadata)