Skip to content
Open
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
100 changes: 95 additions & 5 deletions src/lib/libdylink.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>=} mutableGlobals */',
$relocateExports: (exports, memoryBase, mutableGlobals) => {
#if DYLINK_DEBUG
dbg(`relocateExports memoryBase=${memoryBase} count=${Object.keys(exports).length}`);
#endif
Expand All @@ -293,8 +294,12 @@ 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`. `mutableGlobals`, when known,
// saves probing each global.
var immutable = mutableGlobals
? value instanceof WebAssembly.Global && !mutableGlobals.has(name)

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.

Do we need value instanceof WebAssembly.Global here?

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.

Yes. exports also contains functions, memories and tables, while mutableGlobals only lists globals. Without the check, !mutableGlobals.has(name) would be true for a function export, and it would be relocated as if it were a data address. isImmutableGlobal makes the same instanceof check first.

: isImmutableGlobal(value);
if (immutable) {
return new WebAssembly.Global({'value': '{{{ POINTER_WASM_TYPE }}}'}, value.value + {{{ to64('memoryBase') }}});
}

Expand Down Expand Up @@ -476,7 +481,89 @@ var LibraryDylink = {
if (condition) throw new Error(message);
}

if (binary instanceof WebAssembly.Module) {
// 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() {

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.

This function seems long enough that we should perhaps split it out into its own library function?

Also since it itself include more sub-functions.

But I guess since the subfunctions also close over other helper like getU8 we might need a bigger refactor here?

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.

It closes over getDylinkMetadata's reader (binary, offset, getU8, getLEB), so splitting it out means moving that reader out too. I'd rather do that as a follow-up that factors the section reader out of getDylinkMetadata, so this PR stays focused, but I'm happy to do it here if you prefer.

// 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;
}

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]);
Expand Down Expand Up @@ -562,6 +649,9 @@ var LibraryDylink = {
dbg('dylink needed:', customSection.neededDynlibs);
#endif

if (!isModule) {
customSection.mutableGlobals = getMutableGlobals();
}
return customSection;
},

Expand Down Expand Up @@ -885,7 +975,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);
Expand Down
8 changes: 4 additions & 4 deletions test/codesize/test_codesize_hello_dylink.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions test/codesize/test_codesize_hello_dylink_all.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
54 changes: 54 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -7314,6 +7314,60 @@ 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'])

def test_dlopen_exported_globals(self):
# 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 <assert.h>
#include <dlfcn.h>
#include <stdio.h>
#include <emscripten.h>

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({
// 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')));
assert(metadata.mutableGlobals, 'mutableGlobals not parsed');
assert(metadata.mutableGlobals.size == 1 && metadata.mutableGlobals.has('side_global'));

var g = LDSO.loadedLibsByHandle[$0].exports['side_global'];
assert(g instanceof WebAssembly.Global);
g.value = 7;
}, h);
assert(get_side_global() == 7);
printf("done\n");
return 0;
}''')
self.run_process([EMCC, 'side.c', '-sSIDE_MODULE', '-o', 'libside.wasm'])
self.do_runf('main.c', 'done\n', cflags=['-sMAIN_MODULE=2'])

@requires_node
def test_dlopen_binary_not_retained(self):
# The bytes of a side module should not be kept alive once it has been
Expand Down
Loading