Skip to content

Commit 5dba39c

Browse files
authored
-O0 Hello world working with MINIMAL_RUNTIME. (#10056)
* -O0 Hello world working with MINIMAL_RUNTIME.
1 parent 748c80c commit 5dba39c

File tree

6 files changed

+168
-55
lines changed

6 files changed

+168
-55
lines changed

emscripten.py

Lines changed: 87 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from tools import gen_struct_info
2626
from tools import jsrun
2727
from tools.response_file import substitute_response_files
28-
from tools.shared import WINDOWS, asstr, path_from_root, exit_with_error
28+
from tools.shared import WINDOWS, asstr, path_from_root, exit_with_error, asmjs_mangle, treat_as_user_function
2929
from tools.toolchain_profiler import ToolchainProfiler
3030
from tools.minified_js_name_generator import MinifiedJsNameGenerator
3131

@@ -270,6 +270,26 @@ def get_asm_extern_primitives(pre):
270270
return []
271271

272272

273+
def compute_minimal_runtime_initializer_and_exports(post, initializers, exports, receiving):
274+
# Generate invocations for all global initializers directly off the asm export object, e.g. asm['__GLOBAL__INIT']();
275+
post = post.replace('/*** RUN_GLOBAL_INITIALIZERS(); ***/', '\n'.join(["asm['" + x + "']();" for x in global_initializer_funcs(initializers)]))
276+
277+
if shared.Settings.WASM:
278+
# Declare all exports out to global JS scope so that JS library functions can access them in a way that minifies well with Closure
279+
# e.g. var a,b,c,d,e,f;
280+
exports_that_are_not_initializers = [x for x in exports if x not in initializers]
281+
if shared.Settings.WASM_BACKEND:
282+
# In Wasm backend the exports are still unmangled at this point, so mangle the names here
283+
exports_that_are_not_initializers = [asmjs_mangle(x) for x in exports_that_are_not_initializers]
284+
post = post.replace('/*** ASM_MODULE_EXPORTS_DECLARES ***/', 'var ' + ','.join(exports_that_are_not_initializers) + ';')
285+
286+
# Generate assignments from all asm.js/wasm exports out to the JS variables above: e.g. a = asm['a']; b = asm['b'];
287+
post = post.replace('/*** ASM_MODULE_EXPORTS ***/', receiving)
288+
receiving = ''
289+
290+
return post, receiving
291+
292+
273293
def function_tables_and_exports(funcs, metadata, mem_init, glue, forwarded_data, outfile, DEBUG):
274294
if DEBUG:
275295
logger.debug('emscript: python processing: function tables and exports')
@@ -397,17 +417,7 @@ def define_asmjs_import_names(imports):
397417
post = apply_static_code_hooks(post)
398418

399419
if shared.Settings.MINIMAL_RUNTIME:
400-
# Generate invocations for all global initializers directly off the asm export object, e.g. asm['__GLOBAL__INIT']();
401-
post = post.replace('/*** RUN_GLOBAL_INITIALIZERS(); ***/', '\n'.join(["asm['" + x + "']();" for x in global_initializer_funcs(metadata['initializers'])]))
402-
403-
if shared.Settings.WASM:
404-
# Declare all exports out to global JS scope so that JS library functions can access them in a way that minifies well with Closure
405-
# e.g. var a,b,c,d,e,f;
406-
post = post.replace('/*** ASM_MODULE_EXPORTS_DECLARES ***/', 'var ' + ','.join(shared.Settings.MODULE_EXPORTS) + ';')
407-
408-
# Generate assignments from all asm.js/wasm exports out to the JS variables above: e.g. a = asm['a']; b = asm['b'];
409-
post = post.replace('/*** ASM_MODULE_EXPORTS ***/', receiving)
410-
receiving = ''
420+
post, receiving = compute_minimal_runtime_initializer_and_exports(post, metadata['initializers'], [mangled for mangled, unmangled in shared.Settings.MODULE_EXPORTS], receiving)
411421

412422
function_tables_impls = make_function_tables_impls(function_table_data)
413423
final_function_tables = '\n'.join(function_tables_impls) + '\n' + function_tables_defs
@@ -1710,7 +1720,8 @@ def create_receiving(function_table_data, function_tables_defs, exported_impleme
17101720
''' % {'name': name, 'runtime_assertions': runtime_assertions})
17111721
receiving = '\n'.join(wrappers)
17121722

1713-
shared.Settings.MODULE_EXPORTS = module_exports = exported_implemented_functions + function_tables(function_table_data)
1723+
module_exports = exported_implemented_functions + function_tables(function_table_data)
1724+
shared.Settings.MODULE_EXPORTS = [(f, f) for f in module_exports]
17141725

17151726
if not shared.Settings.SWAPPABLE_ASM_MODULE:
17161727
if shared.Settings.DECLARE_ASM_MODULE_EXPORTS:
@@ -2208,14 +2219,22 @@ def emscript_wasm_backend(infile, outfile, memfile, compiler_engine,
22082219

22092220
staticbump = shared.Settings.STATIC_BUMP
22102221

2222+
if shared.Settings.MINIMAL_RUNTIME:
2223+
# In minimal runtime, global initializers are run after the Wasm Module instantiation has finished.
2224+
global_initializers = ''
2225+
else:
2226+
# In regular runtime, global initializers are recorded in an __ATINIT__ array.
2227+
global_initializers = '''/* global initializers */ %s __ATINIT__.push(%s);
2228+
''' % ('if (!ENVIRONMENT_IS_PTHREAD)' if shared.Settings.USE_PTHREADS else '',
2229+
global_initializers)
2230+
22112231
pre = pre.replace('STATICTOP = STATIC_BASE + 0;', '''STATICTOP = STATIC_BASE + %d;
2212-
/* global initializers */ %s __ATINIT__.push(%s);
2213-
''' % (staticbump,
2214-
'if (!ENVIRONMENT_IS_PTHREAD)' if shared.Settings.USE_PTHREADS else '',
2215-
global_initializers))
2232+
%s
2233+
''' % (staticbump, global_initializers))
22162234

22172235
pre = apply_memory(pre)
2218-
pre = apply_static_code_hooks(pre)
2236+
pre = apply_static_code_hooks(pre) # In regular runtime, atinits etc. exist in the preamble part
2237+
post = apply_static_code_hooks(post) # In MINIMAL_RUNTIME, atinit exists in the postamble part
22192238

22202239
if shared.Settings.RELOCATABLE and not shared.Settings.SIDE_MODULE:
22212240
pre += 'var gb = GLOBAL_BASE, fb = 0;\n'
@@ -2225,6 +2244,10 @@ def emscript_wasm_backend(infile, outfile, memfile, compiler_engine,
22252244

22262245
exports = metadata['exports']
22272246

2247+
# Store exports for Closure compiler to be able to track these as globals in
2248+
# -s DECLARE_ASM_MODULE_EXPORTS=0 builds.
2249+
shared.Settings.MODULE_EXPORTS = [(asmjs_mangle(f), f) for f in exports]
2250+
22282251
if shared.Settings.ASYNCIFY:
22292252
exports += ['asyncify_start_unwind', 'asyncify_stop_unwind', 'asyncify_start_rewind', 'asyncify_stop_rewind']
22302253

@@ -2255,6 +2278,9 @@ def emscript_wasm_backend(infile, outfile, memfile, compiler_engine,
22552278
sending = create_sending_wasm(invoke_funcs, forwarded_json, metadata)
22562279
receiving = create_receiving_wasm(exports)
22572280

2281+
if shared.Settings.MINIMAL_RUNTIME:
2282+
post, receiving = compute_minimal_runtime_initializer_and_exports(post, metadata['initializers'], exports, receiving)
2283+
22582284
module = create_module_wasm(sending, receiving, invoke_funcs, metadata)
22592285

22602286
write_output_file(outfile, post, module)
@@ -2608,7 +2634,7 @@ def fix_import_name(g):
26082634

26092635
def create_receiving_wasm(exports):
26102636
receiving = []
2611-
if not shared.Settings.ASSERTIONS:
2637+
if shared.Settings.MINIMAL_RUNTIME or not shared.Settings.ASSERTIONS:
26122638
runtime_assertions = ''
26132639
else:
26142640
runtime_assertions = RUNTIME_ASSERTIONS
@@ -2623,8 +2649,42 @@ def create_receiving_wasm(exports):
26232649
''' % {'mangled': asmjs_mangle(e), 'e': e, 'assertions': runtime_assertions})
26242650

26252651
if not shared.Settings.SWAPPABLE_ASM_MODULE:
2626-
for e in exports:
2627-
receiving.append('var %(mangled)s = Module["%(mangled)s"] = asm["%(e)s"];' % {'mangled': asmjs_mangle(e), 'e': e})
2652+
if shared.Settings.DECLARE_ASM_MODULE_EXPORTS:
2653+
if shared.Settings.WASM and shared.Settings.MINIMAL_RUNTIME:
2654+
# In Wasm exports are assigned inside a function to variables existing in top level JS scope, i.e.
2655+
# var _main;
2656+
# WebAssembly.instantiate(Module["wasm"], imports).then((function(output) {
2657+
# var asm = output.instance.exports;
2658+
# _main = asm["_main"];
2659+
receiving += [asmjs_mangle(s) + ' = asm["' + s + '"];' for s in exports]
2660+
else:
2661+
if shared.Settings.MINIMAL_RUNTIME:
2662+
# In wasm2js exports can be directly processed at top level, i.e.
2663+
# var asm = Module["asm"](asmGlobalArg, asmLibraryArg, buffer);
2664+
# var _main = asm["_main"];
2665+
receiving += ['var ' + asmjs_mangle(s) + ' = asm["' + asmjs_mangle(s) + '"];' for s in exports]
2666+
else:
2667+
receiving += ['var ' + asmjs_mangle(s) + ' = Module["' + asmjs_mangle(s) + '"] = asm["' + s + '"];' for s in exports]
2668+
else:
2669+
if shared.Settings.target_environment_may_be('node') and shared.Settings.target_environment_may_be('web'):
2670+
global_object = '(typeof process !== "undefined" ? global : this)'
2671+
elif shared.Settings.target_environment_may_be('node'):
2672+
global_object = 'global'
2673+
else:
2674+
global_object = 'this'
2675+
2676+
if shared.Settings.MINIMAL_RUNTIME:
2677+
module_assign = ''
2678+
else:
2679+
module_assign = 'Module[asmjs_mangle(__exportedFunc)] = '
2680+
2681+
receiving.append('''
2682+
function asmjs_mangle(x) {
2683+
var unmangledSymbols = %s;
2684+
return x.indexOf('dynCall_') == 0 || unmangledSymbols.indexOf(x) != -1 ? x : '_' + x;
2685+
}
2686+
''' % shared.Settings.WASM_FUNCTIONS_THAT_ARE_NOT_NAME_MANGLED)
2687+
receiving.append('for(var __exportedFunc in asm) ' + global_object + '[asmjs_mangle(__exportedFunc)] = ' + module_assign + 'asm[__exportedFunc];')
26282688
else:
26292689
receiving.append('Module["asm"] = asm;')
26302690
for e in exports:
@@ -2650,7 +2710,8 @@ def create_module_wasm(sending, receiving, invoke_funcs, metadata):
26502710
if shared.Settings.ASYNCIFY and shared.Settings.ASSERTIONS:
26512711
module.append('Asyncify.instrumentWasmImports(asmLibraryArg);\n')
26522712

2653-
module.append("var asm = createWasm();\n")
2713+
if not shared.Settings.MINIMAL_RUNTIME:
2714+
module.append("var asm = createWasm();\n")
26542715

26552716
module.append(receiving)
26562717
module.append(invoke_wrappers)
@@ -2697,8 +2758,10 @@ def load_metadata_wasm(metadata_raw, DEBUG):
26972758
exit_with_error('unexpected metadata key received from wasm-emscripten-finalize: %s', key)
26982759
metadata[key] = value
26992760

2700-
# Initializers call the global var version of the export, so they get the mangled name.
2701-
metadata['initializers'] = [asmjs_mangle(i) for i in metadata['initializers']]
2761+
if not shared.Settings.MINIMAL_RUNTIME:
2762+
# In regular runtime initializers call the global var version of the export, so they get the mangled name.
2763+
# In MINIMAL_RUNTIME, the initializers are called directly off the export object for minimal code size.
2764+
metadata['initializers'] = [asmjs_mangle(i) for i in metadata['initializers']]
27022765

27032766
if DEBUG:
27042767
logger.debug("Metadata parsed: " + pprint.pformat(metadata))
@@ -2723,30 +2786,6 @@ def create_invoke_wrappers(invoke_funcs):
27232786
return invoke_wrappers
27242787

27252788

2726-
def treat_as_user_function(name):
2727-
library_functions_in_module = ('setTempRet0', 'getTempRet0', 'stackAlloc',
2728-
'stackSave', 'stackRestore',
2729-
'establishStackSpace', '__growWasmMemory',
2730-
'__heap_base', '__data_end')
2731-
if name.startswith('dynCall_'):
2732-
return False
2733-
if name in library_functions_in_module:
2734-
return False
2735-
return True
2736-
2737-
2738-
def asmjs_mangle(name):
2739-
"""Mangle a name the way asm.js/JSBackend globals are mangled.
2740-
2741-
Prepends '_' and replaces non-alphanumerics with '_'.
2742-
Used by wasm backend for JS library consistency with asm.js.
2743-
"""
2744-
if treat_as_user_function(name):
2745-
return '_' + name
2746-
else:
2747-
return name
2748-
2749-
27502789
def normalize_line_endings(text):
27512790
"""Normalize to UNIX line endings.
27522791

src/parseTools.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,3 +1631,12 @@ function makeRemovedFSAssert(fsName) {
16311631
if (SYSTEM_JS_LIBRARIES.indexOf('library_' + lower + '.js') >= 0) return '';
16321632
return "var " + fsName + " = '" + fsName + " is no longer included by default; build with -l" + lower + ".js';";
16331633
}
1634+
1635+
// Given an array of elements [elem1,elem2,elem3], returns a string "['elem1','elem2','elem3']"
1636+
function buildStringArray(array) {
1637+
if (array.length > 0) {
1638+
return "['" + array.join("','") + "']";
1639+
} else {
1640+
return [];
1641+
}
1642+
}

src/postamble_minimal.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,29 @@ WebAssembly.instantiate(Module['wasm'], imports).then(function(output) {
9191

9292
#if DECLARE_ASM_MODULE_EXPORTS == 0
9393

94+
#if WASM_BACKEND
95+
// XXX Hack: some function names need to be mangled when exporting them from wasm module, others do not.
96+
// https://github.com/emscripten-core/emscripten/issues/10054
97+
// Keep in sync with emscripten.py function treat_as_user_function(name).
98+
function asmjs_mangle(x) {
99+
var unmangledSymbols = {{{ buildStringArray(WASM_FUNCTIONS_THAT_ARE_NOT_NAME_MANGLED) }}};
100+
return x.indexOf('dynCall_') == 0 || unmangledSymbols.indexOf(x) != -1 ? x : '_' + x;
101+
}
102+
103+
#if ENVIRONMENT_MAY_BE_NODE
104+
for(var i in asm) (typeof process !== "undefined" ? global : this)[asmjs_mangle(i)] = asm[i];
105+
#else
106+
for(var i in asm) this[asmjs_mangle(i)] = asm[i];
107+
#endif
108+
109+
#else
110+
94111
#if ENVIRONMENT_MAY_BE_NODE
95-
for(var i in asm) (typeof process !== "undefined" ? global : this)[i] = Module[i] = asm[i];
112+
for(var i in asm) (typeof process !== "undefined" ? global : this)[i] = asm[i];
96113
#else
97-
for(var i in asm) this[i] = Module[i] = asm[i];
114+
for(var i in asm) this[i] = asm[i];
115+
#endif
116+
98117
#endif
99118

100119
#else

src/settings_internal.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,8 @@ var MINIFY_ASMJS_IMPORT_NAMES = 0;
156156

157157
// Internal: represents a browser version that is not supported at all.
158158
var TARGET_NOT_SUPPORTED = 0x7FFFFFFF;
159+
160+
// Wasm backend does not apply C name mangling (== prefix with an underscore) to
161+
// the following functions. (it also does not mangle any function that starts with
162+
// string "dynCall_")
163+
var WASM_FUNCTIONS_THAT_ARE_NOT_NAME_MANGLED = ['setTempRet0', 'getTempRet0', 'stackAlloc', 'stackSave', 'stackRestore', 'establishStackSpace', '__growWasmMemory', '__heap_base', '__data_end'];

tests/test_core.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,21 @@ def decorated(self, *args, **kwargs):
216216
return decorator
217217

218218

219+
def no_lsan(note):
220+
assert not callable(note)
221+
222+
def decorator(f):
223+
assert callable(f)
224+
225+
@wraps(f)
226+
def decorated(self, *args, **kwargs):
227+
if '-fsanitize=leak' in self.emcc_args:
228+
self.skipTest(note)
229+
f(self, *args, **kwargs)
230+
return decorated
231+
return decorator
232+
233+
219234
class TestCoreBase(RunnerCore):
220235
def is_wasm2js(self):
221236
return self.is_wasm_backend() and not self.get_setting('WASM')
@@ -8140,10 +8155,16 @@ def test_minimal_runtime_no_declare_asm_module_exports(self):
81408155

81418156
# Tests that -s MINIMAL_RUNTIME=1 works well in different build modes
81428157
@no_emterpreter
8143-
@no_wasm_backend('MINIMAL_RUNTIME not yet available in Wasm backend')
8158+
@no_asan('TODO: ASan with MINIMAL_RUNTIME')
8159+
@no_lsan('TODO: LSan with MINIMAL_RUNTIME')
8160+
@no_wasm2js('TODO: MINIMAL_RUNTIME with WASM2JS')
81448161
def test_minimal_runtime_hello_world(self):
8145-
for args in [[], ['-s', 'MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION=1']]:
8162+
if '-O2' in self.emcc_args or '-O3' in self.emcc_args or '-Os' in self.emcc_args or '-Oz' in self.emcc_args:
8163+
return self.skipTest('TODO: -O2 and higher with wasm backend')
8164+
self.banned_js_engines = [V8_ENGINE, SPIDERMONKEY_ENGINE] # TODO: Support for non-Node.js shells has not yet been added to MINIMAL_RUNTIME
8165+
for args in [[], ['-s', 'MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION=1'], ['-s', 'DECLARE_ASM_MODULE_EXPORTS=0']]:
81468166
self.emcc_args = ['-s', 'MINIMAL_RUNTIME=1'] + args
8167+
self.set_setting('MINIMAL_RUNTIME', 1)
81478168
self.maybe_closure()
81488169
self.do_run(open(path_from_root('tests', 'small_hello_world.c')).read(), 'hello')
81498170

tools/shared.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,26 @@ def demangle_c_symbol_name(name):
14461446
return name[1:] if name.startswith('_') else '$' + name
14471447

14481448

1449+
def treat_as_user_function(name):
1450+
if name.startswith('dynCall_'):
1451+
return False
1452+
if name in Settings.WASM_FUNCTIONS_THAT_ARE_NOT_NAME_MANGLED:
1453+
return False
1454+
return True
1455+
1456+
1457+
def asmjs_mangle(name):
1458+
"""Mangle a name the way asm.js/JSBackend globals are mangled.
1459+
1460+
Prepends '_' and replaces non-alphanumerics with '_'.
1461+
Used by wasm backend for JS library consistency with asm.js.
1462+
"""
1463+
if treat_as_user_function(name):
1464+
return '_' + name
1465+
else:
1466+
return name
1467+
1468+
14491469
# Building
14501470
class Building(object):
14511471
COMPILER = CLANG
@@ -2452,7 +2472,7 @@ def closure_compiler(filename, pretty=True, advanced=True, extra_closure_args=[]
24522472
# externs file for the exports, Closure is able to reason about the exports.
24532473
if Settings.MODULE_EXPORTS and not Settings.DECLARE_ASM_MODULE_EXPORTS:
24542474
# Generate an exports file that records all the exported symbols from asm.js/wasm module.
2455-
module_exports_suppressions = '\n'.join(['/**\n * @suppress {duplicate, undefinedVars}\n */\nvar %s;\n' % i for i in Settings.MODULE_EXPORTS])
2475+
module_exports_suppressions = '\n'.join(['/**\n * @suppress {duplicate, undefinedVars}\n */\nvar %s;\n' % i for i, j in Settings.MODULE_EXPORTS])
24562476
exports_file = configuration.get_temp_files().get('_module_exports.js')
24572477
exports_file.write(module_exports_suppressions.encode())
24582478
exports_file.close()
@@ -2564,7 +2584,7 @@ def metadce(js_file, wasm_file, minify_whitespace, debug_info):
25642584
logger.debug('running meta-DCE')
25652585
temp_files = configuration.get_temp_files()
25662586
# first, get the JS part of the graph
2567-
extra_info = '{ "exports": [' + ','.join(map(lambda x: '["' + x + '","' + x + '"]', Settings.MODULE_EXPORTS)) + ']}'
2587+
extra_info = '{ "exports": [' + ','.join(map(lambda x: '["' + x[0] + '","' + x[1] + '"]', Settings.MODULE_EXPORTS)) + ']}'
25682588
txt = Building.acorn_optimizer(js_file, ['emitDCEGraph', 'noPrint'], return_output=True, extra_info=extra_info)
25692589
graph = json.loads(txt)
25702590
# add exports based on the backend output, that are not present in the JS
@@ -2573,7 +2593,7 @@ def metadce(js_file, wasm_file, minify_whitespace, debug_info):
25732593
for item in graph:
25742594
if 'export' in item:
25752595
exports.add(item['export'])
2576-
for export in Settings.MODULE_EXPORTS:
2596+
for export, unminified in Settings.MODULE_EXPORTS:
25772597
if export not in exports:
25782598
graph.append({
25792599
'export': export,

0 commit comments

Comments
 (0)