diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index f4fe51f2fc3d9..08cb02afd3594 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -280,7 +280,8 @@ var LibraryDylink = { // Applies relocations to exported things. $relocateExports__internal: true, $relocateExports__deps: ['$isImmutableGlobal'], - $relocateExports: (exports, memoryBase = 0) => { + $relocateExports__docs: '/** @param {Set=} mutableGlobals */', + $relocateExports: (exports, memoryBase, mutableGlobals) => { #if DYLINK_DEBUG dbg(`relocateExports memoryBase=${memoryBase} count=${Object.keys(exports).length}`); #endif @@ -293,8 +294,16 @@ var LibraryDylink = { } #endif // Detect immutable wasm global exports. These represent data addresses - // which are relative to `memoryBase` - if (isImmutableGlobal(value)) { + // which are relative to `memoryBase`. +#if SHRINK_LEVEL < 2 + // `mutableGlobals`, when known, saves probing each global. + var immutable = mutableGlobals + ? value instanceof WebAssembly.Global && !mutableGlobals.has(name) + : isImmutableGlobal(value); +#else + var immutable = isImmutableGlobal(value); +#endif + if (immutable) { return new WebAssembly.Global({'value': '{{{ POINTER_WASM_TYPE }}}'}, value.value + {{{ to64('memoryBase') }}}); } @@ -476,7 +485,91 @@ var LibraryDylink = { if (condition) throw new Error(message); } - if (binary instanceof WebAssembly.Module) { +#if SHRINK_LEVEL < 2 + // Returns the names of the exported globals that are mutable, read from + // the import, global and export sections that follow the dylink section. + // This saves relocateExports from probing each exported global with + // isImmutableGlobal, which throws for every immutable one. Returns + // undefined if it finds something it doesn't handle. + function getMutableGlobals() { + // The mutability of each global, imported globals first. + var mutable = []; + var result = new Set(); + + function skipString() { + var len = getLEB(); + offset += len; + } + + function skipValType() { + // (ref null? heaptype) has a heap type immediate. + var type = getU8(); + if (type === 0x63 || type === 0x64) getLEB(); + } + + function skipLimits() { + var flags = getU8(); + getLEB(); // min + if (flags & 1) getLEB(); // max + if (flags & 8) getLEB(); // page size (custom-page-sizes) + } + + while (offset < binary.length) { + var id = getU8(); + var size = getLEB(); + var sectionEnd = offset + size; + if (id === 2) { // import + for (var count = getLEB(); count--;) { + skipString(); + skipString(); + var kind = getU8(); + if (kind === 0) getLEB(); // function + else if (kind === 1) { skipValType(); skipLimits(); } // table + else if (kind === 2) skipLimits(); // memory + else if (kind === 3) { skipValType(); mutable.push(getU8() & 1); } // global + else if (kind === 4) { getU8(); getLEB(); } // tag + else return; + } + } else if (id === 6) { // global + for (var count = getLEB(); count--;) { + skipValType(); + mutable.push(getU8() & 1); + // Skip the constant init expression. + for (var op; (op = getU8()) !== 0x0b;) { + if (op === 0x43) offset += 4; // f32.const + else if (op === 0x44) offset += 8; // f64.const + // i32.const, i64.const, global.get, ref.null, ref.func + else if (op === 0x41 || op === 0x42 || op === 0x23 || op === 0xd0 || op === 0xd2) getLEB(); + // Anything but the extended-const add, sub and mul instructions + else if (!(op >= 0x6a && op <= 0x6c || op >= 0x7c && op <= 0x7e)) return; + } + } + } else if (id === 7) { // export + for (var count = getLEB(); count--;) { + var nameStart = offset; + skipString(); + var kind = getU8(); + var index = getLEB(); + if (kind === 3 && mutable[index]) { + var saved = offset; + offset = nameStart; + result.add(getString()); + offset = saved; + } + } + // The export section is the last one we need. + return offset === sectionEnd ? result : undefined; + } else { + offset = sectionEnd; + } + if (offset !== sectionEnd) return; + } + return result; + } +#endif // SHRINK_LEVEL < 2 + + var isModule = binary instanceof WebAssembly.Module; + if (isModule) { var dylinkSection = WebAssembly.Module.customSections(binary, 'dylink.0'); failIf(!dylinkSection.length, 'need dylink section'); binary = new Uint8Array(dylinkSection[0]); @@ -562,6 +655,11 @@ var LibraryDylink = { dbg('dylink needed:', customSection.neededDynlibs); #endif +#if SHRINK_LEVEL < 2 + if (!isModule) { + customSection.mutableGlobals = getMutableGlobals(); + } +#endif return customSection; }, @@ -885,7 +983,7 @@ var LibraryDylink = { #endif // add new entries to functionsInTableMap updateTableMap(tableBase, metadata.tableSize); - moduleExports = relocateExports(instance.exports, memoryBase); + moduleExports = relocateExports(instance.exports, memoryBase, metadata.mutableGlobals); updateGOT(moduleExports); #if ASYNCIFY moduleExports = Asyncify.instrumentWasmExports(moduleExports); diff --git a/test/codesize/test_codesize_hello_dylink.json b/test/codesize/test_codesize_hello_dylink.json index 15ca5b6de0401..bfa1e7bc269db 100644 --- a/test/codesize/test_codesize_hello_dylink.json +++ b/test/codesize/test_codesize_hello_dylink.json @@ -1,10 +1,10 @@ { - "a.out.js": 26229, - "a.out.js.gz": 11137, + "a.out.js": 27061, + "a.out.js.gz": 11491, "a.out.nodebug.wasm": 16986, "a.out.nodebug.wasm.gz": 8667, - "total": 43215, - "total_gz": 19804, + "total": 44047, + "total_gz": 20158, "sent": [ "__syscall_stat64", "emscripten_resize_heap", diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 4d4e9e1e31f10..4977599c47c1a 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": 272704, + "a.out.js": 273543, "a.out.nodebug.wasm": 589443, - "total": 862147, + "total": 862986, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/test_other.py b/test/test_other.py index f4162deb73fad..935b5c2e824c1 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -7314,6 +7314,69 @@ def test_dlopen_constructors(self): # Repeat the test without NO_AUTOLOAD_DYLIBS self.do_runf('main.c', 'done\n', cflags=['-g', '-sMAIN_MODULE=2', 'libside.wasm']) + @parameterized({ + '': ([], True), + # At -Oz the loader skips parsing the binary and probes each global instead. + 'Oz': (['-Oz'], False), + }) + def test_dlopen_exported_globals(self, args, expect_parsed): + # A side module's data symbols are exported as immutable globals that must + # be relocated by the module's memory base, while an exported mutable + # global must be passed through as is. + create_file('side.c', r''' + __asm__( + ".section .data.side_global,\"\",@\n" + ".globl side_global\n" + ".globaltype side_global, i32\n" + "side_global:\n" + ); + + int side_data = 42; + int* side_data_addr() { return &side_data; } + + int get_side_global() { + int val; + __asm__ volatile("global.get side_global\n" + "local.set %0\n" : "=r" (val)); + return val; + } + ''') + create_file('main.c', r''' + #include + #include + #include + #include + + int main() { + void* h = dlopen("libside.wasm", RTLD_NOW); + assert(h); + int* side_data = dlsym(h, "side_data"); + int* (*side_data_addr)() = dlsym(h, "side_data_addr"); + int (*get_side_global)() = dlsym(h, "get_side_global"); + assert(side_data && side_data == side_data_addr()); + assert(*side_data == 42); + EM_ASM({ + // Unless optimizing for size, the loader read the mutable globals + // from the binary, rather than falling back to probing each one. + var metadata = getDylinkMetadata(new Uint8Array(require('fs').readFileSync('libside.wasm'))); + if ($1) { + if (!metadata.mutableGlobals) throw new Error('mutableGlobals not parsed'); + if (metadata.mutableGlobals.size != 1 || !metadata.mutableGlobals.has('side_global')) throw new Error('wrong mutableGlobals'); + } else if (metadata.mutableGlobals) { + throw new Error('mutableGlobals unexpectedly parsed'); + } + + var g = LDSO.loadedLibsByHandle[$0].exports['side_global']; + if (!(g instanceof WebAssembly.Global)) throw new Error('side_global is not a Global'); + g.value = 7; + }, h, EXPECT_PARSED); + assert(get_side_global() == 7); + printf("done\n"); + return 0; + }''') + self.run_process([EMCC, 'side.c', '-sSIDE_MODULE', '-o', 'libside.wasm'] + args) + self.do_runf('main.c', 'done\n', cflags=['-sMAIN_MODULE=2', f'-DEXPECT_PARSED={int(expect_parsed)}'] + args) + @requires_node def test_dlopen_binary_not_retained(self): # The bytes of a side module should not be kept alive once it has been