Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------
Expand Down
6 changes: 5 additions & 1 deletion src/lib/libcore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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_')) {
Expand Down
8 changes: 4 additions & 4 deletions test/codesize/test_codesize_minimal_64.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
41 changes: 41 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <stdint.h>
#include <emscripten.h>

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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we already have tests for this elsewhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wrapping itself is covered (core_2gb, wasm64), but I couldn't find coverage for the two paths this PR changes, and the test suites agree. With the acorn-optimizer.mjs change reverted, all 2374 tests in wasm64_O2 + core_2gb give the same results as with it; only the O3 variant here fails. Nothing else combines wrapping with DECLARE_ASM_MODULE_EXPORTS=0, so the no_declare_asm_module_exports variant is the only thing that catches the libcore.js change. If you'd rather not have a new test, I could instead add 4GB variants to test_no_declare_asm_module_exports and to an optimized test that minifies export names. Let me know which you prefer.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not to concerned about adding a new test here if it makes sense.

We do have several tests that use -sDECLARE_ASM_MODULE_EXPORTS=0 in test_core.py.. maybe tho issue is that we don't have a wasm64_O3 configuration? Should we add one maybe? (Possibly in addition to this new test).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, and I was wrong earlier: test_no_declare_asm_module_exports does catch the libcore.js change (it fails under wasm64, wasm64_4gb and core_2gb without it). I've removed that variant from the new test in ffe1224.

A wasm64_O3 config would catch the acorn change. Export names are only minified at -O3, so wasm64_O2 can't see it. I tried it locally: 841 of its 1187 tests fail with the acorn change reverted. With this PR, 12 fail, and the same 12 fail under wasm64_O2 on main for me.

I can add wasm64_O3 in a follow-up PR, if that sounds good.


def test_unistd_strerror(self):
self.do_runf_out_file('unistd/strerror.c')

Expand Down
3 changes: 2 additions & 1 deletion tools/acorn-optimizer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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')
);
}

Expand Down
16 changes: 11 additions & 5 deletions tools/emscripten.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 = {};'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about wasmExports = Object.create(origExports); (then we don't need the setPrototypeOf line?)

@rustyconover rustyconover Oct 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.create(origExports) doesn't work here because instance.exports is frozen: its properties are non-writable, so assigning a wrapper to an inherited name on the new object fails silently (or throws in strict mode) instead of shadowing it. More detail in #27996 (comment).

I've updated the PR description to serve as the commit message. If you're happy with this, is it OK to merge?

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
Loading