diff --git a/src/closure-externs/closure-externs.js b/src/closure-externs/closure-externs.js index 86495adcc15a4..361fa16554d53 100644 --- a/src/closure-externs/closure-externs.js +++ b/src/closure-externs/closure-externs.js @@ -14,6 +14,7 @@ // Special placeholder for `import.meta` and `await import`. var EMSCRIPTEN$IMPORT$META; var EMSCRIPTEN$AWAIT$IMPORT; +var EMSCRIPTEN$EXPORT$DEFAULT; // Don't minify createRequire var createRequire; @@ -160,9 +161,10 @@ var wakaUnknownBefore; // Module loaders externs, for AMD etc. /** + * @param {Object} deps * @param {Function} wrapper */ -var define = function (wrapper) {}; +var define = function (deps, wrapper) {}; /** * @type {Worker} @@ -231,20 +233,6 @@ var sampleRate; */ var id; -/** - * Used in MODULARIZE mode as the name of the incoming module argument. - * This is generated outside of the code we pass to closure so from closure's - * POV this is "extern". - */ -var moduleArg; - -/** - * Used in MODULARIZE mode. - * We need to access this after the code we pass to closure so from closure's - * POV this is "extern". - */ -var moduleRtn; - /** * This was removed from upstream closure compiler in * https://github.com/google/closure-compiler/commit/f83322c1b. diff --git a/src/closure-externs/modularize-externs.js b/src/closure-externs/modularize-externs.js deleted file mode 100644 index fd29362e99559..0000000000000 --- a/src/closure-externs/modularize-externs.js +++ /dev/null @@ -1,7 +0,0 @@ -// Due to the way MODULARIZE works, Closure is run on generated code that does not define _scriptName, -// but only after MODULARIZE has finished, _scriptName is injected to the generated code. -// Therefore it cannot be minified. -/** - * @suppress {duplicate, undefinedVars} - */ -var _scriptName; diff --git a/src/parseTools.mjs b/src/parseTools.mjs index 509ff01727b44..8e3509e5af3f4 100644 --- a/src/parseTools.mjs +++ b/src/parseTools.mjs @@ -42,16 +42,17 @@ export function processMacros(text, filename) { // Simple #if/else/endif preprocessing for a file. Checks if the // ident checked is true in our global. // Also handles #include x.js (similar to C #include ) -export function preprocess(filename) { +export function preprocess(filename, closureFriendly = true) { let text = read(filename); - if (EXPORT_ES6 && USE_ES6_IMPORT_META) { + if (closureFriendly && EXPORT_ES6 && USE_ES6_IMPORT_META) { // `eval`, Terser and Closure don't support module syntax; to allow it, // we need to temporarily replace `import.meta` and `await import` usages // with placeholders during preprocess phase, and back after all the other ops. // See also: `phase_final_emitting` in emcc.py. text = text .replace(/\bimport\.meta\b/g, 'EMSCRIPTEN$IMPORT$META') - .replace(/\bawait import\b/g, 'EMSCRIPTEN$AWAIT$IMPORT'); + .replace(/\bawait import\b/g, 'EMSCRIPTEN$AWAIT$IMPORT') + .replace(/\bexport default\b/g, 'EMSCRIPTEN$EXPORT$DEFAULT ='); } // Remove windows line endings, if any text = text.replace(/\r\n/g, '\n'); @@ -1069,6 +1070,31 @@ function ENVIRONMENT_IS_WORKER_THREAD() { return '(' + envs.join('||') + ')'; } +function nodePthreadDetection() { + // Under node we detect that we are running in a pthread by checking the + // workerData property. + if (EXPORT_ES6) { + return "(await import('worker_threads')).workerData === 'em-pthread'"; + } else { + return "require('worker_threads').workerData === 'em-pthread'"; + } +} + +function declareInstanceExports() { + const allExports = Array.from(EXPORTED_FUNCTIONS.keys()).concat( + Array.from(EXPORTED_RUNTIME_METHODS.keys()), + ); + const mangledExports = allExports.map((e) => `__exp_${e}`); + const mangledExportsAs = allExports.map((e) => `__exp_${e} as ${e}`); + // Declare a top level var for each export so that code in the init function + // can assign to it and update the live module bindings. + if (allExports.length == 0) return ''; + let rtn = 'var ' + mangledExports.join(', ') + ';\n'; + // Export the functions with their original name. + rtn += 'export {' + mangledExportsAs.join(', ') + '};\n'; + return rtn; +} + addToCompileTimeContext({ ATEXITS, ATINITS, @@ -1134,4 +1160,6 @@ addToCompileTimeContext({ storeException, to64, toIndexType, + nodePthreadDetection, + declareInstanceExports, }); diff --git a/src/postamble_modularize.js b/src/postamble_modularize.js index d8d3a59a4671e..c563f91c537b0 100644 --- a/src/postamble_modularize.js +++ b/src/postamble_modularize.js @@ -6,18 +6,6 @@ #if WASM_ASYNC_COMPILATION -#if USE_READY_PROMISE -moduleRtn = readyPromise; -#else -moduleRtn = {}; -#endif - -#else // WASM_ASYNC_COMPILATION - -moduleRtn = Module; - -#endif // WASM_ASYNC_COMPILATION - #if ASSERTIONS // Assertion for attempting to access module properties on the incoming // moduleArg. In the past we used this object as the prototype of the module @@ -35,3 +23,70 @@ for (const prop of Object.keys(Module)) { } } #endif + +#if USE_READY_PROMISE + return readyPromise; +#else + return {}; +#endif +#else // WASM_ASYNC_COMPILATION + return Module; +#endif // WASM_ASYNC_COMPILATION +}; // End factory function + +#if ASSERTIONS && MODULARIZE != 'instance' +(() => { + // Create a small, never-async wrapper around {{{ EXPORT_NAME }}} which + // checks for callers incorrectly using it with `new`. + var real_{{{ EXPORT_NAME }}} = {{{ EXPORT_NAME }}}; + {{{ EXPORT_NAME }}} = function(arg) { + if (new.target) throw new Error("{{{ EXPORT_NAME }}}() should not be called with `new {{{ EXPORT_NAME }}}()`"); + return real_{{{ EXPORT_NAME }}}(arg); + } +})(); +#endif + +// Export using a UMD style export, or ES6 exports if selected +#if EXPORT_ES6 +#if MODULARIZE == 'instance' +{{{ declareInstanceExports() }}} +#else +export default {{{ EXPORT_NAME }}}; +#endif +#else +if (typeof exports === 'object' && typeof module === 'object') { + module.exports = {{{ EXPORT_NAME }}}; + // This default export looks redundant, but it allows TS to import this + // commonjs style module. + module.exports.default = {{{ EXPORT_NAME }}}; +} else if (typeof define === 'function' && define['amd']) { + define([], () => {{{ EXPORT_NAME }}}); +} +#endif + + +#if PTHREADS + +// Create code for detecting if we are running in a pthread. +// Normally this detection is done when the module is itself run but +// when running in MODULARIZE mode we need use this to know if we should +// run the module constructor on startup (true only for pthreads). +#if ENVIRONMENT_MAY_BE_WEB || ENVIRONMENT_MAY_BE_WORKER +var isPthread = globalThis.self?.name?.startsWith('em-pthread'); +#if ENVIRONMENT_MAY_BE_NODE +// In order to support both web and node we also need to detect node here. +var isNode = typeof globalThis.process?.versions?.node == 'string'; +if (isNode) isPthread = {{{ nodePthreadDetection() }}}; +#endif +#elif ENVIRONMENT_MAY_BE_NODE +var isPthread = {{{ nodePthreadDetection() }}}; +#endif ENVIRONMENT_MAY_BE_WEB || ENVIRONMENT_MAY_BE_WORKER + +// When running as a pthread, construct a new instance on startup +#if MODULARIZE == 'instance' +isPthread && init(); +#else +isPthread && {{{ EXPORT_NAME }}}(); +#endif + +#endif // PTHREADS diff --git a/src/preamble_modularize.js b/src/preamble_modularize.js new file mode 100644 index 0000000000000..8f65a036450b8 --- /dev/null +++ b/src/preamble_modularize.js @@ -0,0 +1,18 @@ +#if !MINIMAL_RUNTIME || PTHREADS +#if EXPORT_ES6 && USE_ES6_IMPORT_META +var _scriptName = import.meta.url; +#else +var _scriptName = typeof document != 'undefined' ? document.currentScript?.src : undefined; +#if ENVIRONMENT_MAY_BE_NODE +if (typeof __filename != 'undefined') _scriptName = _scriptName || __filename; +#endif +#endif +#endif + +#if MODULARIZE == 'instance' +export default {{{ asyncIf(WASM_ASYNC_COMPILATION || (EXPORT_ES6 && ENVIRONMENT_MAY_BE_NODE)) }}}function init(moduleArg = {}) { +#else +var {{{ EXPORT_NAME }}} = {{{ asyncIf(WASM_ASYNC_COMPILATION || (EXPORT_ES6 && ENVIRONMENT_MAY_BE_NODE)) }}}function(moduleArg = {}) { +#endif + +var Module = moduleArg; diff --git a/src/shell.js b/src/shell.js index b813e3edbcf44..399afd4aa861f 100644 --- a/src/shell.js +++ b/src/shell.js @@ -21,7 +21,7 @@ // before the code. Then that object will be used in the code, and you // can continue to use Module afterwards as well. #if MODULARIZE -var Module = moduleArg; +#include "preamble_modularize.js" #elif USE_CLOSURE_COMPILER /** @type{Object} */ var Module; diff --git a/src/shell_minimal.js b/src/shell_minimal.js index 4ddf6b3b0303c..4318bcd8d6d4b 100644 --- a/src/shell_minimal.js +++ b/src/shell_minimal.js @@ -5,7 +5,7 @@ */ #if MODULARIZE -var Module = moduleArg; +#include "preamble_modularize.js" #elif USE_CLOSURE_COMPILER /** @type{Object} */ var Module; diff --git a/test/code_size/hello_webgl2_wasm.json b/test/code_size/hello_webgl2_wasm.json index 57518ddaba94c..245a59c58a717 100644 --- a/test/code_size/hello_webgl2_wasm.json +++ b/test/code_size/hello_webgl2_wasm.json @@ -1,10 +1,10 @@ { "a.html": 454, "a.html.gz": 328, - "a.js": 4538, - "a.js.gz": 2320, + "a.js": 6293, + "a.js.gz": 3087, "a.wasm": 10206, "a.wasm.gz": 6663, - "total": 15198, - "total_gz": 9311 + "total": 16953, + "total_gz": 10078 } diff --git a/test/code_size/hello_webgl2_wasm2js.json b/test/code_size/hello_webgl2_wasm2js.json index 3074c7154d437..ae41177a32d35 100644 --- a/test/code_size/hello_webgl2_wasm2js.json +++ b/test/code_size/hello_webgl2_wasm2js.json @@ -1,8 +1,8 @@ { "a.html": 346, "a.html.gz": 262, - "a.js": 22202, - "a.js.gz": 11604, - "total": 22548, - "total_gz": 11866 + "a.js": 24118, + "a.js.gz": 12472, + "total": 24464, + "total_gz": 12734 } diff --git a/test/code_size/hello_webgl_wasm.json b/test/code_size/hello_webgl_wasm.json index 1aad262f62aa7..99bee8be1aefe 100644 --- a/test/code_size/hello_webgl_wasm.json +++ b/test/code_size/hello_webgl_wasm.json @@ -1,10 +1,10 @@ { "a.html": 454, "a.html.gz": 328, - "a.js": 4076, - "a.js.gz": 2163, + "a.js": 5799, + "a.js.gz": 2911, "a.wasm": 10206, "a.wasm.gz": 6663, - "total": 14736, - "total_gz": 9154 + "total": 16459, + "total_gz": 9902 } diff --git a/test/code_size/hello_webgl_wasm2js.json b/test/code_size/hello_webgl_wasm2js.json index c0b731a9418e9..78a2bc41b44b9 100644 --- a/test/code_size/hello_webgl_wasm2js.json +++ b/test/code_size/hello_webgl_wasm2js.json @@ -1,8 +1,8 @@ { "a.html": 346, "a.html.gz": 262, - "a.js": 21728, - "a.js.gz": 11435, - "total": 22074, - "total_gz": 11697 + "a.js": 23615, + "a.js.gz": 12296, + "total": 23961, + "total_gz": 12558 } diff --git a/test/modularize_post_js.js b/test/modularize_post_js.js index 18ad0c15b8d6e..6f6b44484358c 100644 --- a/test/modularize_post_js.js +++ b/test/modularize_post_js.js @@ -5,6 +5,11 @@ #if PTHREADS // Avoid instantiating the module on pthreads. -if (!isPthread) +#if EXPORT_ES6 +const isMainThread = (await import('worker_threads')).isMainThread; +#else +const { isMainThread } = require('worker_threads'); +#endif +if (isMainThread) #endif {{{ EXPORT_NAME }}}(); diff --git a/test/other/codesize/test_codesize_minimal_esm.gzsize b/test/other/codesize/test_codesize_minimal_esm.gzsize index 27600766db2b7..f02edd5f91078 100644 --- a/test/other/codesize/test_codesize_minimal_esm.gzsize +++ b/test/other/codesize/test_codesize_minimal_esm.gzsize @@ -1 +1 @@ -1541 +1520 diff --git a/test/other/codesize/test_codesize_minimal_esm.jssize b/test/other/codesize/test_codesize_minimal_esm.jssize index 465a35cfc4a0f..9a1248bac25eb 100644 --- a/test/other/codesize/test_codesize_minimal_esm.jssize +++ b/test/other/codesize/test_codesize_minimal_esm.jssize @@ -1 +1 @@ -3210 +3112 diff --git a/tools/acorn-optimizer.mjs b/tools/acorn-optimizer.mjs index 61ddfed3d69ca..5c1affae56863 100755 --- a/tools/acorn-optimizer.mjs +++ b/tools/acorn-optimizer.mjs @@ -889,10 +889,7 @@ function emitDCEGraph(ast) { // Scoping must balance out. assert(specialScopes === 0); // We must have found the info we need. - assert( - foundWasmImportsAssign, - 'could not find the assignment to "wasmImports". perhaps --pre-js or --post-js code moved it out of the global scope? (things like that should be done after emcc runs, as they do not need to be run through the optimizer which is the special thing about --pre-js/--post-js code)', - ); + assert(foundWasmImportsAssign, 'could not find the assignment to "wasmImports"'); // Read exports that were declared in extraInfo if (extraInfo) { for (const exp of extraInfo.exports) { diff --git a/tools/building.py b/tools/building.py index 81e3781b38c73..834b1899b82ee 100644 --- a/tools/building.py +++ b/tools/building.py @@ -554,7 +554,14 @@ def closure_compiler(filename, advanced=True, extra_closure_args=None): CLOSURE_EXTERNS = [path_from_root('src/closure-externs/closure-externs.js')] if settings.MODULARIZE: - CLOSURE_EXTERNS += [path_from_root('src/closure-externs/modularize-externs.js')] + temp = shared.get_temp_files().get('.js', prefix='emcc_closure_externs_').name + utils.write_file(temp, f''' +/** + * @suppress {{duplicate}} + */ +var {settings.EXPORT_NAME}; +''') + CLOSURE_EXTERNS += [temp] if settings.USE_WEBGPU: CLOSURE_EXTERNS += [path_from_root('src/closure-externs/webgpu-externs.js')] diff --git a/tools/emscripten.py b/tools/emscripten.py index 0db61d81e9c61..fb462c15607e8 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -899,8 +899,7 @@ def can_use_await(): # function. # However, because closure does not see this (it runs only on the inner code), # it sees this as a top-level-await, which it does not yet support. - # FIXME(https://github.com/emscripten-core/emscripten/issues/23158) - return settings.MODULARIZE and not settings.USE_CLOSURE_COMPILER + return settings.MODULARIZE def make_export_wrappers(function_exports): diff --git a/tools/link.py b/tools/link.py index c7c5ffdb0f4cf..8015fa7c58df8 100644 --- a/tools/link.py +++ b/tools/link.py @@ -2084,7 +2084,8 @@ def fix_es6_import_statements(js_file): src = read_file(js_file) write_file(js_file, src .replace('EMSCRIPTEN$IMPORT$META', 'import.meta') - .replace('EMSCRIPTEN$AWAIT$IMPORT', 'await import')) + .replace('EMSCRIPTEN$AWAIT$IMPORT', 'await import') + .replace('EMSCRIPTEN$EXPORT$DEFAULT =', 'export default')) save_intermediate('es6-module') @@ -2121,9 +2122,7 @@ def phase_final_emitting(options, state, target, wasm_target): if settings.AUDIO_WORKLET == 1: create_worker_file('src/audio_worklet.js', target_dir, settings.AUDIO_WORKLET_FILE, options) - if settings.MODULARIZE: - modularize() - elif settings.USE_CLOSURE_COMPILER: + if not settings.MODULARIZE and settings.USE_CLOSURE_COMPILER: module_export_name_substitution() # Run a final optimization pass to clean up items that were not possible to @@ -2371,165 +2370,6 @@ def phase_binaryen(target, options, wasm_target): write_file(final_js, js) -def node_pthread_detection(): - # Under node we detect that we are running in a pthread by checking the - # workerData property. - if settings.EXPORT_ES6: - return "(await import('worker_threads')).workerData === 'em-pthread';\n" - else: - return "require('worker_threads').workerData === 'em-pthread'\n" - - -def modularize(): - global final_js - logger.debug(f'Modularizing, assigning to var {settings.EXPORT_NAME}') - generated_js = read_file(final_js) - - # When targetting node and ES6 we use `await import ..` in the generated code - # so the outer function needs to be marked as async. - if settings.WASM_ASYNC_COMPILATION or (settings.EXPORT_ES6 and settings.ENVIRONMENT_MAY_BE_NODE): - maybe_async = 'async ' - else: - maybe_async = '' - - if settings.MODULARIZE == 'instance': - wrapper_function = ''' -export default %(maybe_async)s function init(moduleArg = {}) { - var moduleRtn; - -%(generated_js)s - - return moduleRtn; -} -''' % { - 'generated_js': generated_js, - 'maybe_async': maybe_async, - } - else: - wrapper_function = ''' -%(maybe_async)sfunction(moduleArg = {}) { - var moduleRtn; - -%(generated_js)s - - return moduleRtn; -} -''' % { - 'maybe_async': maybe_async, - 'generated_js': generated_js - } - - if settings.MINIMAL_RUNTIME and not settings.PTHREADS: - # Single threaded MINIMAL_RUNTIME programs do not need access to - # document.currentScript, so a simple export declaration is enough. - src = f'var {settings.EXPORT_NAME} = {wrapper_function};' - else: - script_url_node = '' - # When MODULARIZE this JS may be executed later, - # after document.currentScript is gone, so we save it. - # In EXPORT_ES6 + PTHREADS the 'thread' is actually an ES6 module - # webworker running in strict mode, so doesn't have access to 'document'. - # In this case use 'import.meta' instead. - if settings.EXPORT_ES6 and settings.USE_ES6_IMPORT_META: - script_url = 'import.meta.url' - else: - script_url = "typeof document != 'undefined' ? document.currentScript?.src : undefined" - if settings.ENVIRONMENT_MAY_BE_NODE: - script_url_node = "if (typeof __filename != 'undefined') _scriptName = _scriptName || __filename;" - if settings.MODULARIZE == 'instance': - src = '''\ - var _scriptName = %(script_url)s; - %(script_url_node)s - %(wrapper_function)s -''' % { - 'script_url': script_url, - 'script_url_node': script_url_node, - 'wrapper_function': wrapper_function, - } - else: - src = '''\ -var %(EXPORT_NAME)s = (() => { - var _scriptName = %(script_url)s; - %(script_url_node)s - return (%(wrapper_function)s); -})(); -''' % { - 'EXPORT_NAME': settings.EXPORT_NAME, - 'script_url': script_url, - 'script_url_node': script_url_node, - 'wrapper_function': wrapper_function, - } - - if settings.ASSERTIONS and settings.MODULARIZE != 'instance': - src += '''\ -(() => { - // Create a small, never-async wrapper around %(EXPORT_NAME)s which - // checks for callers incorrectly using it with `new`. - var real_%(EXPORT_NAME)s = %(EXPORT_NAME)s; - %(EXPORT_NAME)s = function(arg) { - if (new.target) throw new Error("%(EXPORT_NAME)s() should not be called with `new %(EXPORT_NAME)s()`"); - return real_%(EXPORT_NAME)s(arg); - } -})(); -''' % {'EXPORT_NAME': settings.EXPORT_NAME} - - # Given the async nature of how the Module function and Module object - # come into existence in AudioWorkletGlobalScope, store the Module - # function under a different variable name so that AudioWorkletGlobalScope - # will be able to reference it without aliasing/conflicting with the - # Module variable name. This should happen even in MINIMAL_RUNTIME builds - # for MODULARIZE and EXPORT_ES6 to work correctly. - if settings.AUDIO_WORKLET: - src += f'globalThis.AudioWorkletModule = {settings.EXPORT_NAME};\n' - - # Export using a UMD style export, or ES6 exports if selected - if settings.EXPORT_ES6: - if settings.MODULARIZE == 'instance': - exports = settings.EXPORTED_FUNCTIONS + settings.EXPORTED_RUNTIME_METHODS - # Declare a top level var for each export so that code in the init function - # can assign to it and update the live module bindings. - src += 'var ' + ', '.join(['__exp_' + export for export in exports]) + ';\n' - # Export the functions with their original name. - exports = ['__exp_' + export + ' as ' + export for export in exports] - src += 'export {' + ', '.join(exports) + '};\n' - else: - src += 'export default %s;\n' % settings.EXPORT_NAME - elif not settings.MINIMAL_RUNTIME: - src += '''\ -if (typeof exports === 'object' && typeof module === 'object') { - module.exports = %(EXPORT_NAME)s; - // This default export looks redundant, but it allows TS to import this - // commonjs style module. - module.exports.default = %(EXPORT_NAME)s; -} else if (typeof define === 'function' && define['amd']) - define([], () => %(EXPORT_NAME)s); -''' % {'EXPORT_NAME': settings.EXPORT_NAME} - - if settings.PTHREADS: - # Create code for detecting if we are running in a pthread. - # Normally this detection is done when the module is itself run but - # when running in MODULARIZE mode we need use this to know if we should - # run the module constructor on startup (true only for pthreads). - if settings.ENVIRONMENT_MAY_BE_WEB or settings.ENVIRONMENT_MAY_BE_WORKER: - src += "var isPthread = globalThis.self?.name?.startsWith('em-pthread');\n" - # In order to support both web and node we also need to detect node here. - if settings.ENVIRONMENT_MAY_BE_NODE: - src += "var isNode = typeof globalThis.process?.versions?.node == 'string';\n" - src += f'if (isNode) isPthread = {node_pthread_detection()}\n' - elif settings.ENVIRONMENT_MAY_BE_NODE: - src += f'var isPthread = {node_pthread_detection()}\n' - src += '// When running as a pthread, construct a new instance on startup\n' - if settings.MODULARIZE == 'instance': - src += 'isPthread && init();\n' - else: - src += 'isPthread && %s();\n' % settings.EXPORT_NAME - - final_js += '.modular.js' - write_file(final_js, src) - shared.get_temp_files().note(final_js) - save_intermediate('modularized') - - def module_export_name_substitution(): assert not settings.MODULARIZE global final_js diff --git a/tools/preprocessor.mjs b/tools/preprocessor.mjs index 8b15db9d2f03e..948a70128829d 100755 --- a/tools/preprocessor.mjs +++ b/tools/preprocessor.mjs @@ -31,7 +31,7 @@ loadSettingsFile(settingsFile); const parseTools = await import('../src/parseTools.mjs'); await import('../src/modules.mjs'); -let output = parseTools.preprocess(inputFile); +let output = parseTools.preprocess(inputFile, /*closureFriendly=*/ false); if (expandMacros) { output = parseTools.processMacros(output, inputFile); }