Skip to content

Commit f7e4555

Browse files
committed
Use wasmImports as the single global symbol table for dynamic linking
Previously we were looking up symbols on the `Module` object which meant building with `EXPORT_ALL` and exporting all symbols there. This change adds all needed symbols to the `wasmImports` maps and uses that as the single source for symbol lookups. This is split out from my work on #18376. This should be a code size win in for most users of dynamic linking. It also avoids extra symbols being present on the `Module` object that the user didn't ask for.
1 parent 778fdf4 commit f7e4555

35 files changed

+167
-103
lines changed

emcc.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -832,16 +832,14 @@ def process_dynamic_libs(dylibs, lib_dirs):
832832
# main module to avoid creating new invoke functions at runtime.
833833
imports = set(imports)
834834
imports = set(i for i in imports if not i.startswith('invoke_'))
835-
weak_imports = sorted(imports.intersection(exports))
836835
strong_imports = sorted(imports.difference(exports))
837836
logger.debug('Adding symbols requirements from `%s`: %s', dylib, imports)
838837

839838
mangled_imports = [shared.asmjs_mangle(e) for e in sorted(imports)]
840839
mangled_strong_imports = [shared.asmjs_mangle(e) for e in strong_imports]
841840
settings.SIDE_MODULE_IMPORTS.extend(mangled_imports)
842-
settings.EXPORTED_FUNCTIONS.extend(mangled_strong_imports)
843-
settings.EXPORT_IF_DEFINED.extend(weak_imports)
844-
settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.extend(strong_imports)
841+
settings.EXPORT_IF_DEFINED.extend(sorted(imports))
842+
settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.extend(sorted(imports))
845843
building.user_requested_exports.update(mangled_strong_imports)
846844

847845

@@ -2023,7 +2021,6 @@ def phase_linker_setup(options, state, newargs):
20232021

20242022
if settings.MAIN_MODULE == 1 or settings.SIDE_MODULE == 1:
20252023
settings.LINKABLE = 1
2026-
settings.EXPORT_ALL = 1
20272024

20282025
if settings.LINKABLE and settings.USER_EXPORTED_FUNCTIONS:
20292026
diagnostics.warning('unused-command-line-argument', 'EXPORTED_FUNCTIONS is not valid with LINKABLE set (normally due to SIDE_MODULE=1/MAIN_MODULE=1) since all functions are exported this mode. To export only a subset use SIDE_MODULE=2/MAIN_MODULE=2')

emscripten.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -424,15 +424,14 @@ def emscript(in_wasm, out_wasm, outfile_js, memfile):
424424
out.write(normalize_line_endings(pre))
425425
pre = None
426426

427-
sending = create_sending(invoke_funcs, metadata)
428427
receiving = create_receiving(exports)
429428

430429
if settings.MINIMAL_RUNTIME:
431430
if settings.DECLARE_ASM_MODULE_EXPORTS:
432431
post = compute_minimal_runtime_initializer_and_exports(post, exports, receiving)
433432
receiving = ''
434433

435-
module = create_module(sending, receiving, invoke_funcs, metadata)
434+
module = create_module(receiving, invoke_funcs, metadata, forwarded_json['librarySymbols'])
436435

437436
write_output_file(out, module)
438437

@@ -671,7 +670,7 @@ def add_standard_wasm_imports(send_items_map):
671670
send_items_map[s] = s
672671

673672

674-
def create_sending(invoke_funcs, metadata):
673+
def create_sending(invoke_funcs, metadata, library_symbols):
675674
# Map of wasm imports to mangled/external/JS names
676675
send_items_map = {}
677676

@@ -685,6 +684,28 @@ def create_sending(invoke_funcs, metadata):
685684

686685
add_standard_wasm_imports(send_items_map)
687686

687+
if settings.MAIN_MODULE:
688+
# When including dynamic linking support, also add any JS library functions
689+
# that are part of EXPORTED_FUNCTIONS (or in the case of MAIN_MODULE=1 add
690+
# all JS library functions). This allows `dlsym(RTLD_DEFAULT)` to lookup JS
691+
# library functions, since `wasmImports` acts as the global symbol table.
692+
wasm_exports = set(metadata.exports)
693+
library_symbols = set(library_symbols)
694+
if settings.MAIN_MODULE == 1:
695+
for f in library_symbols:
696+
if shared.is_c_symbol(f):
697+
demangled = shared.demangle_c_symbol_name(f)
698+
if demangled in wasm_exports:
699+
continue
700+
send_items_map[demangled] = f
701+
else:
702+
for f in settings.EXPORTED_FUNCTIONS + settings.SIDE_MODULE_IMPORTS:
703+
if f in library_symbols and shared.is_c_symbol(f):
704+
demangled = shared.demangle_c_symbol_name(f)
705+
if demangled in wasm_exports:
706+
continue
707+
send_items_map[demangled] = f
708+
688709
sorted_keys = sorted(send_items_map.keys())
689710
return '{\n ' + ',\n '.join('"' + k + '": ' + send_items_map[k] for k in sorted_keys) + '\n}'
690711

@@ -720,10 +741,10 @@ def make_export_wrappers(exports, delay_assignment):
720741
elif delay_assignment:
721742
# With assertions disabled the wrapper will replace the global var and Module var on
722743
# first use.
723-
wrapper += '''function() {
724-
return (%(mangled)s = %(exported)sModule["asm"]["%(name)s"]).apply(null, arguments);
725-
};
726-
''' % {'mangled': mangled, 'name': name, 'exported': exported}
744+
wrapper += f'''function() {{
745+
return ({mangled} = {exported}Module["asm"]["{name}"]).apply(null, arguments);
746+
}};
747+
'''
727748
else:
728749
wrapper += 'asm["%s"]' % name
729750

@@ -771,11 +792,12 @@ def create_receiving(exports):
771792
return '\n'.join(receiving) + '\n'
772793

773794

774-
def create_module(sending, receiving, invoke_funcs, metadata):
795+
def create_module(receiving, invoke_funcs, metadata, library_symbols):
775796
invoke_wrappers = create_invoke_wrappers(invoke_funcs)
776797
receiving += create_named_globals(metadata)
777798
module = []
778799

800+
sending = create_sending(invoke_funcs, metadata, library_symbols)
779801
module.append('var wasmImports = %s;\n' % sending)
780802
if settings.ASYNCIFY and (settings.ASSERTIONS or settings.ASYNCIFY == 2):
781803
# instrumenting imports is used in asyncify in two ways: to add assertions

src/jsifier.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,10 +271,19 @@ function ${name}(${args}) {
271271
// (not useful to warn/error multiple times)
272272
LibraryManager.library[symbol + '__docs'] = '/** @type {function(...*):?} */';
273273
} else {
274-
const target = `Module['${mangled}']`;
274+
// Create a stub for this symbol which can later be replaced by the
275+
// dynamic linker. If this stub is called before the symbol is
276+
// resolved assert in debug builds or trap in release builds.
277+
if (ASYNCIFY) {
278+
// See the definition of asyncifyStubs in preamble.js for why this
279+
// is needed.
280+
target = `asyncifyStubs['${symbol}']`;
281+
} else {
282+
target = `wasmImports['${symbol}']`;
283+
}
275284
let assertion = '';
276285
if (ASSERTIONS) {
277-
assertion += `if (!${target}) abort("external symbol '${symbol}' is missing. perhaps a side module was not linked in? if this function was expected to arrive from a system library, try to build the MAIN_MODULE with EMCC_FORCE_STDLIBS=1 in the environment");\n`;
286+
assertion += `if (!${target} || ${target}.stub) abort("external symbol '${symbol}' is missing. perhaps a side module was not linked in? if this function was expected to arrive from a system library, try to build the MAIN_MODULE with EMCC_FORCE_STDLIBS=1 in the environment");\n`;
278287
}
279288
const functionBody = assertion + `return ${target}.apply(null, arguments);`;
280289
LibraryManager.library[symbol] = new Function(functionBody);
@@ -426,6 +435,9 @@ function ${name}(${args}) {
426435
}
427436
if (isStub) {
428437
contentText += `\n${mangled}.stub = true;`;
438+
if (ASYNCIFY) {
439+
contentText += `\nasyncifyStubs['${symbol}'] = undefined;`;
440+
}
429441
}
430442

431443
let commentText = '';

src/library_dylink.js

Lines changed: 61 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,44 @@ var dlopenMissingError = "'To use dlopen, you need enable dynamic linking, see h
1010

1111
var LibraryDylink = {
1212
#if RELOCATABLE
13+
$isSymbolDefined: function(symName) {
14+
// Ignore 'stub' symbols that are auto-generated as part of the original
15+
// `wasmImports` used to instantate the main module.
16+
var existing = wasmImports[symName];
17+
if (!existing || existing.stub) {
18+
return false;
19+
}
20+
#if ASYNCIFY
21+
// Even if a symbol exists in wasmImports, and is not itself a stub, it
22+
// could be an ASYNCIFY wrapper function that wraps a stub function.
23+
if (symName in asyncifyStubs && !asyncifyStubs[symName]) {
24+
return false;
25+
}
26+
#endif
27+
return true;
28+
},
29+
30+
$resolveGlobalSymbol__deps: ['$isSymbolDefined'],
1331
$resolveGlobalSymbol__internal: true,
14-
$resolveGlobalSymbol__deps: ['$asmjsMangle'],
15-
$resolveGlobalSymbol: function(symName, direct) {
32+
$resolveGlobalSymbol: function(symName, direct = false) {
1633
var sym;
1734
#if !WASM_BIGINT
1835
if (direct) {
1936
// First look for the orig$ symbol which is the symbols without
2037
// any legalization performed.
2138
sym = wasmImports['orig$' + symName];
39+
if (sym) return sym;
2240
}
2341
#endif
24-
if (!sym) {
42+
if (isSymbolDefined(symName)) {
2543
sym = wasmImports[symName];
26-
// Ignore 'stub' symbols that are auto-generated as part of the original
27-
// `wasmImports` used to instantate the main module.
28-
if (sym && sym.stub) sym = undefined;
44+
} else if (symName.startsWith('invoke_')) {
45+
// Create (and cache) new invoke_ functions on demand.
46+
sym = wasmImports[symName] = createInvokeFunction(symName.split('_')[1]);
2947
}
30-
31-
// Check for the symbol on the Module object. This is the only
32-
// way to dynamically access JS library symbols that were not
33-
// referenced by the main module (and therefore not part of the
34-
// initial set of symbols included in wasmImports when it
35-
// was declared.
36-
if (!sym) {
37-
sym = Module[asmjsMangle(symName)];
38-
}
39-
40-
if (!sym && symName.startsWith('invoke_')) {
41-
sym = createInvokeFunction(symName.split('_')[1]);
42-
}
43-
4448
#if !DISABLE_EXCEPTION_CATCHING
45-
if (!sym && symName.startsWith("__cxa_find_matching_catch")) {
46-
sym = Module["___cxa_find_matching_catch"];
49+
else if (symName.startsWith('__cxa_find_matching_catch_')) {
50+
sym = wasmImports['__cxa_find_matching_catch'];
4751
}
4852
#endif
4953
return sym;
@@ -456,24 +460,15 @@ var LibraryDylink = {
456460
},
457461

458462
// Module.symbols <- libModule.symbols (flags.global handler)
459-
$mergeLibSymbols__deps: ['$asmjsMangle'],
463+
$mergeLibSymbols__deps: ['$asmjsMangle', '$isSymbolDefined'],
460464
$mergeLibSymbols: function(exports, libName) {
461465
// add symbols into global namespace TODO: weak linking etc.
462466
for (var sym in exports) {
463467
if (!exports.hasOwnProperty(sym)) {
464468
continue;
465469
}
466-
467-
// When RTLD_GLOBAL is enable, the symbols defined by this shared object will be made
468-
// available for symbol resolution of subsequently loaded shared objects.
469-
//
470-
// We should copy the symbols (which include methods and variables) from SIDE_MODULE to MAIN_MODULE.
471-
472-
if (!wasmImports.hasOwnProperty(sym)) {
473-
wasmImports[sym] = exports[sym];
474-
}
475470
#if ASSERTIONS == 2
476-
else {
471+
if (isSymbolDefined(sym)) {
477472
var curr = wasmImports[sym], next = exports[sym];
478473
// don't warn on functions - might be odr, linkonce_odr, etc.
479474
if (!(typeof curr == 'function' && typeof next == 'function')) {
@@ -482,19 +477,40 @@ var LibraryDylink = {
482477
}
483478
#endif
484479

485-
// Export native export on the Module object.
486-
// TODO(sbc): Do all users want this? Should we skip this by default?
487-
var module_sym = asmjsMangle(sym);
488-
if (!Module.hasOwnProperty(module_sym)) {
489-
Module[module_sym] = exports[sym];
480+
// When RTLD_GLOBAL is enabled, the symbols defined by this shared object
481+
// will be made available for symbol resolution of subsequently loaded
482+
// shared objects.
483+
//
484+
// We should copy the symbols (which include methods and variables) from
485+
// SIDE_MODULE to MAIN_MODULE.
486+
const setImport = (target) => {
487+
#if ASYNCIFY
488+
if (target in asyncifyStubs) {
489+
asyncifyStubs[target] = exports[sym]
490+
}
491+
#endif
492+
if (!isSymbolDefined(target)) {
493+
wasmImports[target] = exports[sym];
494+
}
490495
}
496+
setImport(sym);
497+
491498
#if !hasExportedSymbol('main')
492-
// If the main module doesn't define main it could be defined in one of
493-
// the side modules, and we need to handle the mangled named.
494-
if (sym == '__main_argc_argv') {
495-
Module['_main'] = exports[sym];
499+
// Special case for handling of main symbol: If a side module exports
500+
// `main` that also acts a definition for `__main_argc_argv` and vice
501+
// versa.
502+
const main_alias = '__main_argc_argv';
503+
if (sym == 'main') {
504+
setImport(main_alias)
505+
}
506+
if (sym == main_alias) {
507+
setImport('main')
496508
}
497509
#endif
510+
511+
if (sym.startsWith('dynCall_') && !Module.hasOwnProperty(sym)) {
512+
Module[sym] = exports[sym];
513+
}
498514
}
499515
},
500516

@@ -575,7 +591,7 @@ var LibraryDylink = {
575591
var moduleExports;
576592

577593
function resolveSymbol(sym) {
578-
var resolved = resolveGlobalSymbol(sym, false);
594+
var resolved = resolveGlobalSymbol(sym);
579595
if (!resolved) {
580596
resolved = moduleExports[sym];
581597
}
@@ -613,7 +629,7 @@ var LibraryDylink = {
613629
return tableBase;
614630
#endif
615631
}
616-
if (prop in wasmImports) {
632+
if (prop in wasmImports && !wasmImports[prop].stub) {
617633
// No stub needed, symbol already exists in symbol table
618634
return wasmImports[prop];
619635
}
@@ -800,7 +816,7 @@ var LibraryDylink = {
800816
// If a library was already loaded, it is not loaded a second time. However
801817
// flags.global and flags.nodelete are handled every time a load request is made.
802818
// Once a library becomes "global" or "nodelete", it cannot be removed or unloaded.
803-
$loadDynamicLibrary__deps: ['$LDSO', '$loadWebAssemblyModule', '$asmjsMangle', '$isInternalSym', '$mergeLibSymbols'],
819+
$loadDynamicLibrary__deps: ['$LDSO', '$loadWebAssemblyModule', '$isInternalSym', '$mergeLibSymbols'],
804820
$loadDynamicLibrary__docs: '/** @param {number=} handle */',
805821
$loadDynamicLibrary: function(lib, flags = {global: true, nodelete: true}, handle = 0) {
806822
#if DYLINK_DEBUG

src/parseTools.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,25 @@ function getUnsharedTextDecoderView(heap, start, end) {
11001100
return `${heap}.buffer instanceof SharedArrayBuffer ? ${shared} : ${unshared}`;
11011101
}
11021102

1103+
function getEntryFunction() {
1104+
var entryFunction = 'main';
1105+
if (STANDALONE_WASM) {
1106+
if (EXPECT_MAIN) {
1107+
entryFunction = '_start';
1108+
} else {
1109+
entryFunction = '_initialize';
1110+
}
1111+
} else if (PROXY_TO_PTHREAD) {
1112+
// User requested the PROXY_TO_PTHREAD option, so call a stub main which pthread_create()s a new thread
1113+
// that will call the user's real main() for the application.
1114+
entryFunction = '_emscripten_proxy_main';
1115+
}
1116+
if (MAIN_MODULE) {
1117+
return `resolveGlobalSymbol('${entryFunction}');`
1118+
}
1119+
return '_' + entryFunction;
1120+
}
1121+
11031122
function preJS() {
11041123
let result = '';
11051124
for (const fileName of PRE_JS_FILES) {

src/postamble.js

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,7 @@ function callMain() {
3131
assert(__ATPRERUN__.length == 0, 'cannot call main when preRun functions remain to be called');
3232
#endif
3333

34-
#if STANDALONE_WASM
35-
#if EXPECT_MAIN
36-
var entryFunction = Module['__start'];
37-
#else
38-
var entryFunction = Module['__initialize'];
39-
#endif
40-
#else
41-
#if PROXY_TO_PTHREAD
42-
// User requested the PROXY_TO_PTHREAD option, so call a stub main which pthread_create()s a new thread
43-
// that will call the user's real main() for the application.
44-
var entryFunction = Module['__emscripten_proxy_main'];
45-
#else
46-
var entryFunction = Module['_main'];
47-
#endif
48-
#endif
34+
var entryFunction = {{{ getEntryFunction() }}};
4935

5036
#if MAIN_MODULE
5137
// Main modules can't tell if they have main() at compile time, since it may

src/preamble.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,4 +1223,13 @@ function runMemoryInitializer() {
12231223
}
12241224
#endif // MEM_INIT_IN_WASM == 0
12251225

1226+
#if MAIN_MODULE && ASYNCIFY
1227+
// With MAIN_MODULE + ASYNCIFY the normal method of placing stub functions in
1228+
// wasmImports for as-yet-undefined symbols doesn't work since ASYNCIFY then
1229+
// wraps these stub functions and we can't then replace them directly. Instead
1230+
// the stub functions call into `asyncifyStubs` which gets populated by the
1231+
// dynamic linker as symbols are loaded.
1232+
var asyncifyStubs = {};
1233+
#endif
1234+
12261235
// === Body ===
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
26008
1+
26013
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
25972
1+
25977
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
30545
1+
30550

0 commit comments

Comments
 (0)