diff --git a/emscripten.py b/emscripten.py index 6b3e12258fa09..e54ceb26b9fb2 100644 --- a/emscripten.py +++ b/emscripten.py @@ -25,7 +25,7 @@ from tools import gen_struct_info from tools import jsrun from tools.response_file import substitute_response_files -from tools.shared import WINDOWS, asstr, path_from_root, exit_with_error +from tools.shared import WINDOWS, asstr, path_from_root, exit_with_error, asmjs_mangle, treat_as_user_function from tools.toolchain_profiler import ToolchainProfiler from tools.minified_js_name_generator import MinifiedJsNameGenerator @@ -270,6 +270,26 @@ def get_asm_extern_primitives(pre): return [] +def compute_minimal_runtime_initializer_and_exports(post, initializers, exports, receiving): + # Generate invocations for all global initializers directly off the asm export object, e.g. asm['__GLOBAL__INIT'](); + post = post.replace('/*** RUN_GLOBAL_INITIALIZERS(); ***/', '\n'.join(["asm['" + x + "']();" for x in global_initializer_funcs(initializers)])) + + if shared.Settings.WASM: + # Declare all exports out to global JS scope so that JS library functions can access them in a way that minifies well with Closure + # e.g. var a,b,c,d,e,f; + exports_that_are_not_initializers = [x for x in exports if x not in initializers] + if shared.Settings.WASM_BACKEND: + # In Wasm backend the exports are still unmangled at this point, so mangle the names here + exports_that_are_not_initializers = [asmjs_mangle(x) for x in exports_that_are_not_initializers] + post = post.replace('/*** ASM_MODULE_EXPORTS_DECLARES ***/', 'var ' + ','.join(exports_that_are_not_initializers) + ';') + + # Generate assignments from all asm.js/wasm exports out to the JS variables above: e.g. a = asm['a']; b = asm['b']; + post = post.replace('/*** ASM_MODULE_EXPORTS ***/', receiving) + receiving = '' + + return post, receiving + + def function_tables_and_exports(funcs, metadata, mem_init, glue, forwarded_data, outfile, DEBUG): if DEBUG: logger.debug('emscript: python processing: function tables and exports') @@ -397,17 +417,7 @@ def define_asmjs_import_names(imports): post = apply_static_code_hooks(post) if shared.Settings.MINIMAL_RUNTIME: - # Generate invocations for all global initializers directly off the asm export object, e.g. asm['__GLOBAL__INIT'](); - post = post.replace('/*** RUN_GLOBAL_INITIALIZERS(); ***/', '\n'.join(["asm['" + x + "']();" for x in global_initializer_funcs(metadata['initializers'])])) - - if shared.Settings.WASM: - # Declare all exports out to global JS scope so that JS library functions can access them in a way that minifies well with Closure - # e.g. var a,b,c,d,e,f; - post = post.replace('/*** ASM_MODULE_EXPORTS_DECLARES ***/', 'var ' + ','.join(shared.Settings.MODULE_EXPORTS) + ';') - - # Generate assignments from all asm.js/wasm exports out to the JS variables above: e.g. a = asm['a']; b = asm['b']; - post = post.replace('/*** ASM_MODULE_EXPORTS ***/', receiving) - receiving = '' + post, receiving = compute_minimal_runtime_initializer_and_exports(post, metadata['initializers'], [mangled for mangled, unmangled in shared.Settings.MODULE_EXPORTS], receiving) function_tables_impls = make_function_tables_impls(function_table_data) 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 ''' % {'name': name, 'runtime_assertions': runtime_assertions}) receiving = '\n'.join(wrappers) - shared.Settings.MODULE_EXPORTS = module_exports = exported_implemented_functions + function_tables(function_table_data) + module_exports = exported_implemented_functions + function_tables(function_table_data) + shared.Settings.MODULE_EXPORTS = [(f, f) for f in module_exports] if not shared.Settings.SWAPPABLE_ASM_MODULE: if shared.Settings.DECLARE_ASM_MODULE_EXPORTS: @@ -2208,14 +2219,22 @@ def emscript_wasm_backend(infile, outfile, memfile, compiler_engine, staticbump = shared.Settings.STATIC_BUMP + if shared.Settings.MINIMAL_RUNTIME: + # In minimal runtime, global initializers are run after the Wasm Module instantiation has finished. + global_initializers = '' + else: + # In regular runtime, global initializers are recorded in an __ATINIT__ array. + global_initializers = '''/* global initializers */ %s __ATINIT__.push(%s); +''' % ('if (!ENVIRONMENT_IS_PTHREAD)' if shared.Settings.USE_PTHREADS else '', + global_initializers) + pre = pre.replace('STATICTOP = STATIC_BASE + 0;', '''STATICTOP = STATIC_BASE + %d; -/* global initializers */ %s __ATINIT__.push(%s); -''' % (staticbump, - 'if (!ENVIRONMENT_IS_PTHREAD)' if shared.Settings.USE_PTHREADS else '', - global_initializers)) +%s +''' % (staticbump, global_initializers)) pre = apply_memory(pre) - pre = apply_static_code_hooks(pre) + pre = apply_static_code_hooks(pre) # In regular runtime, atinits etc. exist in the preamble part + post = apply_static_code_hooks(post) # In MINIMAL_RUNTIME, atinit exists in the postamble part if shared.Settings.RELOCATABLE and not shared.Settings.SIDE_MODULE: pre += 'var gb = GLOBAL_BASE, fb = 0;\n' @@ -2225,6 +2244,10 @@ def emscript_wasm_backend(infile, outfile, memfile, compiler_engine, exports = metadata['exports'] + # Store exports for Closure compiler to be able to track these as globals in + # -s DECLARE_ASM_MODULE_EXPORTS=0 builds. + shared.Settings.MODULE_EXPORTS = [(asmjs_mangle(f), f) for f in exports] + if shared.Settings.ASYNCIFY: exports += ['asyncify_start_unwind', 'asyncify_stop_unwind', 'asyncify_start_rewind', 'asyncify_stop_rewind'] @@ -2255,6 +2278,9 @@ def emscript_wasm_backend(infile, outfile, memfile, compiler_engine, sending = create_sending_wasm(invoke_funcs, forwarded_json, metadata) receiving = create_receiving_wasm(exports) + if shared.Settings.MINIMAL_RUNTIME: + post, receiving = compute_minimal_runtime_initializer_and_exports(post, metadata['initializers'], exports, receiving) + module = create_module_wasm(sending, receiving, invoke_funcs, metadata) write_output_file(outfile, post, module) @@ -2608,7 +2634,7 @@ def fix_import_name(g): def create_receiving_wasm(exports): receiving = [] - if not shared.Settings.ASSERTIONS: + if shared.Settings.MINIMAL_RUNTIME or not shared.Settings.ASSERTIONS: runtime_assertions = '' else: runtime_assertions = RUNTIME_ASSERTIONS @@ -2623,8 +2649,42 @@ def create_receiving_wasm(exports): ''' % {'mangled': asmjs_mangle(e), 'e': e, 'assertions': runtime_assertions}) if not shared.Settings.SWAPPABLE_ASM_MODULE: - for e in exports: - receiving.append('var %(mangled)s = Module["%(mangled)s"] = asm["%(e)s"];' % {'mangled': asmjs_mangle(e), 'e': e}) + if shared.Settings.DECLARE_ASM_MODULE_EXPORTS: + if shared.Settings.WASM and shared.Settings.MINIMAL_RUNTIME: + # In Wasm exports are assigned inside a function to variables existing in top level JS scope, i.e. + # var _main; + # WebAssembly.instantiate(Module["wasm"], imports).then((function(output) { + # var asm = output.instance.exports; + # _main = asm["_main"]; + receiving += [asmjs_mangle(s) + ' = asm["' + s + '"];' for s in exports] + else: + if shared.Settings.MINIMAL_RUNTIME: + # In wasm2js exports can be directly processed at top level, i.e. + # var asm = Module["asm"](asmGlobalArg, asmLibraryArg, buffer); + # var _main = asm["_main"]; + receiving += ['var ' + asmjs_mangle(s) + ' = asm["' + asmjs_mangle(s) + '"];' for s in exports] + else: + receiving += ['var ' + asmjs_mangle(s) + ' = Module["' + asmjs_mangle(s) + '"] = asm["' + s + '"];' for s in exports] + else: + if shared.Settings.target_environment_may_be('node') and shared.Settings.target_environment_may_be('web'): + global_object = '(typeof process !== "undefined" ? global : this)' + elif shared.Settings.target_environment_may_be('node'): + global_object = 'global' + else: + global_object = 'this' + + if shared.Settings.MINIMAL_RUNTIME: + module_assign = '' + else: + module_assign = 'Module[asmjs_mangle(__exportedFunc)] = ' + + receiving.append(''' + function asmjs_mangle(x) { + var unmangledSymbols = %s; + return x.indexOf('dynCall_') == 0 || unmangledSymbols.indexOf(x) != -1 ? x : '_' + x; + } +''' % shared.Settings.WASM_FUNCTIONS_THAT_ARE_NOT_NAME_MANGLED) + receiving.append('for(var __exportedFunc in asm) ' + global_object + '[asmjs_mangle(__exportedFunc)] = ' + module_assign + 'asm[__exportedFunc];') else: receiving.append('Module["asm"] = asm;') for e in exports: @@ -2650,7 +2710,8 @@ def create_module_wasm(sending, receiving, invoke_funcs, metadata): if shared.Settings.ASYNCIFY and shared.Settings.ASSERTIONS: module.append('Asyncify.instrumentWasmImports(asmLibraryArg);\n') - module.append("var asm = createWasm();\n") + if not shared.Settings.MINIMAL_RUNTIME: + module.append("var asm = createWasm();\n") module.append(receiving) module.append(invoke_wrappers) @@ -2697,8 +2758,10 @@ def load_metadata_wasm(metadata_raw, DEBUG): exit_with_error('unexpected metadata key received from wasm-emscripten-finalize: %s', key) metadata[key] = value - # Initializers call the global var version of the export, so they get the mangled name. - metadata['initializers'] = [asmjs_mangle(i) for i in metadata['initializers']] + if not shared.Settings.MINIMAL_RUNTIME: + # In regular runtime initializers call the global var version of the export, so they get the mangled name. + # In MINIMAL_RUNTIME, the initializers are called directly off the export object for minimal code size. + metadata['initializers'] = [asmjs_mangle(i) for i in metadata['initializers']] if DEBUG: logger.debug("Metadata parsed: " + pprint.pformat(metadata)) @@ -2723,30 +2786,6 @@ def create_invoke_wrappers(invoke_funcs): return invoke_wrappers -def treat_as_user_function(name): - library_functions_in_module = ('setTempRet0', 'getTempRet0', 'stackAlloc', - 'stackSave', 'stackRestore', - 'establishStackSpace', '__growWasmMemory', - '__heap_base', '__data_end') - if name.startswith('dynCall_'): - return False - if name in library_functions_in_module: - return False - return True - - -def asmjs_mangle(name): - """Mangle a name the way asm.js/JSBackend globals are mangled. - - Prepends '_' and replaces non-alphanumerics with '_'. - Used by wasm backend for JS library consistency with asm.js. - """ - if treat_as_user_function(name): - return '_' + name - else: - return name - - def normalize_line_endings(text): """Normalize to UNIX line endings. diff --git a/src/parseTools.js b/src/parseTools.js index 8b1e44f8be5a6..c4a7b4b43348c 100644 --- a/src/parseTools.js +++ b/src/parseTools.js @@ -1631,3 +1631,12 @@ function makeRemovedFSAssert(fsName) { if (SYSTEM_JS_LIBRARIES.indexOf('library_' + lower + '.js') >= 0) return ''; return "var " + fsName + " = '" + fsName + " is no longer included by default; build with -l" + lower + ".js';"; } + +// Given an array of elements [elem1,elem2,elem3], returns a string "['elem1','elem2','elem3']" +function buildStringArray(array) { + if (array.length > 0) { + return "['" + array.join("','") + "']"; + } else { + return []; + } +} diff --git a/src/postamble_minimal.js b/src/postamble_minimal.js index d98d7d66de98c..2ef3cc95a3555 100644 --- a/src/postamble_minimal.js +++ b/src/postamble_minimal.js @@ -91,10 +91,29 @@ WebAssembly.instantiate(Module['wasm'], imports).then(function(output) { #if DECLARE_ASM_MODULE_EXPORTS == 0 +#if WASM_BACKEND + // XXX Hack: some function names need to be mangled when exporting them from wasm module, others do not. + // https://github.com/emscripten-core/emscripten/issues/10054 + // Keep in sync with emscripten.py function treat_as_user_function(name). + function asmjs_mangle(x) { + var unmangledSymbols = {{{ buildStringArray(WASM_FUNCTIONS_THAT_ARE_NOT_NAME_MANGLED) }}}; + return x.indexOf('dynCall_') == 0 || unmangledSymbols.indexOf(x) != -1 ? x : '_' + x; + } + +#if ENVIRONMENT_MAY_BE_NODE + for(var i in asm) (typeof process !== "undefined" ? global : this)[asmjs_mangle(i)] = asm[i]; +#else + for(var i in asm) this[asmjs_mangle(i)] = asm[i]; +#endif + +#else + #if ENVIRONMENT_MAY_BE_NODE - for(var i in asm) (typeof process !== "undefined" ? global : this)[i] = Module[i] = asm[i]; + for(var i in asm) (typeof process !== "undefined" ? global : this)[i] = asm[i]; #else - for(var i in asm) this[i] = Module[i] = asm[i]; + for(var i in asm) this[i] = asm[i]; +#endif + #endif #else diff --git a/src/settings_internal.js b/src/settings_internal.js index 336f71675a699..92f783be1e411 100644 --- a/src/settings_internal.js +++ b/src/settings_internal.js @@ -156,3 +156,8 @@ var MINIFY_ASMJS_IMPORT_NAMES = 0; // Internal: represents a browser version that is not supported at all. var TARGET_NOT_SUPPORTED = 0x7FFFFFFF; + +// Wasm backend does not apply C name mangling (== prefix with an underscore) to +// the following functions. (it also does not mangle any function that starts with +// string "dynCall_") +var WASM_FUNCTIONS_THAT_ARE_NOT_NAME_MANGLED = ['setTempRet0', 'getTempRet0', 'stackAlloc', 'stackSave', 'stackRestore', 'establishStackSpace', '__growWasmMemory', '__heap_base', '__data_end']; diff --git a/tests/test_core.py b/tests/test_core.py index 48e53225726c1..c5358a04dda31 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -216,6 +216,21 @@ def decorated(self, *args, **kwargs): return decorator +def no_lsan(note): + assert not callable(note) + + def decorator(f): + assert callable(f) + + @wraps(f) + def decorated(self, *args, **kwargs): + if '-fsanitize=leak' in self.emcc_args: + self.skipTest(note) + f(self, *args, **kwargs) + return decorated + return decorator + + class TestCoreBase(RunnerCore): def is_wasm2js(self): return self.is_wasm_backend() and not self.get_setting('WASM') @@ -8139,10 +8154,16 @@ def test_minimal_runtime_no_declare_asm_module_exports(self): # Tests that -s MINIMAL_RUNTIME=1 works well in different build modes @no_emterpreter - @no_wasm_backend('MINIMAL_RUNTIME not yet available in Wasm backend') + @no_asan('TODO: ASan with MINIMAL_RUNTIME') + @no_lsan('TODO: LSan with MINIMAL_RUNTIME') + @no_wasm2js('TODO: MINIMAL_RUNTIME with WASM2JS') def test_minimal_runtime_hello_world(self): - for args in [[], ['-s', 'MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION=1']]: + if '-O2' in self.emcc_args or '-O3' in self.emcc_args or '-Os' in self.emcc_args or '-Oz' in self.emcc_args: + return self.skipTest('TODO: -O2 and higher with wasm backend') + self.banned_js_engines = [V8_ENGINE, SPIDERMONKEY_ENGINE] # TODO: Support for non-Node.js shells has not yet been added to MINIMAL_RUNTIME + for args in [[], ['-s', 'MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION=1'], ['-s', 'DECLARE_ASM_MODULE_EXPORTS=0']]: self.emcc_args = ['-s', 'MINIMAL_RUNTIME=1'] + args + self.set_setting('MINIMAL_RUNTIME', 1) self.maybe_closure() self.do_run(open(path_from_root('tests', 'small_hello_world.c')).read(), 'hello') diff --git a/tools/shared.py b/tools/shared.py index 451b3c55fa31e..4031021a8a12f 100644 --- a/tools/shared.py +++ b/tools/shared.py @@ -1446,6 +1446,26 @@ def demangle_c_symbol_name(name): return name[1:] if name.startswith('_') else '$' + name +def treat_as_user_function(name): + if name.startswith('dynCall_'): + return False + if name in Settings.WASM_FUNCTIONS_THAT_ARE_NOT_NAME_MANGLED: + return False + return True + + +def asmjs_mangle(name): + """Mangle a name the way asm.js/JSBackend globals are mangled. + + Prepends '_' and replaces non-alphanumerics with '_'. + Used by wasm backend for JS library consistency with asm.js. + """ + if treat_as_user_function(name): + return '_' + name + else: + return name + + # Building class Building(object): COMPILER = CLANG @@ -2452,7 +2472,7 @@ def closure_compiler(filename, pretty=True, advanced=True, extra_closure_args=[] # externs file for the exports, Closure is able to reason about the exports. if Settings.MODULE_EXPORTS and not Settings.DECLARE_ASM_MODULE_EXPORTS: # Generate an exports file that records all the exported symbols from asm.js/wasm module. - module_exports_suppressions = '\n'.join(['/**\n * @suppress {duplicate, undefinedVars}\n */\nvar %s;\n' % i for i in Settings.MODULE_EXPORTS]) + module_exports_suppressions = '\n'.join(['/**\n * @suppress {duplicate, undefinedVars}\n */\nvar %s;\n' % i for i, j in Settings.MODULE_EXPORTS]) exports_file = configuration.get_temp_files().get('_module_exports.js') exports_file.write(module_exports_suppressions.encode()) exports_file.close() @@ -2564,7 +2584,7 @@ def metadce(js_file, wasm_file, minify_whitespace, debug_info): logger.debug('running meta-DCE') temp_files = configuration.get_temp_files() # first, get the JS part of the graph - extra_info = '{ "exports": [' + ','.join(map(lambda x: '["' + x + '","' + x + '"]', Settings.MODULE_EXPORTS)) + ']}' + extra_info = '{ "exports": [' + ','.join(map(lambda x: '["' + x[0] + '","' + x[1] + '"]', Settings.MODULE_EXPORTS)) + ']}' txt = Building.acorn_optimizer(js_file, ['emitDCEGraph', 'noPrint'], return_output=True, extra_info=extra_info) graph = json.loads(txt) # 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): for item in graph: if 'export' in item: exports.add(item['export']) - for export in Settings.MODULE_EXPORTS: + for export, unminified in Settings.MODULE_EXPORTS: if export not in exports: graph.append({ 'export': export,