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
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ See docs/process.md for more on how version tagging works.

6.0.14 (in development)
-----------------------
- In `MAIN_MODULE` builds the GOT entries (and table slots) for the main
module's exports are now created on demand, when first needed by a side
module, rather than all at startup. This can significantly reduce startup
time for programs with a large number of exports. (#27993)
- When compiling and linking in a single command, auxiliary compiler output
files (such as `.dwo` files from `-gsplit-dwarf` or `.json` files from
`-ftime-trace`) are now named `<output>-<input>.<ext>`, matching the behavior
Expand Down
53 changes: 51 additions & 2 deletions src/lib/libdylink.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,12 @@ var LibraryDylink = {
// handler. We abuse the `target` of the Proxy in order to pass the set of
// weak imports to the handler.
$GOTHandler__internal: true,
$GOTHandler__deps: ['$GOT'],
$GOTHandler__deps: ['$GOT', '$addMainModuleGOTEntry'],
$GOTHandler: {
get(weakImports, symName) {
if (!GOT[symName]) {
addMainModuleGOTEntry(symName);
}
var rtn = GOT[symName];
if (!rtn) {
#if DYLINK_DEBUG == 2
Expand Down Expand Up @@ -210,8 +213,49 @@ var LibraryDylink = {
;
},

// The main module's (unrelocated) exports. Rather than creating GOT entries
// for all of these on startup, which is slow for programs with many exports
// since each exported function needs its own table slot, we create them on
// demand using `addMainModuleGOTEntry`.
$mainModuleExports: null,

// Called once the main module has been instantiated. Only the GOT entries
// that the main module itself imports exist at this point, so these are the
// only ones we need to fill in.
$initMainModuleGOT__internal: true,
$initMainModuleGOT__deps: ['$GOT', '$mainModuleExports', '$addMainModuleGOTEntry'],
$initMainModuleGOT: (exports) => {
mainModuleExports = exports;
for (var [symName, entry] of Object.entries(GOT)) {
if (entry.value == {{{ UNDEFINED_ADDR }}}) {
addMainModuleGOTEntry(symName);
}
}
},

// Create (or fill in) the GOT entry for `symName` using the main module's
// definition, if it has one. This is called before any other definition of
// a symbol is added to the GOT, which gives the entry exactly the value it
// would have had if we had created GOT entries for all of the main module's
// exports on startup. In particular this means that the main module's
// definition of a symbol takes precedence over any side module definition.
$addMainModuleGOTEntry__internal: true,
$addMainModuleGOTEntry__deps: ['$mainModuleExports', '$updateGOT'],
$addMainModuleGOTEntry: (symName) => {
#if !WASM_BIGINT
// Prefer the version of the symbol without i64 legalization (see updateGOT).
if (mainModuleExports?.['orig$' + symName]) {
symName = 'orig$' + symName;
}
#endif
var value = mainModuleExports?.[symName];
if (value) {
updateGOT({[symName]: value}, /*replace=*/true);
}
},

$updateGOT__internal: true,
$updateGOT__deps: ['$GOT', '$isInternalSym', '$addFunction'],
$updateGOT__deps: ['$GOT', '$isInternalSym', '$addFunction', '$addMainModuleGOTEntry'],
$updateGOT__docs: '/** @param {boolean=} replace */',
$updateGOT: (exports, replace) => {
#if DYLINK_DEBUG
Expand All @@ -230,6 +274,11 @@ var LibraryDylink = {
}
#endif

if (!replace && !GOT[symName]) {
// The main module's definition, if any, takes precedence.
addMainModuleGOTEntry(symName);
}

var existingEntry = GOT[symName] && GOT[symName].value != {{{ UNDEFINED_ADDR }}};
if (replace || !existingEntry) {
#if DYLINK_DEBUG == 2
Expand Down
2 changes: 1 addition & 1 deletion src/preamble.js
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ function getWasmImports() {
assignWasmExports(wasmExports);

#if MAIN_MODULE
updateGOT(origExports);
initMainModuleGOT(origExports);
#endif

#if EXPORTED_RUNTIME_METHODS.has('wasmExports')
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": 26379,
"a.out.js.gz": 11224,
"a.out.nodebug.wasm": 16986,
"a.out.nodebug.wasm.gz": 8667,
"total": 43215,
"total_gz": 19804,
"total": 43365,
"total_gz": 19891,
"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": 272855,
"a.out.nodebug.wasm": 589443,
"total": 862147,
"total": 862298,
"sent": [
"IMG_Init",
"IMG_Load",
Expand Down
101 changes: 101 additions & 0 deletions test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5261,6 +5261,107 @@ def test_dylink_weak(self):
def test_dylink_weak_undef(self):
self.dylink_testf(test_file('core/test_dylink_weak_undef.c'))

@needs_dylink
@parameterized({
'': ([],),
'pthreads': (['-pthread', '-sEXIT_RUNTIME', '-sPROXY_TO_PTHREAD', '-Wno-experimental'],),
})
def test_dylink_main_module_precedence(self, args):
# When the main module and a side module both define a symbol, the main
# module's definition takes precedence when the symbol is accessed via the
# GOT, both from the defining side module (libside1) and from a later side
# module (libside2). The main module never takes the address of these
# symbols itself, so it has no GOT entries for them on startup.
if args:
self.require_pthreads()
self.cflags += args
create_file('libside1.c', r'''
int dup_data1 = 2;
int dup_func1(void) { return 2; }
int dup_data2 = 2;
int dup_func2(void) { return 2; }
void* side1_get_func(void) { return (void*)&dup_func1; }
int* side1_get_data(void) { return &dup_data1; }
''')
create_file('libside2.c', r'''
int dup_func2(void);
extern int dup_data2;
void* side2_get_func(void) { return (void*)&dup_func2; }
int* side2_get_data(void) { return &dup_data2; }
''')
create_file('libmissing.c', r'''
extern int missing_data;
int* missing_get_data(void) { return &missing_data; }
''')
create_file('main.c', r'''
#include <assert.h>
#include <dlfcn.h>
#include <stdio.h>
#include <emscripten.h>
#ifdef __EMSCRIPTEN_PTHREADS__
#include <pthread.h>
#endif

typedef int (*func_t)(void);

EMSCRIPTEN_KEEPALIVE int dup_data1 = 1;
EMSCRIPTEN_KEEPALIVE int dup_func1(void) { return 1; }
EMSCRIPTEN_KEEPALIVE int dup_data2 = 1;
EMSCRIPTEN_KEEPALIVE int dup_func2(void) { return 1; }

void* thread_main(void* arg) {
printf("thread: %d\n", ((func_t)arg)());
return NULL;
}

void check(const char* lib_name, const char* get_func, const char* get_data,
const char* func_name, const char* data_name) {
void* lib = dlopen(lib_name, RTLD_NOW);
assert(lib);
func_t f = ((func_t(*)(void))dlsym(lib, get_func))();
int* d = ((int*(*)(void))dlsym(lib, get_data))();
printf("%s: func=%d data=%d\n", lib_name, f(), *d);
assert(f == (func_t)dlsym(RTLD_DEFAULT, func_name));
assert(d == (int*)dlsym(RTLD_DEFAULT, data_name));
#ifdef __EMSCRIPTEN_PTHREADS__
// Function pointers must be the same on a newly started thread.
pthread_t t;
pthread_create(&t, NULL, thread_main, f);
pthread_join(t, NULL);
#endif
}

int main() {
check("libside1.so", "side1_get_func", "side1_get_data", "dup_func1", "dup_data1");
check("libside2.so", "side2_get_func", "side2_get_data", "dup_func2", "dup_data2");
printf("done\n");
return 0;
}
''')
create_file('missing.c', r'''
#include <dlfcn.h>
#include <stdio.h>
int main() {
printf("libmissing: %s\n", dlopen("libmissing.so", RTLD_NOW) ? "loaded" : "failed");
return 0;
}
''')
self.build_dlfcn_lib('libside1.c', outfile='libside1.so')
self.build_dlfcn_lib('libside2.c', outfile='libside2.so')
self.build_dlfcn_lib('libmissing.c', outfile='libmissing.so')
self.prep_dlfcn_main(libs=['libside1.so', 'libside2.so'])
expected = 'libside1.so: func=1 data=1\n'
if args:
expected += 'thread: 1\n'
expected += 'libside2.so: func=1 data=1\n'
if args:
expected += 'thread: 1\n'
expected += 'done\n'
self.do_runf('main.c', expected)

# A missing (non-weak) GOT import is still an error.
self.do_runf('missing.c', 'libmissing: failed\n')

@needs_dylink
def test_dylink_weak_multilib(self):
create_file('liba.c', '''
Expand Down
1 change: 1 addition & 0 deletions tools/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,7 @@ def limit_incoming_module_api():
'$reportUndefinedSymbols',
'$relocateExports',
'$GOTHandler',
'$initMainModuleGOT',
]
# shared modules need memory utilities to allocate their memory
settings.ALLOW_TABLE_GROWTH = 1
Expand Down
Loading