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)