From 1887cc4770656cd37db595795b73af322d3b971e Mon Sep 17 00:00:00 2001 From: James Seo Date: Sat, 3 Feb 2024 01:08:51 -0800 Subject: [PATCH] Fix broken JS glue for AUDIO_WORKLETS with EXPORT_ES6 WASM Audio Worklets with EXPORT_ES6 may break at runtime: test.js:989 Uncaught (in promise) TypeError: Cannot set property wasmTable of # which has only a getter at receiveInstance (test.js:989:25) at receiveInstantiationResult (test.js:1011:5) The read-only getter at issue is created in ASSERTIONS-enabled builds, and conflicts with the current way of exporting wasmTable on the Module object. Exporting wasmTable via EXPORTED_RUNTIME_METHODS prevents the getter from being created in normal builds. In MINIMAL_RUNTIME builds, we make sure to delete the getter before manually exporting as before. We also prevent an ES6 Audio Worklet from loading the .wasm binary via `new URL()`, as `URL` is unavailable in AudioWorkletGlobalScope. --- src/postamble_minimal.js | 10 +++++----- src/preamble.js | 11 ++--------- test/test_browser.py | 2 ++ tools/link.py | 21 +++++++++++++-------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/postamble_minimal.js b/src/postamble_minimal.js index 0a0c670cce2ce..5638a986cce94 100644 --- a/src/postamble_minimal.js +++ b/src/postamble_minimal.js @@ -197,19 +197,19 @@ WebAssembly.instantiate(Module['wasm'], imports).then((output) => { #if AUDIO_WORKLET // If we are in the audio worklet environment, we can only access the Module object // and not the global scope of the main JS script. Therefore we need to export - // all functions that the audio worklet scope needs onto the Module object. - Module['wasmTable'] = wasmTable; + // all symbols that the audio worklet scope needs onto the Module object. #if ASSERTIONS - // In ASSERTIONS-enabled builds, the following symbols have gotten read-only getters - // saved to the Module. Remove those getters so we can manually export the stack - // functions here. + // In ASSERTIONS-enabled builds, the needed symbols have gotten read-only getters + // saved to the Module. Remove the getters so we can manually export them here. delete Module['stackSave']; delete Module['stackAlloc']; delete Module['stackRestore']; + delete Module['wasmTable']; #endif Module['stackSave'] = stackSave; Module['stackAlloc'] = stackAlloc; Module['stackRestore'] = stackRestore; + Module['wasmTable'] = wasmTable; #endif #if !IMPORTED_MEMORY diff --git a/src/preamble.js b/src/preamble.js index 032eff4b8eeb8..a49a8f4f5a8fb 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -608,14 +608,14 @@ function instrumentWasmTableWithAbort() { #endif var wasmBinaryFile; -#if EXPORT_ES6 && USE_ES6_IMPORT_META && !SINGLE_FILE +#if EXPORT_ES6 && USE_ES6_IMPORT_META && !SINGLE_FILE && !AUDIO_WORKLET if (Module['locateFile']) { #endif wasmBinaryFile = '{{{ WASM_BINARY_FILE }}}'; if (!isDataURI(wasmBinaryFile)) { wasmBinaryFile = locateFile(wasmBinaryFile); } -#if EXPORT_ES6 && USE_ES6_IMPORT_META && !SINGLE_FILE // in single-file mode, repeating WASM_BINARY_FILE would emit the contents again +#if EXPORT_ES6 && USE_ES6_IMPORT_META && !SINGLE_FILE && !AUDIO_WORKLET // In single-file mode, repeating WASM_BINARY_FILE would emit the contents again. For an Audio Worklet, we cannot use `new URL()`. } else { #if ENVIRONMENT_MAY_BE_SHELL if (ENVIRONMENT_IS_SHELL) @@ -1000,13 +1000,6 @@ function createWasm() { #if ASSERTIONS && !PURE_WASI assert(wasmTable, 'table not found in wasm exports'); #endif - -#if AUDIO_WORKLET - // If we are in the audio worklet environment, we can only access the Module object - // and not the global scope of the main JS script. Therefore we need to export - // all functions that the audio worklet scope needs onto the Module object. - Module['wasmTable'] = wasmTable; -#endif #endif #if hasExportedSymbol('__wasm_call_ctors') diff --git a/test/test_browser.py b/test/test_browser.py index 2bd8adb6a711f..5c65f9f8857b5 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5747,6 +5747,8 @@ def test_full_js_library_strict(self): 'pthreads_and_closure': (['-pthread', '--closure', '1', '-Oz'],), 'minimal_runtime': (['-sMINIMAL_RUNTIME'],), 'minimal_runtime_pthreads_and_closure': (['-sMINIMAL_RUNTIME', '-pthread', '--closure', '1', '-Oz'],), + 'pthreads_es6': (['-pthread', '-sPTHREAD_POOL_SIZE=2', '-sEXPORT_ES6'],), + 'es6': (['-sEXPORT_ES6'],), }) def test_audio_worklet(self, args): if '-sMEMORY64' in args and is_firefox(): diff --git a/tools/link.py b/tools/link.py index e0dc389d4ae77..c92c132487b15 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1339,9 +1339,12 @@ def phase_linker_setup(options, state, newargs): settings.AUDIO_WORKLET_FILE = unsuffixed(os.path.basename(target)) + '.aw.js' settings.JS_LIBRARIES.append((0, shared.path_from_root('src', 'library_webaudio.js'))) if not settings.MINIMAL_RUNTIME: + # If we are in the audio worklet environment, we can only access the Module object + # and not the global scope of the main JS script. Therefore we need to export + # all symbols that the audio worklet scope needs onto the Module object. # MINIMAL_RUNTIME exports these manually, since this export mechanism is placed # in global scope that is not suitable for MINIMAL_RUNTIME loader. - settings.EXPORTED_RUNTIME_METHODS += ['stackSave', 'stackAlloc', 'stackRestore'] + settings.EXPORTED_RUNTIME_METHODS += ['stackSave', 'stackAlloc', 'stackRestore', 'wasmTable'] if settings.FORCE_FILESYSTEM and not settings.MINIMAL_RUNTIME: # when the filesystem is forced, we export by default methods that filesystem usage @@ -2349,13 +2352,15 @@ def modularize(): 'script_url_node': script_url_node, 'src': src, } - # 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. - if settings.AUDIO_WORKLET and settings.MODULARIZE: - src += f'globalThis.AudioWorkletModule = {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 and settings.MODULARIZE: + src += f'globalThis.AudioWorkletModule = {settings.EXPORT_NAME};' # Export using a UMD style export, or ES6 exports if selected if settings.EXPORT_ES6: