Skip to content

Avoid creating JS symbols for symbols only used in dynamic linking #21785

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 20, 2024
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 @@ -20,6 +20,13 @@ See docs/process.md for more on how version tagging works.

3.1.58 (in development)
-----------------------
- The `-sMAIN_MODULE=1` mode no longer exports all the main module symbols on
`Module` object. This saves a huge about of generated JS code due the fact
that `-sMAIN_MODULE=1` includes *all* native symbols in your program as well
is from the standard library. The generated JS code for a simple program
in this mode is reduced from from 3.3mb to 0.5mb. The current implementation
of this feature requires wasm-ld to be on the program twice which could have a
noticeable effect on link times. (#21785)
- In `-sMODULARIZE` mode, the argument passed into the module constructor is
no longer mutated in place. The expectation is that the module instance will
be available via the constructor return value. Attempting to access methods
Expand Down
55 changes: 32 additions & 23 deletions tools/emscripten.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def align_memory(addr):
return (addr + 15) & -16


def update_settings_glue(wasm_file, metadata):
def update_settings_glue(wasm_file, metadata, base_metadata):
maybe_disable_filesystem(metadata.imports)

# Integrate info from backend
Expand All @@ -142,7 +142,10 @@ def update_settings_glue(wasm_file, metadata):
if settings.MAIN_MODULE:
settings.WEAK_IMPORTS += webassembly.get_weak_imports(wasm_file)

settings.WASM_EXPORTS = metadata.all_exports
if base_metadata:
settings.WASM_EXPORTS = base_metadata.all_exports
else:
settings.WASM_EXPORTS = metadata.all_exports
settings.WASM_GLOBAL_EXPORTS = list(metadata.global_exports.keys())
settings.HAVE_EM_ASM = bool(settings.MAIN_MODULE or len(metadata.em_asm_consts) != 0)

Expand Down Expand Up @@ -290,22 +293,22 @@ def trim_asm_const_body(body):
return body


def create_global_exports(metadata):
global_exports = []
for k, v in metadata.global_exports.items():
def create_global_exports(global_exports):
lines = []
for k, v in global_exports.items():
v = int(v)
if settings.RELOCATABLE:
v += settings.GLOBAL_BASE
mangled = asmjs_mangle(k)
if settings.MINIMAL_RUNTIME:
global_exports.append("var %s = %s;" % (mangled, v))
lines.append("var %s = %s;" % (mangled, v))
else:
global_exports.append("var %s = Module['%s'] = %s;" % (mangled, mangled, v))
lines.append("var %s = Module['%s'] = %s;" % (mangled, mangled, v))

return '\n'.join(global_exports)
return '\n'.join(lines)


def emscript(in_wasm, out_wasm, outfile_js, js_syms, finalize=True):
def emscript(in_wasm, out_wasm, outfile_js, js_syms, finalize=True, base_metadata=None):
# Overview:
# * Run wasm-emscripten-finalize to extract metadata and modify the binary
# to use emscripten's wasm<->JS ABI
Expand All @@ -329,16 +332,10 @@ def emscript(in_wasm, out_wasm, outfile_js, js_syms, finalize=True):
if settings.RELOCATABLE and settings.MEMORY64 == 2:
metadata.imports += ['__memory_base32']

if settings.ASYNCIFY == 1:
metadata.function_exports['asyncify_start_unwind'] = webassembly.FuncType([webassembly.Type.I32], [])
metadata.function_exports['asyncify_stop_unwind'] = webassembly.FuncType([], [])
metadata.function_exports['asyncify_start_rewind'] = webassembly.FuncType([webassembly.Type.I32], [])
metadata.function_exports['asyncify_stop_rewind'] = webassembly.FuncType([], [])

# If the binary has already been finalized the settings have already been
# updated and we can skip updating them.
if finalize:
update_settings_glue(out_wasm, metadata)
update_settings_glue(out_wasm, metadata, base_metadata)

if not settings.WASM_BIGINT and metadata.em_js_funcs:
import_map = {}
Expand Down Expand Up @@ -444,18 +441,31 @@ def emscript(in_wasm, out_wasm, outfile_js, js_syms, finalize=True):
'// === Body ===\n',
'// === Body ===\n\n' + extra_code + '\n')

if base_metadata:
function_exports = base_metadata.function_exports
global_exports = base_metadata.global_exports
else:
function_exports = metadata.function_exports
global_exports = metadata.global_exports

if settings.ASYNCIFY == 1:
function_exports['asyncify_start_unwind'] = webassembly.FuncType([webassembly.Type.I32], [])
function_exports['asyncify_stop_unwind'] = webassembly.FuncType([], [])
function_exports['asyncify_start_rewind'] = webassembly.FuncType([webassembly.Type.I32], [])
function_exports['asyncify_stop_rewind'] = webassembly.FuncType([], [])

with open(outfile_js, 'w', encoding='utf-8') as out:
out.write(pre)
pre = None

receiving = create_receiving(metadata.function_exports)
receiving = create_receiving(function_exports)

if settings.MINIMAL_RUNTIME:
if settings.DECLARE_ASM_MODULE_EXPORTS:
post = compute_minimal_runtime_initializer_and_exports(post, metadata.function_exports, receiving)
post = compute_minimal_runtime_initializer_and_exports(post, function_exports, receiving)
receiving = ''

module = create_module(receiving, metadata, forwarded_json['librarySymbols'])
module = create_module(receiving, metadata, global_exports, forwarded_json['librarySymbols'])

metadata.library_definitions = forwarded_json['libraryDefinitions']

Expand Down Expand Up @@ -638,8 +648,7 @@ def create_tsd(metadata, embind_tsd):
out += create_tsd_exported_runtime_methods(metadata)
# Manually generate defintions for any Wasm function exports.
out += 'interface WasmModule {\n'
function_exports = metadata.function_exports
for name, types in function_exports.items():
for name, types in metadata.function_exports.items():
mangled = asmjs_mangle(name)
should_export = settings.EXPORT_KEEPALIVE and mangled in settings.EXPORTED_FUNCTIONS
if not should_export:
Expand Down Expand Up @@ -950,8 +959,8 @@ def create_receiving(function_exports):
return '\n'.join(receiving) + '\n'


def create_module(receiving, metadata, library_symbols):
receiving += create_global_exports(metadata)
def create_module(receiving, metadata, global_exports, library_symbols):
receiving += create_global_exports(global_exports)
module = []

sending = create_sending(metadata, library_symbols)
Expand Down
59 changes: 40 additions & 19 deletions tools/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from . import system_libs
from . import utils
from . import webassembly
from . import extract_metadata
from .utils import read_file, read_binary, write_file, delete_file
from .utils import removeprefix, exit_with_error
from .shared import in_temp, safe_copy, do_replace, OFormat
Expand Down Expand Up @@ -1847,11 +1848,28 @@ def phase_link(linker_arguments, wasm_target, js_syms):
settings.REQUIRED_EXPORTS = dedup_list(settings.REQUIRED_EXPORTS)
settings.EXPORT_IF_DEFINED = dedup_list(settings.EXPORT_IF_DEFINED)

rtn = None
if settings.LINKABLE and not settings.EXPORT_ALL:
# In LINKABLE mode we pass `--export-dynamic` along with `--whole-archive`. This results
# in over 7000 exports, which cannot be distinguished from the few symbols we explicitly
# export via EMSCRIPTEN_KEEPALIVE or EXPORTED_FUNCTIONS.
# In order to avoid unnecessary exported symbols on the `Module` object we run the linker
# twice in this mode:
# 1. Without `--export-dynamic` to get the base exports
# 2. With `--export-dynamic` to get the actual linkable Wasm binary
# TODO(sbc): Remove this double execution of wasm-ld if we ever find a way to
# distinguish EMSCRIPTEN_KEEPALIVE exports from `--export-dynamic` exports.
settings.LINKABLE = False
building.link_lld(linker_arguments, wasm_target, external_symbols=js_syms)
settings.LINKABLE = True
rtn = extract_metadata.extract_metadata(wasm_target)

building.link_lld(linker_arguments, wasm_target, external_symbols=js_syms)
return rtn


@ToolchainProfiler.profile_block('post link')
def phase_post_link(options, state, in_wasm, wasm_target, target, js_syms):
def phase_post_link(options, state, in_wasm, wasm_target, target, js_syms, base_metadata=None):
global final_js

target_basename = unsuffixed_basename(target)
Expand All @@ -1868,7 +1886,7 @@ def phase_post_link(options, state, in_wasm, wasm_target, target, js_syms):

settings.TARGET_JS_NAME = os.path.basename(state.js_target)

metadata = phase_emscript(in_wasm, wasm_target, js_syms)
metadata = phase_emscript(in_wasm, wasm_target, js_syms, base_metadata)

if settings.EMBIND_AOT:
phase_embind_aot(wasm_target, js_syms)
Expand All @@ -1887,7 +1905,7 @@ def phase_post_link(options, state, in_wasm, wasm_target, target, js_syms):


@ToolchainProfiler.profile_block('emscript')
def phase_emscript(in_wasm, wasm_target, js_syms):
def phase_emscript(in_wasm, wasm_target, js_syms, base_metadata):
# Emscripten
logger.debug('emscript')

Expand All @@ -1898,7 +1916,7 @@ def phase_emscript(in_wasm, wasm_target, js_syms):
if shared.SKIP_SUBPROCS:
return

metadata = emscripten.emscript(in_wasm, wasm_target, final_js, js_syms)
metadata = emscripten.emscript(in_wasm, wasm_target, final_js, js_syms, base_metadata=base_metadata)
save_intermediate('original')
return metadata

Expand Down Expand Up @@ -3068,24 +3086,27 @@ def run(linker_inputs, options, state, newargs):
js_info = get_js_sym_info()
if not settings.SIDE_MODULE:
js_syms = js_info['deps']

def add_js_deps(sym):
if sym in js_syms:
native_deps = js_syms[sym]
if native_deps:
settings.REQUIRED_EXPORTS += native_deps

for sym in settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE:
add_js_deps(sym)
for sym in js_info['extraLibraryFuncs']:
add_js_deps(sym)
for sym in settings.EXPORTED_RUNTIME_METHODS:
add_js_deps(shared.demangle_c_symbol_name(sym))
if settings.LINKABLE:
for native_deps in js_syms.values():
settings.REQUIRED_EXPORTS += native_deps
else:
def add_js_deps(sym):
if sym in js_syms:
native_deps = js_syms[sym]
if native_deps:
settings.REQUIRED_EXPORTS += native_deps

for sym in settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE:
add_js_deps(sym)
for sym in js_info['extraLibraryFuncs']:
add_js_deps(sym)
for sym in settings.EXPORTED_RUNTIME_METHODS:
add_js_deps(shared.demangle_c_symbol_name(sym))
if settings.ASYNCIFY:
settings.ASYNCIFY_IMPORTS_EXCEPT_JS_LIBS = settings.ASYNCIFY_IMPORTS[:]
settings.ASYNCIFY_IMPORTS += ['*.' + x for x in js_info['asyncFuncs']]

phase_link(linker_arguments, wasm_target, js_syms)
base_metadata = phase_link(linker_arguments, wasm_target, js_syms)

# Special handling for when the user passed '-Wl,--version'. In this case the linker
# does not create the output file, but just prints its version and exits with 0.
Expand All @@ -3099,6 +3120,6 @@ def add_js_deps(sym):

# Perform post-link steps (unless we are running bare mode)
if options.oformat != OFormat.BARE:
phase_post_link(options, state, wasm_target, wasm_target, target, js_syms)
phase_post_link(options, state, wasm_target, wasm_target, target, js_syms, base_metadata)

return 0
Loading