Skip to content
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
135 changes: 87 additions & 48 deletions emscripten.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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'
Expand All @@ -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']

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does wasm2js output differ from wasm output here? in general in the wasm backend the js/wasm paths are much more similar than in fastcomp (wasm2js literally polyfills the WebAssembly APIs, and everything should be identical except for that), so I was hoping we would not have differences here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know yet. I have not yet tried against wasm2js. I just put this like this to follow the fastcomp route, and did not go too far making modifications to keep the amount to review small for each PR.

# 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:
Expand All @@ -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)
Expand Down Expand Up @@ -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))
Expand All @@ -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.

Expand Down
9 changes: 9 additions & 0 deletions src/parseTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be a string too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would certainly help the code to work..

}
}
23 changes: 21 additions & 2 deletions src/postamble_minimal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/settings_internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
25 changes: 23 additions & 2 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')

Expand Down
26 changes: 23 additions & 3 deletions tools/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down