diff --git a/ChangeLog.md b/ChangeLog.md index 436ab85c022ec..84475cb18373c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -26,6 +26,13 @@ See docs/process.md for more on how version tagging works. - The undocumented `FS.filesystems` object was removed. Replace things like `FS.filesystems['MEMFS']` with plain `MEMFS`. (#27940) - libpng port updated from 1.6.58 to 1.6.59. (#27956) +- When exports are wrapped for pointer conversions (`MEMORY64`, or + `MAXIMUM_MEMORY` over 2GB), the `wasmExports` object no longer contains a + copy of every export. Only the wrapped exports are its own properties and + the rest are inherited from the original `WebAssembly.Instance` exports. + This speeds up startup for programs with many exports (e.g. `MAIN_MODULE`). + Code that enumerates `wasmExports` should use `for...in` rather than + `Object.keys`/`Object.entries`. (#27996) 6.0.12 - 10/08/26 ----------------- diff --git a/src/lib/libcore.js b/src/lib/libcore.js index d280836d8a75b..ed8506a246d2c 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -1765,7 +1765,11 @@ addToLibrary({ #endif ], $assignWasmExports: (wasmExports) => { - for (var [name, exportedSymbol] of Object.entries(wasmExports)) { + // Use `for...in` rather than `Object.entries` here since, after + // `applySignatureConversions`, some exports are inherited rather than own + // properties. + for (var name in wasmExports) { + var exportedSymbol = wasmExports[name]; name = asmjsMangle(name); #if DYNCALLS || !WASM_BIGINT if (name.startsWith('dynCall_')) { diff --git a/test/codesize/test_codesize_minimal_64.json b/test/codesize/test_codesize_minimal_64.json index 77cbcc551baba..303adda359c96 100644 --- a/test/codesize/test_codesize_minimal_64.json +++ b/test/codesize/test_codesize_minimal_64.json @@ -1,10 +1,10 @@ { - "a.out.js": 2367, - "a.out.js.gz": 1157, + "a.out.js": 2381, + "a.out.js.gz": 1169, "a.out.nodebug.wasm": 75, "a.out.nodebug.wasm.gz": 88, - "total": 2442, - "total_gz": 1245, + "total": 2456, + "total_gz": 1257, "sent": [], "imports": [], "exports": [ diff --git a/test/test_other.py b/test/test_other.py index 951bc3a077587..02e78bba36aec 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -13986,6 +13986,47 @@ def test_wasmfs_before_preload(self): def test_hello_world_above_2gb(self): self.do_runf_out_file('hello_world.c', cflags=['-sGLOBAL_BASE=2GB', '-sINITIAL_MEMORY=3GB']) + @parameterized({ + '': ([],), + # Minified export names + 'O3': (['-O3'],), + }) + def test_signature_conversions(self, args): + # Exports returning pointers get wrapped so that they return unsigned + # values when memory can be above 2GB. Check that the wrapped exports are + # used, and that all other exports are still present on `wasmExports` + # (including when enumerated). + create_file('main.c', r''' + #include + #include + + EMSCRIPTEN_KEEPALIVE void* get_high_ptr() { return (void*)(uintptr_t)0x90000000u; } + EMSCRIPTEN_KEEPALIVE int add_one(int x) { return x + 1; } + + int main() { + EM_ASM({ + out('_get_high_ptr: ' + _get_high_ptr()); + out('wasmExports.get_high_ptr: ' + wasmExports['get_high_ptr']()); + out('_add_one: ' + _add_one(41)); + out('wasmExports.add_one: ' + wasmExports['add_one'](41)); + var found = 0; + for (var name in wasmExports) { + if (wasmExports[name] === wasmExports['get_high_ptr']) found |= 1; + if (wasmExports[name] === wasmExports['add_one']) found |= 2; + } + out('enumerated: ' + found); + }); + } + ''') + expected = '''\ +_get_high_ptr: 2415919104 +wasmExports.get_high_ptr: 2415919104 +_add_one: 42 +wasmExports.add_one: 42 +enumerated: 3 +''' + self.do_runf('main.c', expected, cflags=['-sALLOW_MEMORY_GROWTH', '-sMAXIMUM_MEMORY=4GB', '-sSIGNATURE_CONVERSIONS=get_high_ptr:p'] + args) + def test_unistd_strerror(self): self.do_runf_out_file('unistd/strerror.c') diff --git a/tools/acorn-optimizer.mjs b/tools/acorn-optimizer.mjs index 0b759c0cd93e7..b084a54adb378 100755 --- a/tools/acorn-optimizer.mjs +++ b/tools/acorn-optimizer.mjs @@ -464,11 +464,12 @@ function isExportSpecifierList(node) { function isExportUse(node) { // Match usages of symbols on the `wasmExports` object. e.g: // wasmExports['X'] + // `origExports` holds the unwrapped exports in applySignatureConversions. return ( node.type === 'MemberExpression' && node.object.type === 'Identifier' && isLiteralString(node.property) && - node.object.name === 'wasmExports' + (node.object.name === 'wasmExports' || node.object.name === 'origExports') ); } diff --git a/tools/emscripten.py b/tools/emscripten.py index 013f7d0cb16f9..fbda905a0923f 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -1165,8 +1165,6 @@ def create_pointer_conversion_wrappers(metadata): // that it is recognised by metadce and minify-import-export-names // passes. function applySignatureConversions(wasmExports) { - // First, make a copy of the incoming exports object - wasmExports = Object.assign({}, wasmExports); ''' sigs_seen = set() @@ -1195,9 +1193,17 @@ def create_pointer_conversion_wrappers(metadata): sigs_seen.add(sig) wrap_functions.append(symbol) - for f in wrap_functions: - sig = native_sigs[f] - wrappers += f"\n wasmExports['{f}'] = makeWrapper_{sig}(wasmExports['{f}']);" + if wrap_functions: + # Rather than copying the entire exports object (which is slow when there + # are many exports) we put just the wrapped exports on a new object, which + # then inherits all the others from the original. The wrappers can't be + # assigned to an object that already inherits from the original exports + # object, since that is frozen, so we set the prototype afterwards. + wrappers += '\n var origExports = wasmExports;\n wasmExports = {};' + for f in wrap_functions: + sig = native_sigs[f] + wrappers += f"\n wasmExports['{f}'] = makeWrapper_{sig}(origExports['{f}']);" + wrappers += '\n Object.setPrototypeOf(wasmExports, origExports);' wrappers += '\n return wasmExports;\n}' return wrappers