diff --git a/ChangeLog.md b/ChangeLog.md index dc8be88d34a52..ae32353d202ad 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -48,6 +48,13 @@ See docs/process.md for more on how version tagging works. - emcc will now error if `MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION` or `MINIMAL_RUNTIME_STREAMING_WASM_INSTANTIATION` are used with `SINGLE_FILE`. These are fundamentally incompatible but were previously ignored. (#24849) +- Added two new proxying directives. foo__proxy: 'abort' will abort program + execution if JS function foo is called from a pthread or a Wasm Worker. + foo__proxy: 'abort_debug' will do the same, but only in ASSERTIONS builds, and + in non-ASSERTIONS builds will be no-op. + Use these proxying directives to annotate JS functions that should not be + getting called from Workers. (#22648) + - `--export-es6` flag was added to `file_packager.py` available when run standalone, to enable ES6 imports of generated JavaScript code (#24737) diff --git a/src/jsifier.mjs b/src/jsifier.mjs index b7d9dd17e2a94..612bc638df91e 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -17,6 +17,7 @@ import { ATPRERUNS, ATMAINS, ATPOSTRUNS, + ENVIRONMENT_IS_WORKER_THREAD, defineI64Param, indentify, makeReturn64, @@ -437,39 +438,38 @@ function(${args}) { const proxyingMode = LibraryManager.library[symbol + '__proxy']; if (proxyingMode) { - if (proxyingMode !== 'sync' && proxyingMode !== 'async' && proxyingMode !== 'none') { - throw new Error(`Invalid proxyingMode ${symbol}__proxy: '${proxyingMode}' specified!`); + const possibleProxyingModes = ['sync', 'async', 'none', 'abort', 'abort_debug']; + if (!possibleProxyingModes.includes(proxyingMode)) { + throw new Error(`Invalid proxyingMode ${symbol}__proxy: '${proxyingMode}' specified! Possible modes: ${possibleProxyingModes.join(',')}`); } if (SHARED_MEMORY && proxyingMode != 'none') { - const sync = proxyingMode === 'sync'; - if (PTHREADS) { - snippet = modifyJSFunction(snippet, (args, body, async_, oneliner) => { - if (oneliner) { - body = `return ${body}`; + snippet = modifyJSFunction(snippet, (args, body, async_, oneliner) => { + if (oneliner) { + body = `return ${body}`; + } + const rtnType = sig && sig.length ? sig[0] : null; + const proxyFunc = + MEMORY64 && rtnType == 'p' ? 'proxyToMainThreadPtr' : 'proxyToMainThread'; + var prefix = ''; + + if (proxyingMode == 'sync' || proxyingMode == 'async') { + if (PTHREADS) { + deps.push('$' + proxyFunc); + prefix = `if (ENVIRONMENT_IS_PTHREAD) return ${proxyFunc}(${proxiedFunctionTable.length}, 0, ${+(proxyingMode === 'sync')}${args ? ', ' : ''}${args});`; } - const rtnType = sig && sig.length ? sig[0] : null; - const proxyFunc = - MEMORY64 && rtnType == 'p' ? 'proxyToMainThreadPtr' : 'proxyToMainThread'; - deps.push('$' + proxyFunc); - return ` -${async_}function(${args}) { -if (ENVIRONMENT_IS_PTHREAD) - return ${proxyFunc}(${proxiedFunctionTable.length}, 0, ${+sync}${args ? ', ' : ''}${args}); + if (WASM_WORKERS && ASSERTIONS) { + prefix += `\nassert(!ENVIRONMENT_IS_WASM_WORKER, "Attempted to call proxied function '${mangled}' in a Wasm Worker, but in Wasm Worker enabled builds, proxied function architecture is not available!");`; + } + } else if (proxyingMode == 'abort' || (proxyingMode == 'abort_debug' && ASSERTIONS)) { + const insideWorker = ENVIRONMENT_IS_WORKER_THREAD(); + prefix = `if (${insideWorker}) abort("Attempted to call function '${mangled}' inside a pthread/Wasm Worker, but by using the proxying directive '${proxyingMode}', this function has been declared to only be callable from the main browser thread");`; + } + + return `${async_}function(${args}) { +${prefix} ${body} }\n`; - }); - } else if (WASM_WORKERS && ASSERTIONS) { - // In ASSERTIONS builds add runtime checks that proxied functions are not attempted to be called in Wasm Workers - // (since there is no automatic proxying architecture available) - snippet = modifyJSFunction( - snippet, - (args, body) => ` -function(${args}) { - assert(!ENVIRONMENT_IS_WASM_WORKER, "Attempted to call proxied function '${mangled}' in a Wasm Worker, but in Wasm Worker enabled builds, proxied function architecture is not available!"); - ${body} -}\n`, - ); - } + }); proxiedFunctionTable.push(mangled); } } diff --git a/src/lib/libwasi.js b/src/lib/libwasi.js index 2d3b143f0969a..8b0b182fb9181 100644 --- a/src/lib/libwasi.js +++ b/src/lib/libwasi.js @@ -262,6 +262,10 @@ var WasiLibrary = { if (printCharBuffers[1].length) printChar(1, {{{ charCode("\n") }}}); if (printCharBuffers[2].length) printChar(2, {{{ charCode("\n") }}}); }, + // When we don't have a filesystem enabled, we do not need to proxy fd_write + // to the main thread, because all we do is log out() and err() calls. + // This enables printf() to work from Wasm Workers. + fd_write__proxy: 'none', fd_write__deps: ['$flush_NO_FILESYSTEM', '$printChar'], fd_write__postset: () => addAtExit('flush_NO_FILESYSTEM()'), #else diff --git a/src/parseTools.mjs b/src/parseTools.mjs index 4aaa67d2e7dbf..460703e0c8b9d 100644 --- a/src/parseTools.mjs +++ b/src/parseTools.mjs @@ -1091,7 +1091,7 @@ function ENVIRONMENT_IS_MAIN_THREAD() { return `(!${ENVIRONMENT_IS_WORKER_THREAD()})`; } -function ENVIRONMENT_IS_WORKER_THREAD() { +export function ENVIRONMENT_IS_WORKER_THREAD() { assert(PTHREADS || WASM_WORKERS); var envs = []; if (PTHREADS) envs.push('ENVIRONMENT_IS_PTHREAD'); diff --git a/test/pthread/proxy_abort.c b/test/pthread/proxy_abort.c new file mode 100644 index 0000000000000..7f86949535315 --- /dev/null +++ b/test/pthread/proxy_abort.c @@ -0,0 +1,43 @@ +#include +#include +#include + +// Tests behavior of __proxy: 'abort' and __proxy: 'abort_debug' in pthreads. + +void proxied_js_function(void); + +int might_throw(void(*func)()) { + int threw = EM_ASM_INT({ + // Patch over abort() so that we can gracefully finish the test without + // an unhandled exception hanging the test. + abort = function(text) { + throw text; + }; + + try { + getWasmTableEntry($0)(); + } catch(e) { + console.error('Threw an exception: ' + e); + return e.toString().includes('this function has been declared to only be callable from the main browser thread'); + } + console.error('Function did not throw'); + return 0; + }, func); + return threw; +} + +void *thread_main(void *arg) { + REPORT_RESULT(might_throw(proxied_js_function)); + return 0; +} + +char stack[1024]; + +int main() { + proxied_js_function(); // Should be callable from main thread + + pthread_t thread; + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_create(&thread, &attr, thread_main, 0); +} diff --git a/test/test_browser.py b/test/test_browser.py index 67bd81289a135..1def172ef1e8d 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5272,6 +5272,20 @@ def test_wasm_worker_no_proxied_js_functions(self): self.btest('wasm_worker/no_proxied_js_functions.c', expected='0', cflags=['--js-library', test_file('wasm_worker/no_proxied_js_functions.js')]) + # Tests that the proxying directives foo__proxy: 'abort' and foo__proxy: 'abort_debug' work. + @also_with_minimal_runtime + @parameterized({ + '': ('1', ['--js-library', test_file('wasm_worker/proxy_abort.js')]), + 'no_assertions': ('1', ['--js-library', test_file('wasm_worker/proxy_abort.js'), '-sASSERTIONS=0']), + 'debug': ('1', ['--js-library', test_file('wasm_worker/proxy_abort_debug.js'), '-sASSERTIONS']), + 'debug_no_assertions': ('0', ['--js-library', test_file('wasm_worker/proxy_abort_debug.js'), '-sASSERTIONS=0']), + }) + def test_proxy_abort(self, expect_result, args): + self.btest('pthread/proxy_abort.c', expected=expect_result, cflags=args + ['-pthread', '-sPTHREAD_POOL_SIZE=1']) + + self.set_setting('WASM_WORKERS') + self.btest('wasm_worker/proxy_abort.c', expected=expect_result, cflags=args) + # Tests emscripten_semaphore_init(), emscripten_semaphore_waitinf_acquire() and emscripten_semaphore_release() @also_with_minimal_runtime def test_wasm_worker_semaphore_waitinf_acquire(self): diff --git a/test/wasm_worker/proxy_abort.c b/test/wasm_worker/proxy_abort.c new file mode 100644 index 0000000000000..49e4db7f2ac2b --- /dev/null +++ b/test/wasm_worker/proxy_abort.c @@ -0,0 +1,38 @@ +#include +#include +#include + +// Tests behavior of __proxy: 'abort' and __proxy: 'abort_debug' in Wasm Workers. + +void proxied_js_function(void); + +int might_throw(void(*func)()) { + int threw = EM_ASM_INT({ + // Patch over abort() so that we can gracefully finish the test without + // an unhandled exception hanging the test. + abort = function(text) { + throw text; + }; + + try { + getWasmTableEntry($0)(); + } catch(e) { + console.error('Threw an exception: ' + e); + return e.toString().includes('this function has been declared to only be callable from the main browser thread'); + } + console.error('Function did not throw'); + return 0; + }, func); + return threw; +} + +void worker_main() { + REPORT_RESULT(might_throw(proxied_js_function)); +} + +char stack[1024]; + +int main() { + proxied_js_function(); // Should be callable from main thread + emscripten_wasm_worker_post_function_v(emscripten_malloc_wasm_worker(1024), worker_main); +} diff --git a/test/wasm_worker/proxy_abort.js b/test/wasm_worker/proxy_abort.js new file mode 100644 index 0000000000000..27e816e90b991 --- /dev/null +++ b/test/wasm_worker/proxy_abort.js @@ -0,0 +1,6 @@ +addToLibrary({ + proxied_js_function__proxy: 'abort', + proxied_js_function: function() { + console.log('In proxied_js_function'); + } +}); diff --git a/test/wasm_worker/proxy_abort_debug.js b/test/wasm_worker/proxy_abort_debug.js new file mode 100644 index 0000000000000..59e4172c9a498 --- /dev/null +++ b/test/wasm_worker/proxy_abort_debug.js @@ -0,0 +1,6 @@ +addToLibrary({ + proxied_js_function__proxy: 'abort_debug', + proxied_js_function: function() { + console.log('In proxied_js_function'); + } +});