Skip to content

Commit c334944

Browse files
authored
Run wasm-emscripten-finalize only when needed, and add ERROR_ON_WASM_CHANGES_AFTER_LINK option (#12173)
emscripten.py now tracks whether we actually need to run wasm-emscripten-finalize. Currently we don't need to in a specific subset of -O0 builds, in which we don't need legalization (by using BigInt support) and we don't need invokes (which means we don't use longjmp or C++ exceptions; this restriction will be lifted once WebAssembly/binaryen#3081 is fixed). This new option will show an error if any binaryen work is done after wasm-ld. That is, it ensures that we do not run wasm-emscripten-finalize or wasm-opt. That is useful to verify that the link is as fast as possible and also that it does not rewrite DWARF, which is necessary to support split DWARF (binaryen can rewrite normal DWARF, but it's simpler and better to support split DWARF by just not doing any binaryen work after wasm-ld). See WebAssembly/binaryen#3043
1 parent 3f53117 commit c334944

File tree

6 files changed

+125
-11
lines changed

6 files changed

+125
-11
lines changed

ChangeLog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ See docs/process.md for how version tagging works.
1717

1818
Current Trunk
1919
-------------
20+
- Add `ERROR_ON_WASM_CHANGES_AFTER_LINK` option that errors if we need to do
21+
any work in `wasm-emscripten-finalize` or `wasm-opt` after linking. This
22+
can verify the link is maximally fast and also does no DWARF rewriting.
23+
(#12173)
2024

2125
2.0.3: 09/10/2020
2226
-----------------

emscripten.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import os
1515
import json
1616
import subprocess
17+
import shutil
1718
import time
1819
import logging
1920
import pprint
@@ -509,31 +510,49 @@ def finalize_wasm(temp_files, infile, outfile, memfile, DEBUG):
509510

510511
args = ['--detect-features', '--minimize-wasm-changes']
511512

513+
# if we don't need to modify the wasm, don't tell finalize to emit a wasm file
514+
modify_wasm = False
515+
516+
# C++ exceptions and longjmp require invoke processing,
517+
# https://github.com/WebAssembly/binaryen/issues/3081
518+
if shared.Settings.SUPPORT_LONGJMP or shared.Settings.DISABLE_EXCEPTION_CATCHING != 1:
519+
modify_wasm = True
520+
if shared.Settings.WASM2JS:
521+
# wasm2js requires full legalization (and will do extra wasm binary
522+
# later processing later anyhow)
523+
modify_wasm = True
512524
write_source_map = shared.Settings.DEBUG_LEVEL >= 4
513525
if write_source_map:
514526
building.emit_wasm_source_map(base_wasm, base_wasm + '.map')
515527
building.save_intermediate(base_wasm + '.map', 'base_wasm.map')
516528
args += ['--output-source-map-url=' + shared.Settings.SOURCE_MAP_BASE + os.path.basename(shared.Settings.WASM_BINARY_FILE) + '.map']
517-
529+
modify_wasm = True
518530
# tell binaryen to look at the features section, and if there isn't one, to use MVP
519531
# (which matches what llvm+lld has given us)
520532
if shared.Settings.DEBUG_LEVEL >= 2 or shared.Settings.PROFILING_FUNCS or shared.Settings.EMIT_SYMBOL_MAP or shared.Settings.ASYNCIFY_ONLY or shared.Settings.ASYNCIFY_REMOVE or shared.Settings.ASYNCIFY_ADD:
521533
args.append('-g')
522534
if shared.Settings.WASM_BIGINT:
523535
args.append('--bigint')
524-
525-
if not shared.Settings.USE_LEGACY_DYNCALLS:
536+
if shared.Settings.USE_LEGACY_DYNCALLS:
537+
# we need to add all dyncalls to the wasm
538+
modify_wasm = True
539+
else:
526540
if shared.Settings.WASM_BIGINT:
527541
args.append('--no-dyncalls')
528542
else:
529543
args.append('--dyncalls-i64')
530-
531-
if shared.Settings.LEGALIZE_JS_FFI != 1:
544+
# we need to add some dyncalls to the wasm
545+
modify_wasm = True
546+
if not shared.Settings.LEGALIZE_JS_FFI:
532547
args.append('--no-legalize-javascript-ffi')
548+
else:
549+
modify_wasm = True
533550
if not shared.Settings.MEM_INIT_IN_WASM:
534551
args.append('--separate-data-segments=' + memfile)
552+
modify_wasm = True
535553
if shared.Settings.SIDE_MODULE:
536554
args.append('--side-module')
555+
modify_wasm = True
537556
else:
538557
# --global-base is used by wasm-emscripten-finalize to calculate the size
539558
# of the static data used. The argument we supply here needs to match the
@@ -548,23 +567,30 @@ def finalize_wasm(temp_files, infile, outfile, memfile, DEBUG):
548567
args.append('--global-base=%s' % shared.Settings.GLOBAL_BASE)
549568
if shared.Settings.STACK_OVERFLOW_CHECK >= 2:
550569
args.append('--check-stack-overflow')
570+
modify_wasm = True
551571
if shared.Settings.STANDALONE_WASM:
552572
args.append('--standalone-wasm')
573+
modify_wasm = True
553574
# When we dynamically link our JS loader adds functions from wasm modules to
554575
# the table. It must add the original versions of them, not legalized ones,
555576
# so that indirect calls have the right type, so export those.
556577
if shared.Settings.RELOCATABLE:
557578
args.append('--pass-arg=legalize-js-interface-export-originals')
579+
modify_wasm = True
558580
if shared.Settings.DEBUG_LEVEL >= 3:
559581
args.append('--dwarf')
560-
stdout = building.run_binaryen_command('wasm-emscripten-finalize',
561-
infile=base_wasm,
562-
outfile=wasm,
563-
args=args,
564-
stdout=subprocess.PIPE)
582+
stdout = building.run_binaryen_command(
583+
'wasm-emscripten-finalize',
584+
infile=base_wasm,
585+
outfile=wasm if modify_wasm else None,
586+
args=args,
587+
stdout=subprocess.PIPE)
588+
if modify_wasm:
589+
building.save_intermediate(wasm, 'post_finalize.wasm')
590+
else:
591+
shutil.copyfile(base_wasm, wasm)
565592
if write_source_map:
566593
building.save_intermediate(wasm + '.map', 'post_finalize.map')
567-
building.save_intermediate(wasm, 'post_finalize.wasm')
568594

569595
if not shared.Settings.MEM_INIT_IN_WASM:
570596
# we have a separate .mem file. binaryen did not strip any trailing zeros,

src/settings.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1591,6 +1591,17 @@ var WASM2C = 0;
15911591
// in a custom location.
15921592
var SEPARATE_DWARF_URL = '';
15931593

1594+
// Emscripten runs wasm-ld to link, and in some cases will do further changes to
1595+
// the wasm afterwards, like running wasm-opt to optimize the binary in
1596+
// optimized builds. However, in some builds no wasm changes are necessary after
1597+
// link. This can make the entire link step faster, and can also be important
1598+
// for other reasons, like in debugging if the wasm is not modified then the
1599+
// DWARF info from LLVM is preserved (wasm-opt can rewrite it in some cases, but
1600+
// not in others like split-dwarf).
1601+
// When this flag is turned on, we error at link time if the build requires any
1602+
// changes to the wasm after link. This can be useful in testing, for example.
1603+
var ERROR_ON_WASM_CHANGES_AFTER_LINK = 0;
1604+
15941605
// Whether the program should abort when an unhandled WASM exception is encountered.
15951606
// This makes the Emscripten program behave more like a native program where the OS
15961607
// would terminate the process and no further code can be executed when an unhandled

tests/hello_world_main_loop.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2020 The Emscripten Authors. All rights reserved.
2+
// Emscripten is available under two separate licenses, the MIT license and the
3+
// University of Illinois/NCSA Open Source License. Both these licenses can be
4+
// found in the LICENSE file.
5+
6+
#include <stdio.h>
7+
#include <emscripten.h>
8+
9+
void looper() {
10+
static int frame = 0;
11+
frame++;
12+
if (frame == 10) {
13+
puts("hello, world!");
14+
emscripten_cancel_main_loop();
15+
}
16+
}
17+
18+
int main() {
19+
emscripten_set_main_loop(looper, 0, 0);
20+
}

tests/test_other.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9327,6 +9327,44 @@ def test_stdout_link(self):
93279327
self.assertContained('invalid output filename: `-foo`', err)
93289328
self.assertNotExists('-foo')
93299329

9330+
def test_immutable_after_link(self):
9331+
# some builds are guaranteed to not require any binaryen work after wasm-ld
9332+
def ok(args, filename='hello_world.cpp', expected='hello, world!'):
9333+
print('ok', args, filename)
9334+
args += ['-sERROR_ON_WASM_CHANGES_AFTER_LINK']
9335+
self.run_process([EMCC, path_from_root('tests', filename)] + args)
9336+
self.assertContained(expected, self.run_js('a.out.js'))
9337+
9338+
# -O0 with BigInt support (to avoid the need for legalization) and without
9339+
# longjmp
9340+
required_flags = ['-sWASM_BIGINT', '-sSUPPORT_LONGJMP=0']
9341+
ok(required_flags)
9342+
# Same with DWARF
9343+
ok(required_flags + ['-g'])
9344+
# Function pointer calls from JS work too
9345+
ok(required_flags, filename='hello_world_main_loop.cpp')
9346+
9347+
# other builds fail with a standard message + extra details
9348+
def fail(args, details):
9349+
print('fail', args, details)
9350+
args += ['-sERROR_ON_WASM_CHANGES_AFTER_LINK']
9351+
err = self.expect_fail([EMCC, path_from_root('tests', 'hello_world.cpp')] + args)
9352+
self.assertContained('changes to the wasm are required after link, but disallowed by ERROR_ON_WASM_CHANGES_AFTER_LINK', err)
9353+
self.assertContained(details, err)
9354+
9355+
# plain -O0
9356+
legalization_message = 'to disable legalization (which requires changes after link) use -s WASM_BIGINT'
9357+
longjmp_message = 'to disable longjmp support (which requires changes after link) use -s SUPPORT_LONGJMP=0'
9358+
fail([], legalization_message)
9359+
fail(['-sWASM_BIGINT'], longjmp_message)
9360+
fail(['-sSUPPORT_LONGJMP=0'], legalization_message)
9361+
# optimized builds even without legalization
9362+
optimization_message = 'optimizations always require changes, build with -O0 instead'
9363+
fail(required_flags + ['-O1'], optimization_message)
9364+
fail(required_flags + ['-O2'], optimization_message)
9365+
# exceptions fails until invokes are fixed
9366+
fail(required_flags + ['-fexceptions'], 'C++ exceptions always require changes')
9367+
93309368
def test_output_to_nowhere(self):
93319369
self.run_process([EMCC, path_from_root('tests', 'hello_world.cpp'), '-o', os.devnull, '-c'])
93329370

tools/building.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1568,6 +1568,21 @@ def run_binaryen_command(tool, infile, outfile=None, args=[], debug=False, stdou
15681568
cmd += [infile]
15691569
if outfile:
15701570
cmd += ['-o', outfile]
1571+
if Settings.ERROR_ON_WASM_CHANGES_AFTER_LINK:
1572+
# emit some extra helpful text for common issues
1573+
extra = ''
1574+
# a plain -O0 build *almost* doesn't need post-link changes, except for
1575+
# legalization and longjmp. show a clear error for those (as the flags
1576+
# the user passed in are not enough to see what went wrong)
1577+
if shared.Settings.LEGALIZE_JS_FFI:
1578+
extra += '\nnote: to disable legalization (which requires changes after link) use -s WASM_BIGINT'
1579+
if shared.Settings.SUPPORT_LONGJMP:
1580+
extra += '\nnote: to disable longjmp support (which requires changes after link) use -s SUPPORT_LONGJMP=0'
1581+
if shared.Settings.OPT_LEVEL > 0:
1582+
extra += '\nnote: optimizations always require changes, build with -O0 instead'
1583+
if shared.Settings.DISABLE_EXCEPTION_CATCHING != 1:
1584+
extra += '\nnote: C++ exceptions always require changes'
1585+
exit_with_error('changes to the wasm are required after link, but disallowed by ERROR_ON_WASM_CHANGES_AFTER_LINK: ' + str(cmd) + extra)
15711586
if debug:
15721587
cmd += ['-g'] # preserve the debug info
15731588
# if the features are not already handled, handle them

0 commit comments

Comments
 (0)