diff --git a/.eslintrc.yml b/.eslintrc.yml index 1eed29cd3fc5..59361f6b88d2 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -22,7 +22,6 @@ ignorePatterns: - "src/closure-externs/" - "src/embind/" - "src/emrun_postjs.js" - - "src/worker.js" - "src/wasm_worker.js" - "src/audio_worklet.js" - "src/wasm2js.js" diff --git a/ChangeLog.md b/ChangeLog.md index 5bca4decaf73..5abb6a787fb0 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -23,6 +23,12 @@ See docs/process.md for more on how version tagging works. - Enable use of `::` to escape port option separator (#21710) - In multi-threaded builds `--extern-pre-js` and `--extern-post-js` code is now only run on the main thread, and not on each of the workers. (#21750) +- Multi-threaded builds no depend on a separate `.worker.js` file. This saves + on code size and network requests. In order to make this change go smoothly, + without breaking build systems that expect a `worker.js`, emscripten will + generate an empty `.worker.js` to give folks time to transition their + deployment scripts. In `-sSTRICT` mode, this empty file will not be + generated. (#21701) 3.1.57 - 04/10/24 ----------------- diff --git a/site/source/docs/porting/pthreads.rst b/site/source/docs/porting/pthreads.rst index b35dbdde78ba..6f00b3971d6c 100644 --- a/site/source/docs/porting/pthreads.rst +++ b/site/source/docs/porting/pthreads.rst @@ -146,8 +146,6 @@ The Emscripten implementation for the pthreads API should follow the POSIX stand - Pthreads + memory growth (``ALLOW_MEMORY_GROWTH``) is especially tricky, see `Wasm design issue #1271 `_. This currently causes JS accessing the Wasm memory to be slow - but this will likely only be noticeable if the JS does large amounts of memory reads and writes (Wasm runs at full speed, so moving work over can fix this). This also requires that your JS be aware that the HEAP* views may need to be updated - JS code embedded with ``--js-library`` etc will automatically be transformed to use the ``GROWABLE_HEAP_*`` helper functions where ``HEAP*`` are used, but external code that uses ``Module.HEAP*`` directly may encounter problems with views being smaller than memory. -Also note that when compiling code that uses pthreads, an additional JavaScript file ``NAME.worker.js`` is generated alongside the output .js file (where ``NAME`` is the basename of the main file being emitted). That file must be deployed with the rest of the generated code files. By default, ``NAME.worker.js`` will be loaded relative to the main HTML page URL. If it is desirable to load the file from a different location e.g. in a CDN environment, then one can define the ``Module.locateFile(filename)`` function in the main HTML ``Module`` object to return the URL of the target location of the ``NAME.worker.js`` entry point. If this function is not defined in ``Module``, then the default location relative to the main HTML file is used. - .. _Allocator_performance: Allocator performance diff --git a/src/closure-externs/minimal_runtime_worker_externs.js b/src/closure-externs/minimal_runtime_worker_externs.js deleted file mode 100644 index bc733f3f661a..000000000000 --- a/src/closure-externs/minimal_runtime_worker_externs.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @license - * Copyright 2020 The Emscripten Authors - * SPDX-License-Identifier: MIT - */ - -// These externs are needed for MINIMAL_RUNTIME + -pthread -// This file should go away in the future when worker.js is refactored to live inside the JS module. - -/** @suppress {duplicate} */ -var ENVIRONMENT_IS_PTHREAD; -/** @suppress {duplicate} */ -var wasmMemory; diff --git a/src/library.js b/src/library.js index c89cd1c72c95..44531ee1d910 100644 --- a/src/library.js +++ b/src/library.js @@ -83,6 +83,9 @@ addToLibrary({ #if ASSERTIONS || EXIT_RUNTIME '$keepRuntimeAlive', #endif +#if PTHREADS + '$exitOnMainThread', +#endif #if PTHREADS_DEBUG '$runtimeKeepaliveCounter', #endif diff --git a/src/library_pthread.js b/src/library_pthread.js index a6f8daf5996e..bff45471ffab 100644 --- a/src/library_pthread.js +++ b/src/library_pthread.js @@ -75,7 +75,7 @@ var LibraryPThread = { ) { t = _pthread_self(); } - return 'w:' + (Module['workerID'] || 0) + ',t:' + ptrToString(t) + ': '; + return 'w:' + workerID + ',t:' + ptrToString(t) + ': '; } // Prefix all err()/dbg() messages with the calling thread ID. @@ -127,16 +127,6 @@ var LibraryPThread = { }, initWorker() { -#if MAYBE_CLOSURE_COMPILER - // worker.js is not compiled together with us, and must access certain - // things. - PThread['receiveObjectTransfer'] = PThread.receiveObjectTransfer; - PThread['threadInitTLS'] = PThread.threadInitTLS; -#if !MINIMAL_RUNTIME - PThread['setExitStatus'] = PThread.setExitStatus; -#endif -#endif - #if isSymbolNeeded('$noExitRuntime') // The default behaviour for pthreads is always to exit once they return // from their entry point (or call pthread_exit). If we set noExitRuntime @@ -373,16 +363,6 @@ var LibraryPThread = { worker.postMessage({ 'cmd': 'load', 'handlers': handlers, - // If the application main .js file was loaded from a Blob, then it is not possible - // to access the URL of the current script that could be passed to a Web Worker so that - // it could load up the same file. In that case, developer must either deliver the Blob - // object in Module['mainScriptUrlOrBlob'], or a URL to it, so that pthread Workers can - // independently load up the same main application file. - 'urlOrBlob': Module['mainScriptUrlOrBlob'] -#if !EXPORT_ES6 - || _scriptName -#endif - , #if WASM2JS // the polyfill WebAssembly.Memory instance has function properties, // which will fail in postMessage, so just send a custom object with the @@ -440,34 +420,50 @@ var LibraryPThread = { // Creates a new web Worker and places it in the unused worker pool to wait for its use. allocateUnusedWorker() { var worker; -#if MINIMAL_RUNTIME - var pthreadMainJs = Module['worker'] || './{{{ PTHREAD_WORKER_FILE }}}'; -#else + var workerOptions = { +#if EXPORT_ES6 + 'type': 'module', +#endif +#if ENVIRONMENT_MAY_BE_NODE + // This is the way that we signal to the node worker that it is hosting + // a pthread. + 'workerData': 'em-pthread', +#endif +#if ENVIRONMENT_MAY_BE_WEB + // This is the way that we signal to the Web Worker that it is hosting + // a pthread. + 'name': 'em-pthread', +#endif + }; #if EXPORT_ES6 && USE_ES6_IMPORT_META - // If we're using module output and there's no explicit override, use bundler-friendly pattern. - if (!Module['locateFile']) { + // If we're using module output, use bundler-friendly pattern. #if PTHREADS_DEBUG - dbg('Allocating a new web worker from ' + new URL('{{{ PTHREAD_WORKER_FILE }}}', import.meta.url)); + dbg('Allocating a new web worker from ' + import.meta.url); #endif #if TRUSTED_TYPES - // Use Trusted Types compatible wrappers. - if (typeof trustedTypes != 'undefined' && trustedTypes.createPolicy) { - var p = trustedTypes.createPolicy( - 'emscripten#workerPolicy1', - { - createScriptURL: (ignored) => new URL('{{{ PTHREAD_WORKER_FILE }}}', import.meta.url); - } - ); - worker = new Worker(p.createScriptURL('ignored'), {type: 'module'}); - } else -#endif - worker = new Worker(new URL('{{{ PTHREAD_WORKER_FILE }}}', import.meta.url), {type: 'module'}); - } else { + // Use Trusted Types compatible wrappers. + if (typeof trustedTypes != 'undefined' && trustedTypes.createPolicy) { + var p = trustedTypes.createPolicy( + 'emscripten#workerPolicy1', + { + createScriptURL: (ignored) => new URL(import.meta.url); + } + ); + worker = new Worker(p.createScriptURL('ignored'), workerOptions); + } else #endif - // Allow HTML module to configure the location where the 'worker.js' file will be loaded from, - // via Module.locateFile() function. If not specified, then the default URL 'worker.js' relative - // to the main html file is loaded. - var pthreadMainJs = locateFile('{{{ PTHREAD_WORKER_FILE }}}'); + worker = new Worker(new URL(import.meta.url), workerOptions); +#else + var pthreadMainJs = _scriptName; +#if expectToReceiveOnModule('mainScriptUrlOrBlob') + // We can't use makeModuleReceiveWithVar here since we want to also + // call URL.createObjectURL on the mainScriptUrlOrBlob. + if (Module['mainScriptUrlOrBlob']) { + pthreadMainJs = Module['mainScriptUrlOrBlob']; + if (typeof pthreadMainJs != 'string') { + pthreadMainJs = URL.createObjectURL(pthreadMainJs); + } + } #endif #if PTHREADS_DEBUG dbg(`Allocating a new web worker from ${pthreadMainJs}`); @@ -476,14 +472,12 @@ var LibraryPThread = { // Use Trusted Types compatible wrappers. if (typeof trustedTypes != 'undefined' && trustedTypes.createPolicy) { var p = trustedTypes.createPolicy('emscripten#workerPolicy2', { createScriptURL: (ignored) => pthreadMainJs }); - worker = new Worker(p.createScriptURL('ignored'){{{ EXPORT_ES6 ? ", {type: 'module'}" : '' }}}); + worker = new Worker(p.createScriptURL('ignored'), workerOptions); } else #endif - worker = new Worker(pthreadMainJs{{{ EXPORT_ES6 ? ", {type: 'module'}" : '' }}}); -#if EXPORT_ES6 && USE_ES6_IMPORT_META - } -#endif - PThread.unusedWorkers.push(worker); + worker = new Worker(pthreadMainJs, workerOptions); +#endif // EXPORT_ES6 && USE_ES6_IMPORT_META + PThread.unusedWorkers.push(worker); }, getNewWorker() { diff --git a/src/library_sdl.js b/src/library_sdl.js index 110fc69dc06f..bb38f12db446 100644 --- a/src/library_sdl.js +++ b/src/library_sdl.js @@ -2907,14 +2907,14 @@ var LibrarySDL = { return SDL.setGetVolume(SDL.music, volume); }, - Mix_LoadMUS_RW__docs: '/** @param {number} a1 */', - Mix_LoadMUS_RW: 'Mix_LoadWAV_RW', + Mix_LoadMUS_RW__deps: ['Mix_LoadWAV_RW'], + Mix_LoadMUS_RW: (filename) => _Mix_LoadWAV_RW(filename, 0), Mix_LoadMUS__deps: ['Mix_LoadMUS_RW', 'SDL_RWFromFile', 'SDL_FreeRW'], Mix_LoadMUS__proxy: 'sync', Mix_LoadMUS: (filename) => { var rwops = _SDL_RWFromFile(filename, 0); - var result = _Mix_LoadMUS_RW(rwops, 0); + var result = _Mix_LoadMUS_RW(rwops); _SDL_FreeRW(rwops); return result; }, diff --git a/src/parseTools.mjs b/src/parseTools.mjs index 4846870487b9..7f20321c3d25 100644 --- a/src/parseTools.mjs +++ b/src/parseTools.mjs @@ -888,21 +888,6 @@ function buildStringArray(array) { } } -// Generates access to a JS imports scope variable in pthreads worker.js. In MODULARIZE mode these flow into the imports object for the Module. -// In non-MODULARIZE mode, we can directly access the variables in global scope. -function makeAsmImportsAccessInPthread(variable) { - if (!MINIMAL_RUNTIME) { - // Regular runtime uses the name "Module" for both imports and exports. - return `Module['${variable}']`; - } - if (MODULARIZE) { - // MINIMAL_RUNTIME uses 'imports' as the name for the imports object in MODULARIZE builds. - return `imports['${variable}']`; - } - // In non-MODULARIZE builds, can access the imports from global scope. - return `self.${variable}`; -} - function _asmjsDemangle(symbol) { if (symbol.startsWith('dynCall_')) { return symbol; @@ -1130,7 +1115,6 @@ addToCompileTimeContext({ hasExportedSymbol, implicitSelf, isSymbolNeeded, - makeAsmImportsAccessInPthread, makeDynCall, makeEval, makeGetValue, diff --git a/src/postamble.js b/src/postamble.js index 1b71ef3a46ab..17170a23d435 100644 --- a/src/postamble.js +++ b/src/postamble.js @@ -144,8 +144,8 @@ function stackCheckInit() { #if MAIN_MODULE && PTHREADS // Map of modules to be shared with new threads. This gets populated by the -// main thread and shared with all new workers. -var sharedModules = Module['sharedModules'] || []; +// main thread and shared with all new workers via the initial `load` message. +var sharedModules = {}; #endif #if MAIN_READS_PARAMS diff --git a/src/postamble_minimal.js b/src/postamble_minimal.js index 97e4b2d65712..deb1335ba76a 100644 --- a/src/postamble_minimal.js +++ b/src/postamble_minimal.js @@ -89,15 +89,6 @@ function initRuntime(wasmExports) { // Initialize wasm (asynchronous) -var imports = { -#if MINIFY_WASM_IMPORTED_MODULES - 'a': wasmImports, -#else // MINIFY_WASM_IMPORTED_MODULES - 'env': wasmImports, - '{{{ WASI_MODULE_NAME }}}': wasmImports, -#endif // MINIFY_WASM_IMPORTED_MODULES -}; - // In non-fastcomp non-asm.js builds, grab wasm exports to outer scope // for emscripten_get_exported_function() to be able to access them. #if LibraryManager.has('library_exports.js') @@ -112,6 +103,20 @@ var wasmModule; <<< WASM_MODULE_EXPORTS_DECLARES >>> #endif +#if PTHREADS +function loadModule() { + assignWasmImports(); +#endif + +var imports = { +#if MINIFY_WASM_IMPORTED_MODULES + 'a': wasmImports, +#else // MINIFY_WASM_IMPORTED_MODULES + 'env': wasmImports, + '{{{ WASI_MODULE_NAME }}}': wasmImports, +#endif // MINIFY_WASM_IMPORTED_MODULES +}; + #if MINIMAL_RUNTIME_STREAMING_WASM_INSTANTIATION // https://caniuse.com/#feat=wasm and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiateStreaming // Firefox 52 added Wasm support, but only Firefox 58 added instantiateStreaming. @@ -252,3 +257,13 @@ WebAssembly.instantiate(Module['wasm'], imports).then((output) => { } #endif // ASSERTIONS || WASM == 2 ); + +#if PTHREADS +} + +if (!ENVIRONMENT_IS_PTHREAD) { + // When running in a pthread we delay module loading untill we have + // received the module via postMessage + loadModule(); +} +#endif diff --git a/src/preamble.js b/src/preamble.js index bed99d426564..18cf93959e99 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -14,6 +14,10 @@ // An online HTML version (which may be of a different version of Emscripten) // is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html +#if PTHREADS +#include "runtime_pthread.js" +#endif + #if RELOCATABLE {{{ makeModuleReceiveWithVar('dynamicLibraries', undefined, '[]', true) }}} #endif @@ -897,11 +901,27 @@ function instantiateAsync(binary, binaryFile, imports, callback) { } #endif // WASM_ASYNC_COMPILATION -// Create the wasm instance. -// Receives the wasm imports, returns the exports. -function createWasm() { +function getWasmImports() { +#if PTHREADS + assignWasmImports(); +#endif +#if ASYNCIFY && (ASSERTIONS || ASYNCIFY == 2) + // instrumenting imports is used in asyncify in two ways: to add assertions + // that check for proper import use, and for ASYNCIFY=2 we use them to set up + // the Promise API on the import side. +#if PTHREADS || ASYNCIFY_LAZY_LOAD_CODE + // In pthreads builds getWasmImports is called more than once but we only + // and the instrument the imports once. + if (!wasmImports.__instrumented) { + wasmImports.__instrumented = true; + Asyncify.instrumentWasmImports(wasmImports); + } +#else + Asyncify.instrumentWasmImports(wasmImports); +#endif +#endif // prepare imports - var info = { + return { #if MINIFY_WASM_IMPORTED_MODULES 'a': wasmImports, #else // MINIFY_WASM_IMPORTED_MODULES @@ -915,7 +935,13 @@ function createWasm() { 'GOT.mem': new Proxy(wasmImports, GOTHandler), 'GOT.func': new Proxy(wasmImports, GOTHandler), #endif - }; + } +} + +// Create the wasm instance. +// Receives the wasm imports, returns the exports. +function createWasm() { + var info = getWasmImports(); // Load the wasm module and create an instance of using native support in the JS engine. // handle a generated wasm instance, receiving its exports and // performing other necessary setup @@ -1047,21 +1073,6 @@ function createWasm() { // Also pthreads and wasm workers initialize the wasm instance through this // path. if (Module['instantiateWasm']) { - -#if USE_OFFSET_CONVERTER -#if ASSERTIONS -{{{ runIfWorkerThread("assert(Module['wasmOffsetData'], 'wasmOffsetData not found on Module object');") }}} -#endif -{{{ runIfWorkerThread("wasmOffsetConverter = resetPrototype(WasmOffsetConverter, Module['wasmOffsetData']);") }}} -#endif - -#if LOAD_SOURCE_MAP -#if ASSERTIONS -{{{ runIfWorkerThread("assert(Module['wasmSourceMapData'], 'wasmSourceMapData not found on Module object');") }}} -#endif -{{{ runIfWorkerThread("wasmSourceMap = resetPrototype(WasmSourceMap, Module['wasmSourceMapData']);") }}} -#endif - try { return Module['instantiateWasm'](info, receiveInstance); } catch(e) { diff --git a/src/preamble_minimal.js b/src/preamble_minimal.js index 82471a4a940c..e1bf618043f3 100644 --- a/src/preamble_minimal.js +++ b/src/preamble_minimal.js @@ -12,6 +12,10 @@ #include "runtime_asan.js" #endif +#if PTHREADS +#include "runtime_pthread.js" +#endif + #if ASSERTIONS /** @type {function(*, string=)} */ function assert(condition, text) { @@ -85,11 +89,19 @@ else { #endif // MODULARIZE #endif // PTHREADS +#if PTHREADS +if (!ENVIRONMENT_IS_PTHREAD) { +#endif + #if ASSERTIONS && SHARED_MEMORY assert(wasmMemory.buffer instanceof SharedArrayBuffer, 'requested a shared WebAssembly.Memory but the returned buffer is not a SharedArrayBuffer, indicating that while the browser has SharedArrayBuffer it does not have WebAssembly threads support - you may need to set a flag'); #endif updateMemoryViews(); + +#if PTHREADS +} +#endif #endif // IMPORTED_MEMORY #include "runtime_stack_check.js" diff --git a/src/runtime_init_memory.js b/src/runtime_init_memory.js index 5817f16c8559..4344a5cdb73e 100644 --- a/src/runtime_init_memory.js +++ b/src/runtime_init_memory.js @@ -9,21 +9,10 @@ {{{ throw "this file should not be be included when IMPORTED_MEMORY is set"; }}} #endif -{{{ makeModuleReceiveWithVar('INITIAL_MEMORY', undefined, INITIAL_MEMORY) }}} - -#if ASSERTIONS -assert(INITIAL_MEMORY >= {{{STACK_SIZE}}}, 'INITIAL_MEMORY should be larger than STACK_SIZE, was ' + INITIAL_MEMORY + '! (STACK_SIZE=' + {{{STACK_SIZE}}} + ')'); -#endif - // check for full engine support (use string 'subarray' to avoid closure compiler confusion) #if PTHREADS -if (ENVIRONMENT_IS_PTHREAD) { - wasmMemory = Module['wasmMemory']; -#if ASSERTIONS - assert(wasmMemory, 'wasmMemory not set on incomming module object'); -#endif -} else { +if (!ENVIRONMENT_IS_PTHREAD) { #endif // PTHREADS #if expectToReceiveOnModule('wasmMemory') @@ -32,6 +21,11 @@ if (ENVIRONMENT_IS_PTHREAD) { } else #endif { + {{{ makeModuleReceiveWithVar('INITIAL_MEMORY', undefined, INITIAL_MEMORY) }}} + +#if ASSERTIONS + assert(INITIAL_MEMORY >= {{{STACK_SIZE}}}, 'INITIAL_MEMORY should be larger than STACK_SIZE, was ' + INITIAL_MEMORY + '! (STACK_SIZE=' + {{{STACK_SIZE}}} + ')'); +#endif wasmMemory = new WebAssembly.Memory({ 'initial': INITIAL_MEMORY / {{{ WASM_PAGE_SIZE }}}, #if ALLOW_MEMORY_GROWTH @@ -62,15 +56,8 @@ if (ENVIRONMENT_IS_PTHREAD) { #endif } + updateMemoryViews(); #if PTHREADS } #endif -updateMemoryViews(); - -// If the user provides an incorrect length, just use that length instead rather than providing the user to -// specifically provide the memory length with Module['INITIAL_MEMORY']. -INITIAL_MEMORY = wasmMemory.buffer.byteLength; -#if ASSERTIONS -assert(INITIAL_MEMORY % {{{ WASM_PAGE_SIZE }}} === 0); -#endif diff --git a/src/runtime_pthread.js b/src/runtime_pthread.js new file mode 100644 index 000000000000..ca2f6138e61e --- /dev/null +++ b/src/runtime_pthread.js @@ -0,0 +1,250 @@ +/** + * @license + * Copyright 2015 The Emscripten Authors + * SPDX-License-Identifier: MIT + */ + +// Pthread Web Worker handling code. +// This code runs only on pthread web workers and handles pthread setup +// and communication with the main thread via postMessage. + +#if ASSERTIONS +// Unique ID of the current pthread worker (zero on non-pthread-workers +// including the main thread). +var workerID = 0; +#endif + +if (ENVIRONMENT_IS_PTHREAD) { +#if !MINIMAL_RUNTIME + var wasmPromiseResolve; + var wasmPromiseReject; +#endif + var receivedWasmModule; + +#if ENVIRONMENT_MAY_BE_NODE + // Node.js support + if (ENVIRONMENT_IS_NODE) { + // Create as web-worker-like an environment as we can. + + var parentPort = worker_threads['parentPort']; + parentPort.on('message', (data) => onmessage({ data: data })); + + Object.assign(globalThis, { + self: global, + // Dummy importScripts. The presence of this global is used + // to detect that we are running on a Worker. + // TODO(sbc): Find another way? + importScripts: () => { +#if ASSERTIONS + assert(false, 'dummy importScripts called'); +#endif + }, + postMessage: (msg) => parentPort.postMessage(msg), + performance: global.performance || { now: Date.now }, + }); + } +#endif // ENVIRONMENT_MAY_BE_NODE + + // Thread-local guard variable for one-time init of the JS state + var initializedJS = false; + + function threadPrintErr(...args) { + var text = args.join(' '); +#if ENVIRONMENT_MAY_BE_NODE + // See https://github.com/emscripten-core/emscripten/issues/14804 + if (ENVIRONMENT_IS_NODE) { + fs.writeSync(2, text + '\n'); + return; + } +#endif + console.error(text); + } + +#if expectToReceiveOnModule('printErr') + if (!Module['printErr']) +#endif + err = threadPrintErr; +#if ASSERTIONS || RUNTIME_DEBUG + dbg = threadPrintErr; +#endif + function threadAlert(...args) { + var text = args.join(' '); + postMessage({cmd: 'alert', text, threadId: _pthread_self()}); + } + self.alert = threadAlert; + +#if !MINIMAL_RUNTIME + Module['instantiateWasm'] = (info, receiveInstance) => { + return new Promise((resolve, reject) => { + wasmPromiseResolve = (module) => { + // Instantiate from the module posted from the main thread. + // We can just use sync instantiation in the worker. + var instance = new WebAssembly.Instance(module, getWasmImports()); +#if RELOCATABLE || MAIN_MODULE + receiveInstance(instance, module); +#else + // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, + // the above line no longer optimizes out down to the following line. + // When the regression is fixed, we can remove this if/else. + receiveInstance(instance); +#endif + resolve(); + }; + wasmPromiseReject = reject; + }); + } +#endif + + // Turn unhandled rejected promises into errors so that the main thread will be + // notified about them. + self.onunhandledrejection = (e) => { throw e.reason || e; }; + + function handleMessage(e) { + try { + var msgData = e['data']; + //dbg('msgData: ' + Object.keys(msgData)); + var cmd = msgData['cmd']; + if (cmd === 'load') { // Preload command that is called once per worker to parse and load the Emscripten code. +#if ASSERTIONS + workerID = msgData['workerID']; +#endif +#if PTHREADS_DEBUG + dbg('worker: loading module') +#endif + + // Until we initialize the runtime, queue up any further incoming messages. + let messageQueue = []; + self.onmessage = (e) => messageQueue.push(e); + + // And add a callback for when the runtime is initialized. + self.startWorker = (instance) => { + // Notify the main thread that this thread has loaded. + postMessage({ 'cmd': 'loaded' }); + // Process any messages that were queued before the thread was ready. + for (let msg of messageQueue) { + handleMessage(msg); + } + // Restore the real message handler. + self.onmessage = handleMessage; + }; + +#if MAIN_MODULE + sharedModules = msgData['sharedModules']; +#if RUNTIME_DEBUG + dbg(`worker: received ${Object.keys(msgData['sharedModules']).length} shared modules: ${Object.keys(msgData.sharedModules)}`); +#endif +#endif + + // Use `const` here to ensure that the variable is scoped only to + // that iteration, allowing safe reference from a closure. + for (const handler of msgData['handlers']) { + // The the main module has a handler for a certain even, but no + // handler exists on the pthread worker, then proxy that handler + // back to the main thread. + if (!Module[handler] || Module[handler].proxy) { +#if RUNTIME_DEBUG + dbg(`worker: installer proxying handler: ${handler}`); +#endif + Module[handler] = (...args) => { +#if RUNTIME_DEBUG + dbg(`worker: calling handler on main thread: ${handler}`); +#endif + postMessage({ cmd: 'callHandler', handler, args: args }); + } + // Rebind the out / err handlers if needed + if (handler == 'print') out = Module[handler]; + if (handler == 'printErr') err = Module[handler]; + } +#if RUNTIME_DEBUG + else dbg(`worker: using thread-local handler: ${handler}`); +#endif + } + + wasmMemory = msgData['wasmMemory']; + updateMemoryViews(); + +#if LOAD_SOURCE_MAP + wasmSourceMap = resetPrototype(WasmSourceMap, msgData['wasmSourceMap']); +#endif +#if USE_OFFSET_CONVERTER + wasmOffsetConverter = resetPrototype(WasmOffsetConverter, msgData['wasmOffsetConverter']); +#endif + +#if MINIMAL_RUNTIME + // Pass the shared Wasm module in the Module object for MINIMAL_RUNTIME. + Module['wasm'] = msgData['wasmModule']; + loadModule(); +#else + wasmPromiseResolve(msgData['wasmModule']); +#endif // MINIMAL_RUNTIME + } else if (cmd === 'run') { + // Pass the thread address to wasm to store it for fast access. + __emscripten_thread_init(msgData['pthread_ptr'], /*is_main=*/0, /*is_runtime=*/0, /*can_block=*/1, 0, 0); + + // Await mailbox notifications with `Atomics.waitAsync` so we can start + // using the fast `Atomics.notify` notification path. + __emscripten_thread_mailbox_await(msgData['pthread_ptr']); + +#if ASSERTIONS + assert(msgData['pthread_ptr']); +#endif + // Also call inside JS module to set up the stack frame for this pthread in JS module scope + establishStackSpace(); + PThread.receiveObjectTransfer(msgData); + PThread.threadInitTLS(); + + if (!initializedJS) { +#if EMBIND +#if PTHREADS_DEBUG + dbg(`worker: Pthread 0x${_pthread_self().toString(16)} initializing embind.`); +#endif + // Embind must initialize itself on all threads, as it generates support JS. + // We only do this once per worker since they get reused + __embind_initialize_bindings(); +#endif // EMBIND + initializedJS = true; + } + + try { + invokeEntryPoint(msgData['start_routine'], msgData['arg']); + } catch(ex) { + if (ex != 'unwind') { + // The pthread "crashed". Do not call `_emscripten_thread_exit` (which + // would make this thread joinable). Instead, re-throw the exception + // and let the top level handler propagate it back to the main thread. + throw ex; + } +#if RUNTIME_DEBUG + dbg(`worker: Pthread 0x${_pthread_self().toString(16)} completed its main entry point with an 'unwind', keeping the worker alive for asynchronous operation.`); +#endif + } + } else if (cmd === 'cancel') { // Main thread is asking for a pthread_cancel() on this thread. + if (_pthread_self()) { + __emscripten_thread_exit({{{ cDefs.PTHREAD_CANCELED }}}); + } + } else if (msgData.target === 'setimmediate') { + // no-op + } else if (cmd === 'checkMailbox') { + if (initializedJS) { + checkMailbox(); + } + } else if (cmd) { + // The received message looks like something that should be handled by this message + // handler, (since there is a cmd field present), but is not one of the + // recognized commands: + err(`worker: received unknown command ${cmd}`); + err(msgData); + } + } catch(ex) { +#if ASSERTIONS + err(`worker: onmessage() captured an uncaught exception: ${ex}`); + if (ex?.stack) err(ex.stack); +#endif + __emscripten_thread_crashed(); + throw ex; + } + }; + + self.onmessage = handleMessage; + +} // ENVIRONMENT_IS_PTHREAD diff --git a/src/settings_internal.js b/src/settings_internal.js index 2e7f9bca8c9a..6018b31c6e83 100644 --- a/src/settings_internal.js +++ b/src/settings_internal.js @@ -131,9 +131,6 @@ var USER_EXPORTED_FUNCTIONS = []; // name of the file containing wasm binary, if relevant var WASM_BINARY_FILE = ''; -// name of the file containing the pthread *.worker.js, if relevant -var PTHREAD_WORKER_FILE = ''; - // name of the file containing the Wasm Worker *.ww.js, if relevant var WASM_WORKER_FILE = ''; diff --git a/src/shell.js b/src/shell.js index 3d31a75e8fb1..319d793d1c56 100644 --- a/src/shell.js +++ b/src/shell.js @@ -118,8 +118,9 @@ if (Module['ENVIRONMENT']) { // 2) We could be the application main() thread proxied to worker. (with Emscripten -sPROXY_TO_WORKER) (ENVIRONMENT_IS_WORKER == true, ENVIRONMENT_IS_PTHREAD == false) // 3) We could be an application pthread running in a worker. (ENVIRONMENT_IS_WORKER == true and ENVIRONMENT_IS_PTHREAD == true) -// ENVIRONMENT_IS_PTHREAD=true will have been preset in worker.js. Make it false in the main runtime thread. -var ENVIRONMENT_IS_PTHREAD = globalThis['ENVIRONMENT_IS_PTHREAD'] || false; +// The way we signal to a worker that it is hosting a pthread is to construct +// it with a specific name. +var ENVIRONMENT_IS_PTHREAD = ENVIRONMENT_IS_WORKER && self.name == 'em-pthread'; #if MODULARIZE && ASSERTIONS if (ENVIRONMENT_IS_PTHREAD) { @@ -129,6 +130,31 @@ if (ENVIRONMENT_IS_PTHREAD) { #endif #endif +#if ENVIRONMENT_MAY_BE_NODE +if (ENVIRONMENT_IS_NODE) { + // `require()` is no-op in an ESM module, use `createRequire()` to construct + // the require()` function. This is only necessary for multi-environment + // builds, `-sENVIRONMENT=node` emits a static import declaration instead. + // TODO: Swap all `require()`'s with `import()`'s? +#if EXPORT_ES6 && ENVIRONMENT_MAY_BE_WEB + const { createRequire } = await import('module'); + /** @suppress{duplicate} */ + var require = createRequire(import.meta.url); +#endif + +#if PTHREADS || WASM_WORKERS + var worker_threads = require('worker_threads'); + global.Worker = worker_threads.Worker; + ENVIRONMENT_IS_WORKER = !worker_threads.isMainThread; +#if PTHREADS + // Under node we set `workerData` to `em-pthread` to signal that the worker + // is hosting a pthread. + ENVIRONMENT_IS_PTHREAD = ENVIRONMENT_IS_WORKER && worker_threads['workerData'] == 'em-pthread' +#endif // PTHREADS +#endif // PTHREADS || WASM_WORKERS +} +#endif // ENVIRONMENT_MAY_BE_NODE + #if WASM_WORKERS var ENVIRONMENT_IS_WASM_WORKER = Module['$ww']; #endif @@ -155,14 +181,18 @@ var quit_ = (status, toThrow) => { // before the page load. In non-MODULARIZE modes generate it here. var _scriptName = (typeof document != 'undefined') ? document.currentScript?.src : undefined; -if (ENVIRONMENT_IS_WORKER) { - _scriptName = self.location.href; -} #if ENVIRONMENT_MAY_BE_NODE -else if (ENVIRONMENT_IS_NODE) { +if (ENVIRONMENT_IS_NODE) { +#if EXPORT_ES6 + _scriptName = typeof __filename != 'undefined' ? __filename : import.meta.url +#else _scriptName = __filename; -} +#endif +} else #endif // ENVIRONMENT_MAY_BE_NODE +if (ENVIRONMENT_IS_WORKER) { + _scriptName = self.location.href; +} #endif // SHARED_MEMORY && !MODULARIZE // `/` should be present at the end if `scriptDirectory` is not empty @@ -197,15 +227,6 @@ if (ENVIRONMENT_IS_NODE) { } #endif - // `require()` is no-op in an ESM module, use `createRequire()` to construct - // the require()` function. This is only necessary for multi-environment - // builds, `-sENVIRONMENT=node` emits a static import declaration instead. - // TODO: Swap all `require()`'s with `import()`'s? -#if EXPORT_ES6 && ENVIRONMENT_MAY_BE_WEB - const { createRequire } = await import('module'); - /** @suppress{duplicate} */ - var require = createRequire(import.meta.url); -#endif // These modules will usually be used on Node.js. Load them eagerly to avoid // the complexity of lazy-loading. var fs = require('fs'); @@ -269,10 +290,6 @@ if (ENVIRONMENT_IS_NODE) { throw toThrow; }; -#if PTHREADS || WASM_WORKERS - global.Worker = require('worker_threads').Worker; -#endif - #if WASM == 2 // If target shell does not support Wasm, load the JS version of the code. if (typeof WebAssembly == 'undefined') { @@ -398,9 +415,9 @@ if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { if (!(typeof window == 'object' || typeof importScripts == 'function')) throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); #endif +#if PTHREADS && ENVIRONMENT_MAY_BE_NODE // Differentiate the Web Worker from the Node Worker case, as reading must // be done differently. -#if PTHREADS && ENVIRONMENT_MAY_BE_NODE if (!ENVIRONMENT_IS_NODE) #endif { diff --git a/src/shell_minimal.js b/src/shell_minimal.js index 7492f44bb0fc..6c2167fd2f31 100644 --- a/src/shell_minimal.js +++ b/src/shell_minimal.js @@ -137,20 +137,8 @@ function ready() { #if PTHREADS // MINIMAL_RUNTIME does not support --proxy-to-worker option, so Worker and Pthread environments // coincide. -#if WASM_WORKERS -var ENVIRONMENT_IS_WORKER = typeof importScripts == 'function', - ENVIRONMENT_IS_PTHREAD = ENVIRONMENT_IS_WORKER && !ENVIRONMENT_IS_WASM_WORKER; -#else -var ENVIRONMENT_IS_WORKER = typeof importScripts == 'function', - ENVIRONMENT_IS_PTHREAD = ENVIRONMENT_IS_WORKER; -#endif -#endif - -// --pre-jses are emitted after the Module integration code, so that they can -// refer to Module (if they choose; they can also define Module) -{{{ preJS() }}} - -#if PTHREADS +var ENVIRONMENT_IS_WORKER = typeof importScripts == 'function'; +var ENVIRONMENT_IS_PTHREAD = ENVIRONMENT_IS_WORKER && self.name == 'em-pthread'; #if !MODULARIZE // In MODULARIZE mode _scriptName needs to be captured already at the very top of the page immediately when the page is parsed, so it is generated there @@ -158,22 +146,26 @@ var ENVIRONMENT_IS_WORKER = typeof importScripts == 'function', var _scriptName = (typeof document != 'undefined') ? document.currentScript?.src : undefined; #endif -if (ENVIRONMENT_IS_WORKER) { - _scriptName = self.location.href; -} #if ENVIRONMENT_MAY_BE_NODE -else if (ENVIRONMENT_IS_NODE) { +if (ENVIRONMENT_IS_NODE) { + var worker_threads = require('worker_threads'); + global.Worker = worker_threads.Worker; + ENVIRONMENT_IS_WORKER = !worker_threads.isMainThread; + // Under node we set `workerData` to `em-pthread` to signal that the worker + // is hosting a pthread. + ENVIRONMENT_IS_PTHREAD = ENVIRONMENT_IS_WORKER && worker_threads['workerData'] == 'em-pthread' _scriptName = __filename; -} +} else #endif - -#if ENVIRONMENT_MAY_BE_NODE -if (ENVIRONMENT_IS_NODE) { - global.Worker = require('worker_threads').Worker; +if (ENVIRONMENT_IS_WORKER) { + _scriptName = self.location.href; } -#endif #endif // PTHREADS +// --pre-jses are emitted after the Module integration code, so that they can +// refer to Module (if they choose; they can also define Module) +{{{ preJS() }}} + #if !SINGLE_FILE #if PTHREADS @@ -214,3 +206,4 @@ if (ENVIRONMENT_IS_SHELL) { #endif #endif // !SINGLE_FILE + diff --git a/src/wasm_worker.js b/src/wasm_worker.js index 2418999dbea8..4391925883bd 100644 --- a/src/wasm_worker.js +++ b/src/wasm_worker.js @@ -17,15 +17,14 @@ if (ENVIRONMENT_IS_NODE) { parentPort.on('message', (data) => typeof onmessage === "function" && onmessage({ data: data })); var fs = require('fs'); + var vm = require('vm'); Object.assign(global, { self: global, require, - location: { - href: __filename - }, + __filename, Worker: nodeWorkerThreads.Worker, - importScripts: (f) => (0, eval)(fs.readFileSync(f, 'utf8') + '//# sourceURL=' + f), + importScripts: (f) => vm.runInThisContext(fs.readFileSync(f, 'utf8'), {filename: f}), postMessage: (msg) => parentPort.postMessage(msg), performance: global.performance || { now: Date.now }, addEventListener: (name, handler) => parentPort.on(name, handler), diff --git a/src/worker.js b/src/worker.js deleted file mode 100644 index 3e28808aa3ac..000000000000 --- a/src/worker.js +++ /dev/null @@ -1,299 +0,0 @@ -/** - * @license - * Copyright 2015 The Emscripten Authors - * SPDX-License-Identifier: MIT - */ - -// Pthread Web Worker startup routine: -// This is the entry point file that is loaded first by each Web Worker -// that executes pthreads on the Emscripten application. - -'use strict'; - -var Module = {}; - -#if ENVIRONMENT_MAY_BE_NODE -// Node.js support -var ENVIRONMENT_IS_NODE = typeof process == 'object' && typeof process.versions == 'object' && typeof process.versions.node == 'string'; -if (ENVIRONMENT_IS_NODE) { - // Create as web-worker-like an environment as we can. - - // See the parallel code in shell.js, but here we don't need the condition on - // multi-environment builds, as we do not have the need to interact with the - // modularization logic as shell.js must (see link.py:node_es6_imports and - // how that is used in link.py). -#if EXPORT_ES6 - const { createRequire } = await import('module'); - /** @suppress{duplicate} */ - var require = createRequire(import.meta.url); -#endif - - var nodeWorkerThreads = require('worker_threads'); - - var parentPort = nodeWorkerThreads.parentPort; - - parentPort.on('message', (data) => onmessage({ data: data })); - - var fs = require('fs'); - var vm = require('vm'); - - Object.assign(global, { - self: global, - require, - Module, - location: { - // __filename is undefined in ES6 modules, and import.meta.url only in ES6 - // modules. -#if EXPORT_ES6 - href: typeof __filename != 'undefined' ? __filename : import.meta.url -#else - href: __filename -#endif - }, - Worker: nodeWorkerThreads.Worker, - importScripts: (f) => vm.runInThisContext(fs.readFileSync(f, 'utf8'), {filename: f}), - postMessage: (msg) => parentPort.postMessage(msg), - performance: global.performance || { now: Date.now }, - }); -} -#endif // ENVIRONMENT_MAY_BE_NODE - -// Thread-local guard variable for one-time init of the JS state -var initializedJS = false; - -#if ASSERTIONS -function assert(condition, text) { - if (!condition) abort('Assertion failed: ' + text); -} -#endif - -function threadPrintErr(...args) { - var text = args.join(' '); -#if ENVIRONMENT_MAY_BE_NODE - // See https://github.com/emscripten-core/emscripten/issues/14804 - if (ENVIRONMENT_IS_NODE) { - fs.writeSync(2, text + '\n'); - return; - } -#endif - console.error(text); -} -function threadAlert(...args) { - var text = args.join(' '); - postMessage({cmd: 'alert', text, threadId: Module['_pthread_self']()}); -} -#if ASSERTIONS -// We don't need out() for now, but may need to add it if we want to use it -// here. Or, if this code all moves into the main JS, that problem will go -// away. (For now, adding it here increases code size for no benefit.) -var out = () => { throw 'out() is not defined in worker.js.'; } -#endif -var err = threadPrintErr; -self.alert = threadAlert; -#if ASSERTIONS || RUNTIME_DEBUG -var dbg = threadPrintErr; -#endif - -#if !MINIMAL_RUNTIME -Module['instantiateWasm'] = (info, receiveInstance) => { - // Instantiate from the module posted from the main thread. - // We can just use sync instantiation in the worker. - var module = Module['wasmModule']; - // We don't need the module anymore; new threads will be spawned from the main thread. - Module['wasmModule'] = null; - var instance = new WebAssembly.Instance(module, info); -#if RELOCATABLE || MAIN_MODULE - return receiveInstance(instance, module); -#else - // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, - // the above line no longer optimizes out down to the following line. - // When the regression is fixed, we can remove this if/else. - return receiveInstance(instance); -#endif -} -#endif - -// Turn unhandled rejected promises into errors so that the main thread will be -// notified about them. -self.onunhandledrejection = (e) => { - throw e.reason || e; -}; - -function handleMessage(e) { - try { - if (e.data.cmd === 'load') { // Preload command that is called once per worker to parse and load the Emscripten code. -#if PTHREADS_DEBUG - dbg('worker.js: loading module') -#endif -#if MINIMAL_RUNTIME - var imports = {}; -#endif - - // Until we initialize the runtime, queue up any further incoming messages. - let messageQueue = []; - self.onmessage = (e) => messageQueue.push(e); - - // And add a callback for when the runtime is initialized. - self.startWorker = (instance) => { -#if MODULARIZE - Module = instance; -#endif - // Notify the main thread that this thread has loaded. - postMessage({ 'cmd': 'loaded' }); - // Process any messages that were queued before the thread was ready. - for (let msg of messageQueue) { - handleMessage(msg); - } - // Restore the real message handler. - self.onmessage = handleMessage; - }; - - // Module and memory were sent from main thread -#if MINIMAL_RUNTIME -#if MODULARIZE - imports['wasm'] = e.data.wasmModule; // Pass the shared Wasm module in an imports object for the MODULARIZEd build. -#else - Module['wasm'] = e.data.wasmModule; // Pass the shared Wasm module in the Module object for MINIMAL_RUNTIME. -#endif -#else - Module['wasmModule'] = e.data.wasmModule; -#endif // MINIMAL_RUNTIME - -#if MAIN_MODULE - Module['sharedModules'] = e.data.sharedModules; -#if RUNTIME_DEBUG - dbg(`received ${Object.keys(e.data.sharedModules).length} shared modules: ${Object.keys(e.data.sharedModules)}`); -#endif -#endif - - // Use `const` here to ensure that the variable is scoped only to - // that iteration, allowing safe reference from a closure. - for (const handler of e.data.handlers) { - Module[handler] = (...args) => { -#if RUNTIME_DEBUG - dbg(`calling handler on main thread: ${handler}`); -#endif - postMessage({ cmd: 'callHandler', handler, args: args }); - } - } - - {{{ makeAsmImportsAccessInPthread('wasmMemory') }}} = e.data.wasmMemory; - -#if LOAD_SOURCE_MAP - Module['wasmSourceMapData'] = e.data.wasmSourceMap; -#endif -#if USE_OFFSET_CONVERTER - Module['wasmOffsetData'] = e.data.wasmOffsetConverter; -#endif - - {{{ makeAsmImportsAccessInPthread('buffer') }}} = {{{ makeAsmImportsAccessInPthread('wasmMemory') }}}.buffer; - -#if ASSERTIONS - Module['workerID'] = e.data.workerID; -#endif - -#if !MINIMAL_RUNTIME - // Set ENVIRONMENT_IS_PTHREAD before importing the main script - globalThis['ENVIRONMENT_IS_PTHREAD'] = true; -#endif - -#if MODULARIZE && EXPORT_ES6 - (e.data.urlOrBlob ? import(e.data.urlOrBlob) : import('./{{{ TARGET_JS_NAME }}}')) - .then(exports => exports.default(Module)); -#else - if (typeof e.data.urlOrBlob == 'string') { -#if TRUSTED_TYPES - if (typeof self.trustedTypes != 'undefined' && self.trustedTypes.createPolicy) { - var p = self.trustedTypes.createPolicy('emscripten#workerPolicy3', { createScriptURL: (ignored) => e.data.urlOrBlob }); - importScripts(p.createScriptURL('ignored')); - } else -#endif - importScripts(e.data.urlOrBlob); - } else { - var objectUrl = URL.createObjectURL(e.data.urlOrBlob); -#if TRUSTED_TYPES - if (typeof self.trustedTypes != 'undefined' && self.trustedTypes.createPolicy) { - var p = self.trustedTypes.createPolicy('emscripten#workerPolicy3', { createScriptURL: (ignored) => objectUrl }); - importScripts(p.createScriptURL('ignored')); - } else -#endif - importScripts(objectUrl); - URL.revokeObjectURL(objectUrl); - } -#if MODULARIZE -#if MINIMAL_RUNTIME - {{{ EXPORT_NAME }}}(imports); -#else - {{{ EXPORT_NAME }}}(Module); -#endif -#endif -#endif // MODULARIZE && EXPORT_ES6 - } else if (e.data.cmd === 'run') { - // Pass the thread address to wasm to store it for fast access. - Module['__emscripten_thread_init'](e.data.pthread_ptr, /*is_main=*/0, /*is_runtime=*/0, /*can_block=*/1); - - // Await mailbox notifications with `Atomics.waitAsync` so we can start - // using the fast `Atomics.notify` notification path. - Module['__emscripten_thread_mailbox_await'](e.data.pthread_ptr); - -#if ASSERTIONS - assert(e.data.pthread_ptr); -#endif - // Also call inside JS module to set up the stack frame for this pthread in JS module scope - Module['establishStackSpace'](); - Module['PThread'].receiveObjectTransfer(e.data); - Module['PThread'].threadInitTLS(); - - if (!initializedJS) { -#if EMBIND -#if PTHREADS_DEBUG - dbg(`Pthread 0x${Module['_pthread_self']().toString(16)} initializing embind.`); -#endif - // Embind must initialize itself on all threads, as it generates support JS. - // We only do this once per worker since they get reused - Module['__embind_initialize_bindings'](); -#endif // EMBIND - initializedJS = true; - } - - try { - Module['invokeEntryPoint'](e.data.start_routine, e.data.arg); - } catch(ex) { - if (ex != 'unwind') { - // The pthread "crashed". Do not call `_emscripten_thread_exit` (which - // would make this thread joinable). Instead, re-throw the exception - // and let the top level handler propagate it back to the main thread. - throw ex; - } -#if RUNTIME_DEBUG - dbg(`Pthread 0x${Module['_pthread_self']().toString(16)} completed its main entry point with an 'unwind', keeping the worker alive for asynchronous operation.`); -#endif - } - } else if (e.data.cmd === 'cancel') { // Main thread is asking for a pthread_cancel() on this thread. - if (Module['_pthread_self']()) { - Module['__emscripten_thread_exit']({{{ cDefs.PTHREAD_CANCELED }}}); - } - } else if (e.data.target === 'setimmediate') { - // no-op - } else if (e.data.cmd === 'checkMailbox') { - if (initializedJS) { - Module['checkMailbox'](); - } - } else if (e.data.cmd) { - // The received message looks like something that should be handled by this message - // handler, (since there is a e.data.cmd field present), but is not one of the - // recognized commands: - err(`worker.js received unknown command ${e.data.cmd}`); - err(e.data); - } - } catch(ex) { -#if ASSERTIONS - err(`worker.js onmessage() captured an uncaught exception: ${ex}`); - if (ex?.stack) err(ex.stack); -#endif - Module['__emscripten_thread_crashed']?.(); - throw ex; - } -}; - -self.onmessage = handleMessage; diff --git a/test/browser_reporting.js b/test/browser_reporting.js index fa3ac22cb0ae..eee9e988919b 100644 --- a/test/browser_reporting.js +++ b/test/browser_reporting.js @@ -63,7 +63,9 @@ function report_error(e) { } if (typeof window === 'object' && window) { - window.addEventListener('error', event => report_error(event.error)); + window.addEventListener('error', event => { + report_error(event.error || event) + }); window.addEventListener('unhandledrejection', event => report_error(event.reason)); } @@ -76,11 +78,17 @@ if (hasModule) { maybeReportResultToServer('exit:' + status); } } + // Force these handlers to be proxied back to the main thread. + // Without this tagging the handler will run each thread, which means + // each thread uses its own copy of `maybeReportResultToServer` which + // breaks the checking for duplicate reporting. + Module['onExit'].proxy = true; } if (!Module['onAbort']) { Module['onAbort'] = function(reason) { maybeReportResultToServer('abort:' + reason); } + Module['onAbort'].proxy = true; } } diff --git a/test/other/embind_tsgen_ignore_1.d.ts b/test/other/embind_tsgen_ignore_1.d.ts index 365c6fad4c16..befb5985963e 100644 --- a/test/other/embind_tsgen_ignore_1.d.ts +++ b/test/other/embind_tsgen_ignore_1.d.ts @@ -10,10 +10,6 @@ declare namespace RuntimeExports { let HEAPU32: any; let HEAP64: any; let HEAPU64: any; - function keepRuntimeAlive(): any; - /** @constructor */ - function ExitStatus(status: any): void; - let wasmMemory: any; let FS_createPath: any; function FS_createDataFile(parent: any, name: any, fileData: any, canRead: any, canWrite: any, canOwn: any): void; function FS_createPreloadedFile(parent: any, name: any, url: any, canRead: any, canWrite: any, onload: any, onerror: any, dontCreateFile: any, canOwn: any, preFinish: any): void; @@ -24,14 +20,8 @@ declare namespace RuntimeExports { let removeRunDependency: any; } interface WasmModule { - _pthread_self(): number; _main(_0: number, _1: number): number; - __embind_initialize_bindings(): void; - __emscripten_tls_init(): number; __emscripten_proxy_main(_0: number, _1: number): number; - __emscripten_thread_init(_0: number, _1: number, _2: number, _3: number, _4: number, _5: number): void; - __emscripten_thread_crashed(): void; - __emscripten_thread_exit(_0: number): void; } type EmbindString = ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|string; diff --git a/test/other/metadce/test_metadce_hello_dylink.gzsize b/test/other/metadce/test_metadce_hello_dylink.gzsize index 918dc7d7ea1a..8ce186d95f54 100644 --- a/test/other/metadce/test_metadce_hello_dylink.gzsize +++ b/test/other/metadce/test_metadce_hello_dylink.gzsize @@ -1 +1 @@ -6453 +6443 diff --git a/test/other/metadce/test_metadce_hello_dylink.jssize b/test/other/metadce/test_metadce_hello_dylink.jssize index cec22ff74b48..fdcb28a60e0d 100644 --- a/test/other/metadce/test_metadce_hello_dylink.jssize +++ b/test/other/metadce/test_metadce_hello_dylink.jssize @@ -1 +1 @@ -14319 +14308 diff --git a/test/other/metadce/test_metadce_minimal_pthreads.gzsize b/test/other/metadce/test_metadce_minimal_pthreads.gzsize index f01ddd67f2bc..651ba07e2d02 100644 --- a/test/other/metadce/test_metadce_minimal_pthreads.gzsize +++ b/test/other/metadce/test_metadce_minimal_pthreads.gzsize @@ -1 +1 @@ -6099 +5254 diff --git a/test/other/metadce/test_metadce_minimal_pthreads.jssize b/test/other/metadce/test_metadce_minimal_pthreads.jssize index 8544f4bdba02..7374ccef7d4d 100644 --- a/test/other/metadce/test_metadce_minimal_pthreads.jssize +++ b/test/other/metadce/test_metadce_minimal_pthreads.jssize @@ -1 +1 @@ -13430 +11457 diff --git a/test/other/test_unoptimized_code_size.js.size b/test/other/test_unoptimized_code_size.js.size index 713cb9839dec..2fbf80c56226 100644 --- a/test/other/test_unoptimized_code_size.js.size +++ b/test/other/test_unoptimized_code_size.js.size @@ -1 +1 @@ -55596 +55580 diff --git a/test/other/test_unoptimized_code_size_no_asserts.js.size b/test/other/test_unoptimized_code_size_no_asserts.js.size index fed9441206d5..5802b66090c5 100644 --- a/test/other/test_unoptimized_code_size_no_asserts.js.size +++ b/test/other/test_unoptimized_code_size_no_asserts.js.size @@ -1 +1 @@ -31481 +31465 diff --git a/test/other/test_unoptimized_code_size_strict.js.size b/test/other/test_unoptimized_code_size_strict.js.size index ecc9f68d5180..567862079995 100644 --- a/test/other/test_unoptimized_code_size_strict.js.size +++ b/test/other/test_unoptimized_code_size_strict.js.size @@ -1 +1 @@ -54483 +54468 diff --git a/test/test_browser.py b/test/test_browser.py index 58b1fa22c4d1..c272da01d3f2 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -3997,46 +3997,6 @@ def test_pthread_supported(self, args): def test_pthread_dispatch_after_exit(self): self.btest_exit('pthread/test_pthread_dispatch_after_exit.c', args=['-pthread']) - # Test the operation of Module.pthreadMainPrefixURL variable - @requires_wasm2js - def test_pthread_custom_pthread_main_url(self): - self.set_setting('EXIT_RUNTIME') - ensure_dir('cdn') - create_file('main.cpp', r''' - #include - #include - #include - #include - #include - #include - - _Atomic int result = 0; - void *thread_main(void *arg) { - result = 1; - pthread_exit(0); - } - - int main() { - pthread_t t; - pthread_create(&t, 0, thread_main, 0); - pthread_join(t, 0); - assert(result == 1); - return 0; - } - ''') - - # Test that it is possible to define "Module.locateFile" string to locate where worker.js will be loaded from. - create_file('shell.html', read_file(path_from_root('src/shell.html')).replace('var Module = {', 'var Module = { locateFile: function (path, prefix) {if (path.endsWith(".wasm")) {return prefix + path;} else {return "cdn/" + path;}}, ')) - self.compile_btest('main.cpp', ['--shell-file', 'shell.html', '-pthread', '-sPTHREAD_POOL_SIZE', '-o', 'test.html'], reporting=Reporting.JS_ONLY) - shutil.move('test.worker.js', Path('cdn/test.worker.js')) - self.run_browser('test.html', '/report_result?exit:0') - - # Test that it is possible to define "Module.locateFile(foo)" function to locate where worker.js will be loaded from. - create_file('shell2.html', read_file(path_from_root('src/shell.html')).replace('var Module = {', 'var Module = { locateFile: function(filename) { if (filename == "test.worker.js") return "cdn/test.worker.js"; else return filename; }, ')) - self.compile_btest('main.cpp', ['--shell-file', 'shell2.html', '-pthread', '-sPTHREAD_POOL_SIZE', '-o', 'test2.html'], reporting=Reporting.JS_ONLY) - delete_file('test.worker.js') - self.run_browser('test2.html', '/report_result?exit:0') - # Test that if the main thread is performing a futex wait while a pthread # needs it to do a proxied operation (before that pthread would wake up the # main thread), that it's not a deadlock. diff --git a/test/test_core.py b/test/test_core.py index 3baf7ea93535..b9cdf2055589 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -9382,6 +9382,7 @@ def test_Module_dynamicLibraries(self, args): # test that Module.dynamicLibraries works with pthreads self.emcc_args += args self.emcc_args += ['--pre-js', 'pre.js'] + self.emcc_args += ['--js-library', 'lib.js'] # This test is for setting dynamicLibraries at runtime so we don't # want emscripten loading `liblib.so` automatically (which it would # do without this setting. @@ -9391,22 +9392,29 @@ def test_Module_dynamicLibraries(self, args): Module['dynamicLibraries'] = ['liblib.so']; ''') - if args: - self.setup_node_pthreads() - create_file('post.js', ''' - if (ENVIRONMENT_IS_PTHREAD) { + create_file('lib.js', ''' + addToLibrary({ + mainCallback: () => { +#if PTHREADS err('sharedModules: ' + Object.keys(sharedModules)); assert('liblib.so' in sharedModules); assert(sharedModules['liblib.so'] instanceof WebAssembly.Module); - } - ''') - self.emcc_args += ['--post-js', 'post.js'] +#endif + }, + }) + ''') + + if args: + self.setup_node_pthreads() self.dylink_test( r''' + void mainCallback(); + #include int side(); int main() { + mainCallback(); printf("result is %d\n", side()); return 0; } @@ -9414,7 +9422,8 @@ def test_Module_dynamicLibraries(self, args): r''' int side() { return 42; } ''', - 'result is 42') + 'result is 42', + force_c=True) # Tests the emscripten_get_exported_function() API. def test_get_exported_function(self): diff --git a/test/test_other.py b/test/test_other.py index cd8da1fdbf62..06548b78b025 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -345,11 +345,8 @@ def test_emcc_output_worker_mjs(self, args): test_file('hello_world.c')] + args) src = read_file('subdir/hello_world.mjs') self.assertContained("new URL('hello_world.wasm', import.meta.url)", src) - self.assertContained("new Worker(new URL('hello_world.worker.mjs', import.meta.url), {type: 'module'})", src) - self.assertContained("new Worker(pthreadMainJs, {type: 'module'})", src) + self.assertContained("new Worker(new URL(import.meta.url), workerOptions)", src) self.assertContained('export default Module;', src) - src = read_file('subdir/hello_world.worker.mjs') - self.assertContained("import('./hello_world.mjs')", src) self.assertContained('hello, world!', self.run_js('subdir/hello_world.mjs')) @node_pthreads @@ -360,8 +357,7 @@ def test_emcc_output_worker_mjs_single_file(self): test_file('hello_world.c'), '-sSINGLE_FILE']) src = read_file('hello_world.mjs') self.assertNotContained("new URL('data:", src) - self.assertContained("new Worker(new URL('hello_world.worker.mjs', import.meta.url), {type: 'module'})", src) - self.assertContained("new Worker(pthreadMainJs, {type: 'module'})", src) + self.assertContained("new Worker(new URL(import.meta.url), workerOptions)", src) self.assertContained('hello, world!', self.run_js('hello_world.mjs')) def test_emcc_output_mjs_closure(self): @@ -409,8 +405,7 @@ def test_export_es6_allows_export_in_post_js(self): @parameterized({ '': ([],), # load a worker before startup to check ES6 modules there as well - # pass -O2 to ensure the worker JS file is minified with Acorn - 'pthreads': (['-O2', '-pthread', '-sPTHREAD_POOL_SIZE=1'],), + 'pthreads': (['-pthread', '-sPTHREAD_POOL_SIZE=1'],), }) def test_export_es6(self, args, package_json): self.run_process([EMCC, test_file('hello_world.c'), '-sEXPORT_ES6', @@ -8409,9 +8404,6 @@ def run_metadce_test(self, filename, args=[], expected_exists=[], expected_not_e size_file = expected_basename + '.size' js_size = os.path.getsize('a.out.js') gz_size = get_file_gzipped_size('a.out.js') - if '-pthread' in args: - js_size += os.path.getsize('a.out.worker.js') - gz_size += get_file_gzipped_size('a.out.worker.js') js_size_file = expected_basename + '.jssize' gz_size_file = expected_basename + '.gzsize' self.check_expected_size_in_file('wasm', size_file, wasm_size) @@ -14486,10 +14478,17 @@ def test_embind_no_duplicate_symbols(self): create_file('b.cpp', '#include ') self.run_process([EMXX, '-std=c++23', '-lembind', 'a.cpp', 'b.cpp']) + def test_legacy_pthread_worker_js(self): + self.do_runf('hello_world.c', emcc_args=['-pthread', '-sSTRICT']) + self.assertNotExists('hello_world.worker.js') + self.do_runf('hello_world.c', emcc_args=['-pthread']) + self.assertExists('hello_world.worker.js') + def test_no_pthread(self): self.do_runf('hello_world.c', emcc_args=['-pthread', '-no-pthread']) self.assertExists('hello_world.js') self.assertNotExists('hello_world.worker.js') + self.assertNotContained('Worker', read_file('hello_world.js')) def test_sysroot_includes_first(self): self.do_other_test('test_stdint_limits.c', emcc_args=['-std=c11', '-iwithsysroot/include']) diff --git a/test/wasm_worker/shared_memory.c b/test/wasm_worker/shared_memory.c index cf9e5cf56d5a..9b03a6032d8b 100644 --- a/test/wasm_worker/shared_memory.c +++ b/test/wasm_worker/shared_memory.c @@ -1,7 +1,8 @@ #include #include -int main() -{ - printf("%d\n", EM_ASM_INT(return wasmMemory.buffer instanceof SharedArrayBuffer)); +int main() { + int is_shared = EM_ASM_INT(return wasmMemory.buffer instanceof SharedArrayBuffer); + printf("%d\n", is_shared); + return 0; } diff --git a/tools/acorn-optimizer.mjs b/tools/acorn-optimizer.mjs index 4628ef7aa032..1835326b1ae8 100755 --- a/tools/acorn-optimizer.mjs +++ b/tools/acorn-optimizer.mjs @@ -471,6 +471,15 @@ function runAJSDCE(ast) { function isWasmImportsAssign(node) { // var wasmImports = .. + // or + // wasmImports = .. + if ( + node.type === 'AssignmentExpression' && + node.left.name == 'wasmImports' && + node.right.type == 'ObjectExpression' + ) { + return true; + } return ( node.type === 'VariableDeclaration' && node.declarations.length === 1 && @@ -481,7 +490,11 @@ function isWasmImportsAssign(node) { } function getWasmImportsValue(node) { - return node.declarations[0].init; + if (node.declarations) { + return node.declarations[0].init; + } else { + return node.right; + } } function isExportUse(node) { diff --git a/tools/building.py b/tools/building.py index bb08cc58915a..b52f4a2f159b 100644 --- a/tools/building.py +++ b/tools/building.py @@ -599,9 +599,6 @@ def closure_compiler(filename, advanced=True, extra_closure_args=None): if settings.DYNCALLS: CLOSURE_EXTERNS += [path_from_root('src/closure-externs/dyncall-externs.js')] - if settings.MINIMAL_RUNTIME and settings.PTHREADS: - CLOSURE_EXTERNS += [path_from_root('src/closure-externs/minimal_runtime_worker_externs.js')] - args = ['--compilation_level', 'ADVANCED_OPTIMIZATIONS' if advanced else 'SIMPLE_OPTIMIZATIONS'] # Keep in sync with ecmaVersion in tools/acorn-optimizer.mjs args += ['--language_in', 'ECMASCRIPT_2021'] diff --git a/tools/emscripten.py b/tools/emscripten.py index 3217c1d6d7ec..347027a96201 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -18,6 +18,7 @@ import pprint import shutil import sys +import textwrap from tools import building from tools import config @@ -886,7 +887,7 @@ def install_wrapper(sym): # With assertions enabled we create a wrapper that are calls get routed through, for # the lifetime of the program. wrapper += f"createExportWrapper('{name}', {nargs});" - elif settings.WASM_ASYNC_COMPILATION: + elif settings.WASM_ASYNC_COMPILATION or settings.PTHREADS: # With WASM_ASYNC_COMPILATION wrapper will replace the global var and Module var on # first use. args = [f'a{i}' for i in range(nargs)] @@ -939,12 +940,16 @@ def create_module(receiving, metadata, library_symbols): module = [] sending = create_sending(metadata, library_symbols) - module.append('var wasmImports = %s;\n' % sending) - if settings.ASYNCIFY and (settings.ASSERTIONS or settings.ASYNCIFY == 2): - # instrumenting imports is used in asyncify in two ways: to add assertions - # that check for proper import use, and for ASYNCIFY=2 we use them to set up - # the Promise API on the import side. - module.append('Asyncify.instrumentWasmImports(wasmImports);\n') + if settings.PTHREADS: + sending = textwrap.indent(sending, ' ').strip() + module.append('''\ +var wasmImports; +function assignWasmImports() { + wasmImports = %s; +} +''' % sending) + else: + module.append('var wasmImports = %s;\n' % sending) if not settings.MINIMAL_RUNTIME: module.append("var wasmExports = createWasm();\n") diff --git a/tools/js_manipulation.py b/tools/js_manipulation.py index f860671b83fa..ced8f4ed2850 100644 --- a/tools/js_manipulation.py +++ b/tools/js_manipulation.py @@ -44,7 +44,7 @@ def add_files_pre_js(pre_js_list, files_pre_js): utils.write_file(pre, ''' // All the pre-js content up to here must remain later on, we need to run // it. - if (globalThis['ENVIRONMENT_IS_PTHREAD'] || Module['$ww']) Module['preRun'] = []; + if (Module['$ww'] || (typeof ENVIRONMENT_IS_PTHREAD != 'undefined' && ENVIRONMENT_IS_PTHREAD)) Module['preRun'] = []; var necessaryPreJSTasks = Module['preRun'].slice(); ''') utils.write_file(post, ''' diff --git a/tools/link.py b/tools/link.py index eaebd41d5ce6..a7e274ca3e0a 100644 --- a/tools/link.py +++ b/tools/link.py @@ -467,7 +467,7 @@ def get_worker_js_suffix(): return '.worker.mjs' if settings.EXPORT_ES6 else '.worker.js' -def setup_pthreads(target): +def setup_pthreads(): if settings.RELOCATABLE: # pthreads + dynamic linking has certain limitations if settings.SIDE_MODULE: @@ -484,12 +484,16 @@ def setup_pthreads(target): # Functions needs to be exported from the module since they are used in worker.js settings.REQUIRED_EXPORTS += [ '_emscripten_thread_free_data', + '_emscripten_thread_crashed', 'emscripten_main_runtime_thread_id', 'emscripten_main_thread_process_queued_calls', '_emscripten_run_on_main_thread_js', 'emscripten_stack_set_limits', ] + if settings.EMBIND: + settings.REQUIRED_EXPORTS.append('_embind_initialize_bindings') + if settings.MAIN_MODULE: settings.REQUIRED_EXPORTS += [ '_emscripten_dlsync_self', @@ -499,61 +503,18 @@ def setup_pthreads(target): '__dl_seterr', ] + # runtime_pthread.js depends on these library symbols settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += [ - '$exitOnMainThread', + '$PThread', + '$establishStackSpace', + '$invokeEntryPoint', ] - # Some symbols are required by worker.js. - # Because emitDCEGraph only considers the main js file, and not worker.js - # we have explicitly mark these symbols as user-exported so that they will - # kept alive through DCE. - # TODO: Find a less hacky way to do this, perhaps by also scanning worker.js - # for roots. - worker_imports = [ - '__emscripten_thread_init', - '__emscripten_thread_exit', - '__emscripten_thread_crashed', - '__emscripten_thread_mailbox_await', - '__emscripten_tls_init', - '_pthread_self', - 'checkMailbox', - ] - if settings.EMBIND: - worker_imports.append('__embind_initialize_bindings') - settings.EXPORTED_FUNCTIONS += worker_imports - building.user_requested_exports.update(worker_imports) - - # set location of worker.js - settings.PTHREAD_WORKER_FILE = unsuffixed_basename(target) + get_worker_js_suffix() if settings.MINIMAL_RUNTIME: building.user_requested_exports.add('exit') - # pthread stack setup and other necessary utilities - def include_and_export(name): - settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$' + name] - settings.EXPORTED_FUNCTIONS += [name] - - include_and_export('establishStackSpace') - include_and_export('invokeEntryPoint') - include_and_export('PThread') - if not settings.MINIMAL_RUNTIME: - # keepRuntimeAlive does not apply to MINIMAL_RUNTIME. - settings.EXPORTED_RUNTIME_METHODS += ['keepRuntimeAlive', 'ExitStatus', 'wasmMemory'] - - if settings.MODULARIZE: - if not settings.EXPORT_ES6 and settings.EXPORT_NAME == 'Module': - exit_with_error('pthreads + MODULARIZE currently require you to set -sEXPORT_NAME=Something (see settings.js) to Something != Module, so that the .worker.js file can work') - - # MODULARIZE+PTHREADS mode requires extra exports out to Module so that worker.js - # can access them: - - # general threading variables: - settings.EXPORTED_RUNTIME_METHODS += ['PThread'] - - # To keep code size to minimum, MINIMAL_RUNTIME does not utilize the global ExitStatus - # object, only regular runtime has it. - if not settings.MINIMAL_RUNTIME: - settings.EXPORTED_RUNTIME_METHODS += ['ExitStatus'] + if settings.MODULARIZE and not settings.EXPORT_ES6 and settings.EXPORT_NAME == 'Module': + exit_with_error('pthreads + MODULARIZE currently require you to set -sEXPORT_NAME=Something (see settings.js) to Something != Module, so that the .worker.js file can work') def set_initial_memory(): @@ -724,9 +685,9 @@ def phase_linker_setup(options, state, newargs): if settings.PTHREADS: # Don't run extern pre/post code on pthreads. if options.extern_pre_js: - options.extern_pre_js = 'if (typeof ENVIRONMENT_IS_PTHREAD == "undefined") {' + options.extern_pre_js + '}' + options.extern_pre_js = 'if (!isPthread) {' + options.extern_pre_js + '}' if options.extern_post_js: - options.extern_post_js = 'if (typeof ENVIRONMENT_IS_PTHREAD == "undefined") {' + options.extern_post_js + '}' + options.extern_post_js = 'if (!isPthread) {' + options.extern_post_js + '}' # TODO: support source maps with js_transform if options.js_transform and settings.GENERATE_SOURCE_MAP: @@ -1366,7 +1327,7 @@ def phase_linker_setup(options, state, newargs): settings.EMIT_TSD = True if settings.PTHREADS: - setup_pthreads(target) + setup_pthreads() settings.JS_LIBRARIES.append((0, 'library_pthread.js')) if settings.PROXY_TO_PTHREAD: settings.PTHREAD_POOL_SIZE_STRICT = 0 @@ -1841,12 +1802,14 @@ def get_full_import_name(name): if settings.SIDE_MODULE: # For side modules, we ignore all REQUIRED_EXPORTS that might have been added above. # They all come from either libc or compiler-rt. The exception is __wasm_call_ctors - # which is a per-module export. + # and _emscripten_tls_init which are per-module exports. settings.REQUIRED_EXPORTS.clear() if not settings.STANDALONE_WASM: # in standalone mode, crt1 will call the constructors from inside the wasm settings.REQUIRED_EXPORTS.append('__wasm_call_ctors') + if settings.PTHREADS: + settings.REQUIRED_EXPORTS.append('_emscripten_tls_init') settings.PRE_JS_FILES = [os.path.abspath(f) for f in options.pre_js] settings.POST_JS_FILES = [os.path.abspath(f) for f in options.post_js] @@ -2068,8 +2031,13 @@ def phase_final_emitting(options, state, target, wasm_target): return target_dir = os.path.dirname(os.path.abspath(target)) - if settings.PTHREADS: - create_worker_file('src/worker.js', target_dir, settings.PTHREAD_WORKER_FILE, options) + if settings.PTHREADS and not settings.STRICT: + write_file(unsuffixed_basename(target) + '.worker.js', '''\ +// This file is no longer used by emscripten and has been created as a placeholder +// to allow build systems to transition away from depending on it. +// +// Future versions of emscripten will likely stop generating this file at all. +''') # Deploy the Wasm Worker bootstrap file as an output file (*.ww.js) if settings.WASM_WORKERS == 1: @@ -2080,7 +2048,7 @@ def phase_final_emitting(options, state, target, wasm_target): create_worker_file('src/audio_worklet.js', target_dir, settings.AUDIO_WORKLET_FILE, options) if settings.MODULARIZE: - modularize() + modularize(options) elif settings.USE_CLOSURE_COMPILER: module_export_name_substitution() @@ -2107,6 +2075,8 @@ def phase_final_emitting(options, state, target, wasm_target): src = read_file(final_js) final_js += '.epp.js' with open(final_js, 'w', encoding='utf-8') as f: + if settings.PTHREADS and (options.extern_pre_js or options.extern_post_js): + f.write(make_pthread_detection()) f.write(options.extern_pre_js) f.write(src) f.write(options.extern_post_js) @@ -2335,7 +2305,43 @@ def node_es6_imports(): ''' -def modularize(): +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 make_pthread_detection(): + """Create code for detecting if we are running in a pthread. + + Normally this detection is done when the module is itself is run but there + are some cases were we need to do this detection outside of the normal + module code. + + 1. When running in MODULARIZE mode we need use this to know if we should + run the module constructor on startup (true only for pthreads) + 2. When using `--extern-pre-js` and `--extern-post-js` we need to avoid + running this code on pthreads. + """ + + if settings.ENVIRONMENT_MAY_BE_WEB or settings.ENVIRONMENT_MAY_BE_WORKER: + code = "var isPthread = globalThis.self?.name === 'em-pthread';\n" + # In order to support both web and node we also need to detect node here. + if settings.ENVIRONMENT_MAY_BE_NODE: + code += "var isNode = typeof globalThis.process?.versions?.node == 'string';\n" + code += f'if (isNode) isPthread = {node_pthread_detection()}\n' + elif settings.ENVIRONMENT_MAY_BE_NODE: + code = f'var isPthread = {node_pthread_detection()}\n' + else: + assert False + + return code + + +def modularize(options): global final_js logger.debug(f'Modularizing, assigning to var {settings.EXPORT_NAME}') src = read_file(final_js) @@ -2411,12 +2417,13 @@ def modularize(): # 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 and settings.MODULARIZE: - src += f'globalThis.AudioWorkletModule = {settings.EXPORT_NAME};' + 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: - src += 'export default %s;' % settings.EXPORT_NAME + src += 'export default %s;\n' % settings.EXPORT_NAME + elif not settings.MINIMAL_RUNTIME: src += '''\ if (typeof exports === 'object' && typeof module === 'object') @@ -2425,6 +2432,12 @@ def modularize(): define([], () => %(EXPORT_NAME)s); ''' % {'EXPORT_NAME': settings.EXPORT_NAME} + if settings.PTHREADS: + if not options.extern_pre_js and not options.extern_post_js: + src += make_pthread_detection() + src += '// When running as a pthread, construct a new instance on startup\n' + src += 'isPthread && %s();\n' % settings.EXPORT_NAME + final_js += '.modular.js' write_file(final_js, src) shared.get_temp_files().note(final_js) diff --git a/tools/minimal_runtime_shell.py b/tools/minimal_runtime_shell.py index b26359544333..dca647ea972b 100644 --- a/tools/minimal_runtime_shell.py +++ b/tools/minimal_runtime_shell.py @@ -68,9 +68,6 @@ def generate_minimal_runtime_load_statement(target_basename): if download_wasm and settings.WASM_WORKERS == 1: files_to_load += ["binary('%s')" % (target_basename + '.ww.js')] - if settings.MODULARIZE and settings.PTHREADS: - modularize_imports += ["worker: '{{{ PTHREAD_WORKER_FILE }}}'"] - # Download Wasm2JS code if target browser does not support WebAssembly if settings.WASM == 2: if settings.MODULARIZE: @@ -194,7 +191,7 @@ def generate_minimal_runtime_html(target, options, js_target, target_basename): shell = shell.replace('{{{ TARGET_BASENAME }}}', target_basename) shell = shell.replace('{{{ EXPORT_NAME }}}', settings.EXPORT_NAME) - shell = shell.replace('{{{ PTHREAD_WORKER_FILE }}}', settings.PTHREAD_WORKER_FILE) + shell = shell.replace('{{{ TARGET_JS_NAME }}}', settings.TARGET_JS_NAME) # In SINGLE_FILE build, embed the main .js file into the .html output if settings.SINGLE_FILE: diff --git a/tools/unsafe_optimizations.mjs b/tools/unsafe_optimizations.mjs index f0b8f3a9bad0..e7f63e7c7efa 100755 --- a/tools/unsafe_optimizations.mjs +++ b/tools/unsafe_optimizations.mjs @@ -69,7 +69,13 @@ function optPassRemoveRedundantOperatorNews(ast) { for (let i = 0; i < nodeArray.length; ++i) { const n = nodeArray[i]; if (n.type == 'ExpressionStatement' && n.expression.type == 'NewExpression') { - nodeArray.splice(i--, 1); + // Make an exception for new `new Promise` which is sometimes used + // in emscripten with real side effects. For example, see + // loadWasmModuleToWorker which returns a `new Promise` that is never + // referenced (a least in some builds). + if (n.expression.callee.name !== 'Promise') { + nodeArray.splice(i--, 1); + } } } }); @@ -252,6 +258,8 @@ function runTests() { 'WebAssembly.instantiate(c.wasm,{}).then(a=>{});', ); test('let x=new Uint16Array(a);', 'let x=new Uint16Array(a);'); + // new Promise should be preserved + test('new Promise();', 'new Promise;'); // optPassMergeVarDeclarations: test('var a; var b;', 'var a,b;');