diff --git a/src/library_atomic.js b/src/library_atomic.js new file mode 100644 index 0000000000000..7b9b0ac239dda --- /dev/null +++ b/src/library_atomic.js @@ -0,0 +1,151 @@ +/** + * @license + * Copyright 2023 The Emscripten Authors + * SPDX-License-Identifier: MIT + */ + +assert(SHARED_MEMORY); + +addToLibrary({ +// Chrome 87 (and hence Edge 87) shipped Atomics.waitAsync: +// https://www.chromestatus.com/feature/6243382101803008 +// However its implementation is faulty: +// https://bugs.chromium.org/p/chromium/issues/detail?id=1167541 +// Firefox Nightly 86.0a1 (2021-01-15) does not yet have it: +// https://bugzilla.mozilla.org/show_bug.cgi?id=1467846 +// And at the time of writing, no other browser has it either. +#if MIN_EDGE_VERSION < 91 || MIN_CHROME_VERSION < 91 || MIN_SAFARI_VERSION != TARGET_NOT_SUPPORTED || MIN_FIREFOX_VERSION != TARGET_NOT_SUPPORTED || ENVIRONMENT_MAY_BE_NODE + // Partially polyfill Atomics.waitAsync() if not available in the browser. + // Also polyfill for old Chrome-based browsers, where Atomics.waitAsync is + // broken until Chrome 91, see: + // https://bugs.chromium.org/p/chromium/issues/detail?id=1167541 + // https://github.com/tc39/proposal-atomics-wait-async/blob/master/PROPOSAL.md + // This polyfill performs polling with setTimeout() to observe a change in the + // target memory location. + $polyfillWaitAsync__postset: `if (!Atomics.waitAsync || (typeof navigator !== 'undefined' && navigator.userAgent && jstoi_q((navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./)||[])[2]) < 91)) { + let __Atomics_waitAsyncAddresses = [/*[i32a, index, value, maxWaitMilliseconds, promiseResolve]*/]; + function __Atomics_pollWaitAsyncAddresses() { + let now = performance.now(); + let l = __Atomics_waitAsyncAddresses.length; + for (let i = 0; i < l; ++i) { + let a = __Atomics_waitAsyncAddresses[i]; + let expired = (now > a[3]); + let awoken = (Atomics.load(a[0], a[1]) != a[2]); + if (expired || awoken) { + __Atomics_waitAsyncAddresses[i--] = __Atomics_waitAsyncAddresses[--l]; + __Atomics_waitAsyncAddresses.length = l; + a[4](awoken ? 'ok': 'timed-out'); + } + } + if (l) { + // If we still have addresses to wait, loop the timeout handler to continue polling. + setTimeout(__Atomics_pollWaitAsyncAddresses, 10); + } + } + #if ASSERTIONS && WASM_WORKERS + if (!ENVIRONMENT_IS_WASM_WORKER) err('Current environment does not support Atomics.waitAsync(): polyfilling it, but this is going to be suboptimal.'); + #endif + /** + * @param {number=} maxWaitMilliseconds + */ + Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => { + let val = Atomics.load(i32a, index); + if (val != value) return { async: false, value: 'not-equal' }; + if (maxWaitMilliseconds <= 0) return { async: false, value: 'timed-out' }; + maxWaitMilliseconds = performance.now() + (maxWaitMilliseconds || Infinity); + let promiseResolve; + let promise = new Promise((resolve) => { promiseResolve = resolve; }); + if (!__Atomics_waitAsyncAddresses[0]) setTimeout(__Atomics_pollWaitAsyncAddresses, 10); + __Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, promiseResolve]); + return { async: true, value: promise }; + }; +}`, + $polyfillWaitAsync__deps: ['$jstoi_q'], +#endif + + $polyfillWaitAsync__internal: true, + $polyfillWaitAsync: () => { + // nop, used for its postset to ensure `Atomics.waitAsync()` polyfill is + // included exactly once and only included when needed. + // Any function using Atomics.waitAsync should depend on this. + }, + + $atomicWaitStates__internal: true, + $atomicWaitStates: ['ok', 'not-equal', 'timed-out'], + $liveAtomicWaitAsyncs: {}, + $liveAtomicWaitAsyncs__internal: true, + $liveAtomicWaitAsyncCounter: 0, + $liveAtomicWaitAsyncCounter__internal: true, + + emscripten_atomic_wait_async__deps: ['$atomicWaitStates', '$liveAtomicWaitAsyncs', '$liveAtomicWaitAsyncCounter', '$polyfillWaitAsync', '$callUserCallback'], + emscripten_atomic_wait_async: (addr, val, asyncWaitFinished, userData, maxWaitMilliseconds) => { + let wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds); + if (!wait.async) return atomicWaitStates.indexOf(wait.value); + // Increment waitAsync generation counter, account for wraparound in case + // application does huge amounts of waitAsyncs per second (not sure if + // possible?) + // Valid counterrange: 0...2^31-1 + let counter = liveAtomicWaitAsyncCounter; + liveAtomicWaitAsyncCounter = Math.max(0, (liveAtomicWaitAsyncCounter+1)|0); + liveAtomicWaitAsyncs[counter] = addr; + {{{ runtimeKeepalivePush() }}} + wait.value.then((value) => { + if (liveAtomicWaitAsyncs[counter]) { + {{{ runtimeKeepalivePop() }}} + delete liveAtomicWaitAsyncs[counter]; + callUserCallback(() => {{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(addr, val, atomicWaitStates.indexOf(value), userData)); + } + }); + return -counter; + }, + + emscripten_atomic_cancel_wait_async__deps: ['$liveAtomicWaitAsyncs'], + emscripten_atomic_cancel_wait_async: (waitToken) => { +#if ASSERTIONS + if (waitToken == {{{ cDefs.ATOMICS_WAIT_NOT_EQUAL }}}) { + warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_NOT_EQUAL (1) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()'); + } else if (waitToken == {{{ cDefs.ATOMICS_WAIT_TIMED_OUT }}}) { + warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_TIMED_OUT (2) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()'); + } else if (waitToken > 0) { + warnOnce(`Attempted to call emscripten_atomic_cancel_wait_async() with an invalid wait token value ${waitToken}`); + } +#endif + var address = liveAtomicWaitAsyncs[waitToken]; + if (address) { + // Notify the waitAsync waiters on the memory location, so that JavaScript + // garbage collection can occur. + // See https://github.com/WebAssembly/threads/issues/176 + // This has the unfortunate effect of causing spurious wakeup of all other + // waiters at the address (which causes a small performance loss). + Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}}); + delete liveAtomicWaitAsyncs[waitToken]; + {{{ runtimeKeepalivePop() }}} + return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}}; + } + // This waitToken does not exist. + return {{{ cDefs.EMSCRIPTEN_RESULT_INVALID_PARAM }}}; + }, + + emscripten_atomic_cancel_all_wait_asyncs__deps: ['$liveAtomicWaitAsyncs'], + emscripten_atomic_cancel_all_wait_asyncs: () => { + let waitAsyncs = Object.values(liveAtomicWaitAsyncs); + waitAsyncs.forEach((address) => { + Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}}); + }); + liveAtomicWaitAsyncs = {}; + return waitAsyncs.length; + }, + + emscripten_atomic_cancel_all_wait_asyncs_at_address__deps: ['$liveAtomicWaitAsyncs'], + emscripten_atomic_cancel_all_wait_asyncs_at_address: (address) => { + let numCancelled = 0; + Object.keys(liveAtomicWaitAsyncs).forEach((waitToken) => { + if (liveAtomicWaitAsyncs[waitToken] == address) { + Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}}); + delete liveAtomicWaitAsyncs[waitToken]; + numCancelled++; + } + }); + return numCancelled; + }, +}); diff --git a/src/library_wasm_worker.js b/src/library_wasm_worker.js index 266ccf6ab05b3..110c05cf3eb9f 100644 --- a/src/library_wasm_worker.js +++ b/src/library_wasm_worker.js @@ -1,3 +1,9 @@ +/** + * @license + * Copyright 2023 The Emscripten Authors + * SPDX-License-Identifier: MIT + */ + {{{ global.captureModuleArg = () => MODULARIZE ? '' : 'self.Module=d;'; global.instantiateModule = () => MODULARIZE ? `${EXPORT_NAME}(d);` : ''; @@ -241,137 +247,6 @@ if (ENVIRONMENT_IS_WASM_WORKER) { _wasmWorkers[id].postMessage({'_wsc': funcPtr, 'x': readEmAsmArgs(sigPtr, varargs) }); }, - $atomicWaitStates: "['ok', 'not-equal', 'timed-out']", - -// Chrome 87 (and hence Edge 87) shipped Atomics.waitAsync (https://www.chromestatus.com/feature/6243382101803008) -// However its implementation is faulty: https://bugs.chromium.org/p/chromium/issues/detail?id=1167541 -// Firefox Nightly 86.0a1 (2021-01-15) does not yet have it, https://bugzilla.mozilla.org/show_bug.cgi?id=1467846 -// And at the time of writing, no other browser has it either. -#if MIN_EDGE_VERSION < 91 || MIN_CHROME_VERSION < 91 || MIN_SAFARI_VERSION != TARGET_NOT_SUPPORTED || MIN_FIREFOX_VERSION != TARGET_NOT_SUPPORTED || ENVIRONMENT_MAY_BE_NODE - // Partially polyfill Atomics.waitAsync() if not available in the browser. - // Also polyfill for old Chrome-based browsers, where Atomics.waitAsync is - // broken until Chrome 91, see - // https://bugs.chromium.org/p/chromium/issues/detail?id=1167541 - // https://github.com/tc39/proposal-atomics-wait-async/blob/master/PROPOSAL.md - // This polyfill performs polling with setTimeout() to observe a change in the - // target memory location. - $polyfillWaitAsync__deps: ['$jstoi_q'], - $polyfillWaitAsync__postset: `if (!Atomics.waitAsync || (typeof navigator !== 'undefined' && navigator.userAgent && jstoi_q((navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./)||[])[2]) < 91)) { -let __Atomics_waitAsyncAddresses = [/*[i32a, index, value, maxWaitMilliseconds, promiseResolve]*/]; -function __Atomics_pollWaitAsyncAddresses() { - let now = performance.now(); - let l = __Atomics_waitAsyncAddresses.length; - for (let i = 0; i < l; ++i) { - let a = __Atomics_waitAsyncAddresses[i]; - let expired = (now > a[3]); - let awoken = (Atomics.load(a[0], a[1]) != a[2]); - if (expired || awoken) { - __Atomics_waitAsyncAddresses[i--] = __Atomics_waitAsyncAddresses[--l]; - __Atomics_waitAsyncAddresses.length = l; - a[4](awoken ? 'ok': 'timed-out'); - } - } - if (l) { - // If we still have addresses to wait, loop the timeout handler to continue polling. - setTimeout(__Atomics_pollWaitAsyncAddresses, 10); - } -} -#if ASSERTIONS - if (!ENVIRONMENT_IS_WASM_WORKER) err('Current environment does not support Atomics.waitAsync(): polyfilling it, but this is going to be suboptimal.'); -#endif -Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => { - let val = Atomics.load(i32a, index); - if (val != value) return { async: false, value: 'not-equal' }; - if (maxWaitMilliseconds <= 0) return { async: false, value: 'timed-out' }; - maxWaitMilliseconds = performance.now() + (maxWaitMilliseconds || Infinity); - let promiseResolve; - let promise = new Promise((resolve) => { promiseResolve = resolve; }); - if (!__Atomics_waitAsyncAddresses[0]) setTimeout(__Atomics_pollWaitAsyncAddresses, 10); - __Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, promiseResolve]); - return { async: true, value: promise }; -}; -}`, -#endif - - $polyfillWaitAsync__internal: true, - $polyfillWaitAsync: () => { - // nop, used for its postset to ensure `Atomics.waitAsync()` polyfill is - // included exactly once and only included when needed. - // Any function using Atomics.waitAsync should depend on this. - }, - - $liveAtomicWaitAsyncs: {}, - $liveAtomicWaitAsyncCounter: 0, - - emscripten_atomic_wait_async__deps: ['$atomicWaitStates', '$liveAtomicWaitAsyncs', '$liveAtomicWaitAsyncCounter', '$polyfillWaitAsync'], - emscripten_atomic_wait_async: (addr, val, asyncWaitFinished, userData, maxWaitMilliseconds) => { - let wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds); - if (!wait.async) return atomicWaitStates.indexOf(wait.value); - // Increment waitAsync generation counter, account for wraparound in case - // application does huge amounts of waitAsyncs per second (not sure if - // possible?) - // Valid counterrange: 0...2^31-1 - let counter = liveAtomicWaitAsyncCounter; - liveAtomicWaitAsyncCounter = Math.max(0, (liveAtomicWaitAsyncCounter+1)|0); - liveAtomicWaitAsyncs[counter] = addr; - wait.value.then((value) => { - if (liveAtomicWaitAsyncs[counter]) { - delete liveAtomicWaitAsyncs[counter]; - {{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(addr, val, atomicWaitStates.indexOf(value), userData); - } - }); - return -counter; - }, - - emscripten_atomic_cancel_wait_async__deps: ['$liveAtomicWaitAsyncs'], - emscripten_atomic_cancel_wait_async: (waitToken) => { -#if ASSERTIONS - if (waitToken == {{{ cDefs.ATOMICS_WAIT_NOT_EQUAL }}}) { - warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_NOT_EQUAL (1) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()'); - } else if (waitToken == {{{ cDefs.ATOMICS_WAIT_TIMED_OUT }}}) { - warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_TIMED_OUT (2) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()'); - } else if (waitToken > 0) { - warnOnce(`Attempted to call emscripten_atomic_cancel_wait_async() with an invalid wait token value ${waitToken}`); - } -#endif - var address = liveAtomicWaitAsyncs[waitToken]; - if (address) { - // Notify the waitAsync waiters on the memory location, so that JavaScript - // garbage collection can occur. - // See https://github.com/WebAssembly/threads/issues/176 - // This has the unfortunate effect of causing spurious wakeup of all other - // waiters at the address (which causes a small performance loss). - Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}}); - delete liveAtomicWaitAsyncs[waitToken]; - return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}}; - } - // This waitToken does not exist. - return {{{ cDefs.EMSCRIPTEN_RESULT_INVALID_PARAM }}}; - }, - - emscripten_atomic_cancel_all_wait_asyncs__deps: ['$liveAtomicWaitAsyncs'], - emscripten_atomic_cancel_all_wait_asyncs: () => { - let waitAsyncs = Object.values(liveAtomicWaitAsyncs); - waitAsyncs.forEach((address) => { - Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}}); - }); - liveAtomicWaitAsyncs = {}; - return waitAsyncs.length; - }, - - emscripten_atomic_cancel_all_wait_asyncs_at_address__deps: ['$liveAtomicWaitAsyncs'], - emscripten_atomic_cancel_all_wait_asyncs_at_address: (address) => { - let numCancelled = 0; - Object.keys(liveAtomicWaitAsyncs).forEach((waitToken) => { - if (liveAtomicWaitAsyncs[waitToken] == address) { - Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}}); - delete liveAtomicWaitAsyncs[waitToken]; - numCancelled++; - } - }); - return numCancelled; - }, - emscripten_navigator_hardware_concurrency: () => { #if ENVIRONMENT_MAY_BE_NODE if (ENVIRONMENT_IS_NODE) return require('os').cpus().length; diff --git a/src/modules.js b/src/modules.js index 4102320797ad8..55ccb1764ace5 100644 --- a/src/modules.js +++ b/src/modules.js @@ -153,6 +153,10 @@ global.LibraryManager = { libraries.push('library_lz4.js'); } + if (SHARED_MEMORY) { + libraries.push('library_atomic.js'); + } + if (MAX_WEBGL_VERSION >= 2) { // library_webgl2.js must be included only after library_webgl.js, so if we are // about to include library_webgl2.js, first squeeze in library_webgl.js. diff --git a/system/include/emscripten/atomic.h b/system/include/emscripten/atomic.h index 12467b8ad2cba..7f9bc49d5d1ad 100644 --- a/system/include/emscripten/atomic.h +++ b/system/include/emscripten/atomic.h @@ -9,6 +9,8 @@ #include +#include + #ifdef __cplusplus extern "C" { #endif @@ -223,6 +225,71 @@ _EM_INLINE int64_t emscripten_atomic_notify(void *addr __attribute__((nonnull)), return __builtin_wasm_memory_atomic_notify((int*)addr, count); } +#define EMSCRIPTEN_WAIT_ASYNC_INFINITY __builtin_inf() + +// Represents a pending 'Atomics.waitAsync' wait operation. +#define ATOMICS_WAIT_TOKEN_T int32_t + +#define EMSCRIPTEN_IS_VALID_WAIT_TOKEN(token) ((token) <= 0) + +// Issues the JavaScript 'Atomics.waitAsync' instruction: +// performs an asynchronous wait operation on the main thread. If the given +// 'addr' contains 'value', issues a deferred wait that will invoke the +// specified callback function 'asyncWaitFinished' once that address has been +// notified by another thread. +// NOTE: Unlike functions emscripten_atomic_wait_u32() and +// emscripten_atomic_wait_u64() which take in the wait timeout parameter as int64 +// nanosecond units, this function takes in the wait timeout parameter as double +// millisecond units. See https://github.com/WebAssembly/threads/issues/175 for +// more information. +// Pass in maxWaitMilliseconds == EMSCRIPTEN_WAIT_ASYNC_INFINITY +// (==__builtin_inf()) to wait infinitely long. +// Returns one of: +// - ATOMICS_WAIT_NOT_EQUAL if the waitAsync operation could not be registered +// since the memory value did not contain the value 'value'. +// - ATOMICS_WAIT_TIMED_OUT if the waitAsync operation timeout parameter was <= 0. +// - Any other value: denotes a 'wait token' that can be passed to function +// emscripten_atomic_cancel_wait_async() to unregister an asynchronous wait. +// You can use the macro EMSCRIPTEN_IS_VALID_WAIT_TOKEN(retval) to check if +// this function returned a valid wait token. +ATOMICS_WAIT_TOKEN_T emscripten_atomic_wait_async(void *addr __attribute__((nonnull)), + uint32_t value, + void (*asyncWaitFinished)(int32_t *addr, uint32_t value, ATOMICS_WAIT_RESULT_T waitResult, void *userData) __attribute__((nonnull)), + void *userData, + double maxWaitMilliseconds); + +// Unregisters a pending Atomics.waitAsync operation that was established via a +// call to emscripten_atomic_wait_async() in the calling thread. Pass in the +// wait token handle that was received as the return value from the wait +// function. Returns EMSCRIPTEN_RESULT_SUCCESS if the cancellation was +// successful, or EMSCRIPTEN_RESULT_INVALID_PARAM if the asynchronous wait has +// already resolved prior and the callback has already been called. +// NOTE: Because of needing to work around issue +// https://github.com/WebAssembly/threads/issues/176, calling this function has +// an effect of introducing spurious wakeups to any other threads waiting on the +// same address that the async wait denoted by the token does. This means that +// in order to safely use this function, the mechanisms used in any wait code on +// that address must be written to be spurious wakeup safe. (this is the case +// for all the synchronization primitives declared in this header, but if you +// are rolling out your own, you need to be aware of this). If +// https://github.com/tc39/proposal-cancellation/issues/29 is resolved, then the +// spurious wakeups can be avoided. +EMSCRIPTEN_RESULT emscripten_atomic_cancel_wait_async(ATOMICS_WAIT_TOKEN_T waitToken); + +// Cancels all pending async waits in the calling thread. Because of +// https://github.com/WebAssembly/threads/issues/176, if you are using +// asynchronous waits in your application, and need to be able to let GC reclaim +// Wasm heap memory when deinitializing an application, you *must* call this +// function to help the GC unpin all necessary memory. Otherwise, you can wrap +// the Wasm content in an iframe and unload the iframe to let GC occur. +// (navigating away from the page or closing that tab will also naturally +// reclaim the memory) +int emscripten_atomic_cancel_all_wait_asyncs(void); + +// Cancels all pending async waits in the calling thread to the given memory +// address. Returns the number of async waits canceled. +int emscripten_atomic_cancel_all_wait_asyncs_at_address(void *addr __attribute__((nonnull))); + #undef _EM_INLINE #ifdef __cplusplus diff --git a/system/include/emscripten/em_types.h b/system/include/emscripten/em_types.h index c4dc53b06c433..7d738beeba70a 100644 --- a/system/include/emscripten/em_types.h +++ b/system/include/emscripten/em_types.h @@ -33,3 +33,16 @@ typedef void (*em_str_callback_func)(const char *); #define EM_TRUE 1 #define EM_FALSE 0 #define EM_UTF8 char + +#define EMSCRIPTEN_RESULT int + +#define EMSCRIPTEN_RESULT_SUCCESS 0 +#define EMSCRIPTEN_RESULT_DEFERRED 1 +#define EMSCRIPTEN_RESULT_NOT_SUPPORTED -1 +#define EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED -2 +#define EMSCRIPTEN_RESULT_INVALID_TARGET -3 +#define EMSCRIPTEN_RESULT_UNKNOWN_TARGET -4 +#define EMSCRIPTEN_RESULT_INVALID_PARAM -5 +#define EMSCRIPTEN_RESULT_FAILED -6 +#define EMSCRIPTEN_RESULT_NO_DATA -7 +#define EMSCRIPTEN_RESULT_TIMED_OUT -8 diff --git a/system/include/emscripten/html5.h b/system/include/emscripten/html5.h index 6438eea86273c..998db860eac87 100644 --- a/system/include/emscripten/html5.h +++ b/system/include/emscripten/html5.h @@ -70,19 +70,6 @@ extern "C" { #define EMSCRIPTEN_EVENT_CANVASRESIZED 37 #define EMSCRIPTEN_EVENT_POINTERLOCKERROR 38 -#define EMSCRIPTEN_RESULT int - -#define EMSCRIPTEN_RESULT_SUCCESS 0 -#define EMSCRIPTEN_RESULT_DEFERRED 1 -#define EMSCRIPTEN_RESULT_NOT_SUPPORTED -1 -#define EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED -2 -#define EMSCRIPTEN_RESULT_INVALID_TARGET -3 -#define EMSCRIPTEN_RESULT_UNKNOWN_TARGET -4 -#define EMSCRIPTEN_RESULT_INVALID_PARAM -5 -#define EMSCRIPTEN_RESULT_FAILED -6 -#define EMSCRIPTEN_RESULT_NO_DATA -7 -#define EMSCRIPTEN_RESULT_TIMED_OUT -8 - #define EMSCRIPTEN_EVENT_TARGET_INVALID 0 #define EMSCRIPTEN_EVENT_TARGET_DOCUMENT ((const char*)1) #define EMSCRIPTEN_EVENT_TARGET_WINDOW ((const char*)2) diff --git a/system/include/emscripten/wasm_worker.h b/system/include/emscripten/wasm_worker.h index 74ef1fe931517..cb3fee2910c6b 100644 --- a/system/include/emscripten/wasm_worker.h +++ b/system/include/emscripten/wasm_worker.h @@ -1,8 +1,9 @@ #pragma once +#include #include -#include #include +#include #ifdef __cplusplus extern "C" { @@ -89,71 +90,6 @@ void emscripten_wasm_worker_post_function_vdd(emscripten_wasm_worker_t id, void void emscripten_wasm_worker_post_function_vddd(emscripten_wasm_worker_t id, void (*funcPtr)(double, double, double) __attribute__((nonnull)), double arg0, double arg1, double arg2); void emscripten_wasm_worker_post_function_sig(emscripten_wasm_worker_t id, void *funcPtr __attribute__((nonnull)), const char *sig __attribute__((nonnull)), ...); -#define EMSCRIPTEN_WAIT_ASYNC_INFINITY __builtin_inf() - -// Represents a pending 'Atomics.waitAsync' wait operation. -#define ATOMICS_WAIT_TOKEN_T int32_t - -#define EMSCRIPTEN_IS_VALID_WAIT_TOKEN(token) ((token) <= 0) - -// Issues the JavaScript 'Atomics.waitAsync' instruction: -// performs an asynchronous wait operation on the main thread. If the given -// 'address' contains 'value', issues a deferred wait that will invoke the -// specified callback function 'asyncWaitFinished' once that address has been -// notified by another thread. -// NOTE: Unlike functions emscripten_atomic_wait_u32() and -// emscripten_atomic_wait_u64() which take in the wait timeout parameter as int64 -// nanosecond units, this function takes in the wait timeout parameter as double -// millisecond units. See https://github.com/WebAssembly/threads/issues/175 for -// more information. -// Pass in maxWaitMilliseconds == EMSCRIPTEN_WAIT_ASYNC_INFINITY -// (==__builtin_inf()) to wait infinitely long. -// Returns one of: -// - ATOMICS_WAIT_NOT_EQUAL if the waitAsync operation could not be registered -// since the memory value did not contain the value 'value'. -// - ATOMICS_WAIT_TIMED_OUT if the waitAsync operation timeout parameter was <= 0. -// - Any other value: denotes a 'wait token' that can be passed to function -// emscripten_atomic_cancel_wait_async() to unregister an asynchronous wait. -// You can use the macro EMSCRIPTEN_IS_VALID_WAIT_TOKEN(retval) to check if -// this function returned a valid wait token. -ATOMICS_WAIT_TOKEN_T emscripten_atomic_wait_async(int32_t *address __attribute__((nonnull)), - uint32_t value, - void (*asyncWaitFinished)(int32_t *address, uint32_t value, ATOMICS_WAIT_RESULT_T waitResult, void *userData) __attribute__((nonnull)), - void *userData, - double maxWaitMilliseconds); - -// Unregisters a pending Atomics.waitAsync operation that was established via a -// call to emscripten_atomic_wait_async() in the calling thread. Pass in the -// wait token handle that was received as the return value from the wait -// function. Returns EMSCRIPTEN_RESULT_SUCCESS if the cancellation was -// successful, or EMSCRIPTEN_RESULT_INVALID_PARAM if the asynchronous wait has -// already resolved prior and the callback has already been called. -// NOTE: Because of needing to work around issue -// https://github.com/WebAssembly/threads/issues/176, calling this function has -// an effect of introducing spurious wakeups to any other threads waiting on the -// same address that the async wait denoted by the token does. This means that -// in order to safely use this function, the mechanisms used in any wait code on -// that address must be written to be spurious wakeup safe. (this is the case -// for all the synchronization primitives declared in this header, but if you -// are rolling out your own, you need to be aware of this). If -// https://github.com/tc39/proposal-cancellation/issues/29 is resolved, then the -// spurious wakeups can be avoided. -EMSCRIPTEN_RESULT emscripten_atomic_cancel_wait_async(ATOMICS_WAIT_TOKEN_T waitToken); - -// Cancels all pending async waits in the calling thread. Because of -// https://github.com/WebAssembly/threads/issues/176, if you are using -// asynchronous waits in your application, and need to be able to let GC reclaim -// Wasm heap memory when deinitializing an application, you *must* call this -// function to help the GC unpin all necessary memory. Otherwise, you can wrap -// the Wasm content in an iframe and unload the iframe to let GC occur. -// (navigating away from the page or closing that tab will also naturally -// reclaim the memory) -int emscripten_atomic_cancel_all_wait_asyncs(void); - -// Cancels all pending async waits in the calling thread to the given memory -// address. Returns the number of async waits canceled. -int emscripten_atomic_cancel_all_wait_asyncs_at_address(int32_t *address __attribute__((nonnull))); - // Sleeps the calling wasm worker for the given nanoseconds. Calling this // function on the main thread either results in a TypeError exception // (Firefox), or a silent return without waiting (Chrome), see diff --git a/test/wasm_worker/wait32_notify.c b/test/atomic/test_wait32_notify.c similarity index 100% rename from test/wasm_worker/wait32_notify.c rename to test/atomic/test_wait32_notify.c diff --git a/test/wasm_worker/wait32_notify.out b/test/atomic/test_wait32_notify.out similarity index 100% rename from test/wasm_worker/wait32_notify.out rename to test/atomic/test_wait32_notify.out diff --git a/test/wasm_worker/wait64_notify.c b/test/atomic/test_wait64_notify.c similarity index 97% rename from test/wasm_worker/wait64_notify.c rename to test/atomic/test_wait64_notify.c index df9bb12e106e6..d6b93cfc5aa1b 100644 --- a/test/wasm_worker/wait64_notify.c +++ b/test/atomic/test_wait64_notify.c @@ -76,6 +76,10 @@ EM_BOOL main_loop(double time, void *userData) { emscripten_out("main: waking worker"); emscripten_atomic_notify((int32_t*)&addr, 1); +#ifndef __EMSCRIPTEN_WASM_WORKERS__ + pthread_join(t, NULL); +#endif + return EM_FALSE; } return EM_TRUE; diff --git a/test/wasm_worker/wait64_notify.out b/test/atomic/test_wait64_notify.out similarity index 100% rename from test/wasm_worker/wait64_notify.out rename to test/atomic/test_wait64_notify.out diff --git a/test/atomic/test_wait_async.c b/test/atomic/test_wait_async.c new file mode 100644 index 0000000000000..0d9555a90c15f --- /dev/null +++ b/test/atomic/test_wait_async.c @@ -0,0 +1,115 @@ +#include +#include +#include +#include + +// Test emscripten_atomic_wait_u64() and emscripten_atomic_notify() functions. + +volatile int32_t addr = 0; + +void run_test() { + emscripten_out("worker_main"); +#if __EMSCRIPTEN_WASM_WORKERS__ + emscripten_wasm_worker_sleep(1000 * 1000000ull); // Sleep one second. +#else + emscripten_thread_sleep(1000); // Sleep one second. +#endif + emscripten_out("worker: addr = 1234"); + emscripten_atomic_store_u32((void*)&addr, 1234); + emscripten_out("worker: notify async waiting main thread"); + emscripten_atomic_notify((int32_t*)&addr, 1); +} + + +int numCalled = 0; + +void asyncWaitShouldTimeout(int32_t* ptr, + uint32_t val, + ATOMICS_WAIT_RESULT_T waitResult, + void* userData) { + emscripten_out("main: asyncWaitShouldTimeout"); + ++numCalled; + assert(numCalled == 1); + assert(ptr == &addr); + assert(val == 1); + assert(userData == (void*)42); + assert(waitResult == ATOMICS_WAIT_TIMED_OUT); +} + +void asyncWaitFinishedShouldNotBeCalled(int32_t* ptr, + uint32_t val, + ATOMICS_WAIT_RESULT_T waitResult, + void* userData) { + assert(0); // We should not reach here +} + +// This test run in both wasm workers and pthreads mode +#ifdef __EMSCRIPTEN_WASM_WORKERS__ + +void worker_main() { + run_test(); +} + +#else + +pthread_t t; + +void* thread_main(void* arg) { + run_test(); + return 0; +} + +#endif + +void asyncWaitFinishedShouldBeOk(int32_t* ptr, + uint32_t val, + ATOMICS_WAIT_RESULT_T waitResult, + void* userData) { + emscripten_out("main: asyncWaitFinished"); + assert(ptr == &addr); + assert(val == 1); + assert(userData == (void*)42); + ++numCalled; + assert(numCalled == 2); + assert(waitResult == ATOMICS_WAIT_OK); + emscripten_out("test finished"); +#ifdef REPORT_RESULT + REPORT_RESULT(0); +#endif +#if !defined(__EMSCRIPTEN_WASM_WORKERS__) + pthread_join(t, NULL); +#endif +} + +int main() { + emscripten_out("main: creating worker"); + +#ifdef __EMSCRIPTEN_WASM_WORKERS__ + emscripten_wasm_worker_t worker = emscripten_malloc_wasm_worker(1024); + emscripten_wasm_worker_post_function_v(worker, worker_main); +#else + pthread_create(&t, NULL, thread_main, NULL); +#endif + + addr = 1; + + emscripten_out("Async waiting on address with unexpected value should return 'not-equal'"); + ATOMICS_WAIT_TOKEN_T ret = emscripten_atomic_wait_async((int32_t*)&addr, 2, asyncWaitFinishedShouldNotBeCalled, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + assert(ret == ATOMICS_WAIT_NOT_EQUAL); + + emscripten_out("Waiting on address with unexpected value should return 'not-equal' also if timeout==0"); + ret = emscripten_atomic_wait_async((int32_t*)&addr, 2, asyncWaitFinishedShouldNotBeCalled, (void*)42, 0); + assert(ret == ATOMICS_WAIT_NOT_EQUAL); + + emscripten_out("Waiting for 0 milliseconds should return 'timed-out'"); + ret = emscripten_atomic_wait_async((int32_t*)&addr, 1, asyncWaitFinishedShouldNotBeCalled, (void*)42, 0); + assert(ret == ATOMICS_WAIT_TIMED_OUT); + + emscripten_out("Waiting for >0 milliseconds should return 'ok' (but successfully time out in first call to asyncWaitFinished)"); + ret = emscripten_atomic_wait_async((int32_t*)&addr, 1, asyncWaitShouldTimeout, (void*)42, 10); + assert(EMSCRIPTEN_IS_VALID_WAIT_TOKEN(ret)); + + emscripten_out("Waiting for infinitely long should return 'ok' (and return 'ok' in second call to asyncWaitFinished)"); + ret = emscripten_atomic_wait_async((int32_t*)&addr, 1, asyncWaitFinishedShouldBeOk, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + assert(EMSCRIPTEN_IS_VALID_WAIT_TOKEN(ret)); +} diff --git a/test/atomic/test_wait_async.out b/test/atomic/test_wait_async.out new file mode 100644 index 0000000000000..2e93c548a95f3 --- /dev/null +++ b/test/atomic/test_wait_async.out @@ -0,0 +1,12 @@ +main: creating worker +Async waiting on address with unexpected value should return 'not-equal' +Waiting on address with unexpected value should return 'not-equal' also if timeout==0 +Waiting for 0 milliseconds should return 'timed-out' +Waiting for >0 milliseconds should return 'ok' (but successfully time out in first call to asyncWaitFinished) +Waiting for infinitely long should return 'ok' (and return 'ok' in second call to asyncWaitFinished) +main: asyncWaitShouldTimeout +worker_main +worker: addr = 1234 +worker: notify async waiting main thread +main: asyncWaitFinished +test finished diff --git a/test/test_browser.py b/test/test_browser.py index d94b4207d867d..0ce0d534f9f13 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5301,17 +5301,17 @@ def test_wasm_worker_hardware_concurrency_is_lock_free(self): # Tests emscripten_atomic_wait_u32() and emscripten_atomic_notify() functions. @also_with_minimal_runtime def test_wasm_worker_wait32_notify(self): - self.btest('wasm_worker/wait32_notify.c', expected='3', args=['-sWASM_WORKERS']) + self.btest('atomic/test_wait32_notify.c', expected='3', args=['-sWASM_WORKERS']) # Tests emscripten_atomic_wait_u64() and emscripten_atomic_notify() functions. @also_with_minimal_runtime def test_wasm_worker_wait64_notify(self): - self.btest('wasm_worker/wait64_notify.c', expected='3', args=['-sWASM_WORKERS']) + self.btest('atomic/test_wait64_notify.c', expected='3', args=['-sWASM_WORKERS']) # Tests emscripten_atomic_wait_async() function. @also_with_minimal_runtime def test_wasm_worker_wait_async(self): - self.btest('wasm_worker/wait_async.c', expected='0', args=['-sWASM_WORKERS']) + self.btest('atomic/test_wait_async.c', expected='0', args=['-sWASM_WORKERS']) # Tests emscripten_atomic_cancel_wait_async() function. @also_with_minimal_runtime diff --git a/test/test_core.py b/test/test_core.py index 0535d813d73c2..921e92d28a50a 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -2936,13 +2936,18 @@ def test_pthread_run_script(self): @node_pthreads def test_pthread_wait32_notify(self): self.set_setting('EXIT_RUNTIME') - self.do_run_in_out_file_test(test_file('wasm_worker/wait32_notify.c')) + self.do_run_in_out_file_test(test_file('atomic/test_wait32_notify.c')) @node_pthreads @no_wasm2js('https://github.com/WebAssembly/binaryen/issues/5991') def test_pthread_wait64_notify(self): self.set_setting('EXIT_RUNTIME') - self.do_run_in_out_file_test(test_file('wasm_worker/wait64_notify.c')) + self.do_run_in_out_file_test(test_file('atomic/test_wait64_notify.c')) + + @node_pthreads + def test_pthread_wait_async(self): + self.set_setting('PROXY_TO_PTHREAD') + self.do_run_in_out_file_test(test_file('atomic/test_wait_async.c')) def test_tcgetattr(self): self.do_runf(test_file('termios/test_tcgetattr.c'), 'success') @@ -9941,7 +9946,7 @@ def test_wasm_worker_malloc(self): @node_pthreads def test_wasm_worker_wait_async(self): self.prep_wasm_worker_in_node() - self.do_runf(test_file('wasm_worker/wait_async.c'), emcc_args=['-sWASM_WORKERS']) + self.do_runf(test_file('atomic/test_wait_async.c'), emcc_args=['-sWASM_WORKERS']) # Generate tests for everything diff --git a/test/wasm_worker/hello_wasm_worker.c b/test/wasm_worker/hello_wasm_worker.c index 95abab5db2cab..cae9ac83cabe1 100644 --- a/test/wasm_worker/hello_wasm_worker.c +++ b/test/wasm_worker/hello_wasm_worker.c @@ -1,4 +1,5 @@ #include +#include #include #include diff --git a/test/wasm_worker/malloc_wasm_worker.c b/test/wasm_worker/malloc_wasm_worker.c index ab7cb0450a8f5..818e88804166f 100644 --- a/test/wasm_worker/malloc_wasm_worker.c +++ b/test/wasm_worker/malloc_wasm_worker.c @@ -1,4 +1,5 @@ #include +#include #include #include @@ -6,7 +7,7 @@ void worker_main() { - emscripten_console_log("Hello from wasm worker!"); + emscripten_out("Hello from wasm worker!"); assert(emscripten_current_thread_is_wasm_worker()); #ifdef REPORT_RESULT REPORT_RESULT(0); diff --git a/test/wasm_worker/post_function.c b/test/wasm_worker/post_function.c index 41cb82849ee56..ec1a06a715937 100644 --- a/test/wasm_worker/post_function.c +++ b/test/wasm_worker/post_function.c @@ -1,4 +1,5 @@ #include +#include #include #include diff --git a/test/wasm_worker/post_function_to_main_thread.c b/test/wasm_worker/post_function_to_main_thread.c index 3ef342ad4372f..1636ef3eba9e6 100644 --- a/test/wasm_worker/post_function_to_main_thread.c +++ b/test/wasm_worker/post_function_to_main_thread.c @@ -1,4 +1,5 @@ #include +#include #include #include diff --git a/test/wasm_worker/terminate_all_wasm_workers.c b/test/wasm_worker/terminate_all_wasm_workers.c index 847dda59b9c79..32f7b579ac6c0 100644 --- a/test/wasm_worker/terminate_all_wasm_workers.c +++ b/test/wasm_worker/terminate_all_wasm_workers.c @@ -1,4 +1,6 @@ #include +#include +#include #include #include @@ -10,7 +12,7 @@ static volatile int worker_started = 0; void this_function_should_not_be_called(void *userData) { worker_started = -1; - emscripten_console_error("this_function_should_not_be_called"); + emscripten_err("this_function_should_not_be_called"); #ifdef REPORT_RESULT REPORT_RESULT(1/*fail*/); #endif @@ -20,7 +22,7 @@ void test_passed(void *userData) { if (worker_started == 2) { - emscripten_console_error("test_passed"); + emscripten_err("test_passed"); #ifdef REPORT_RESULT REPORT_RESULT(0/*ok*/); #endif @@ -30,7 +32,7 @@ void test_passed(void *userData) void worker_main() { ++worker_started; - emscripten_console_error("Hello from wasm worker!"); + emscripten_err("Hello from wasm worker!"); // Schedule a function to be called, that should never happen, since the Worker // dies before that. emscripten_set_timeout(this_function_should_not_be_called, 2000, 0); diff --git a/test/wasm_worker/terminate_wasm_worker.c b/test/wasm_worker/terminate_wasm_worker.c index fa3fb0fe04abb..bfd3611f3d2c8 100644 --- a/test/wasm_worker/terminate_wasm_worker.c +++ b/test/wasm_worker/terminate_wasm_worker.c @@ -1,4 +1,6 @@ #include +#include +#include #include #include diff --git a/test/wasm_worker/wait_async.c b/test/wasm_worker/wait_async.c deleted file mode 100644 index e7360ae3c9afb..0000000000000 --- a/test/wasm_worker/wait_async.c +++ /dev/null @@ -1,82 +0,0 @@ -#include -#include -#include -#include - -// Test emscripten_atomic_wait_u64() and emscripten_atomic_notify() functions. - -volatile int32_t addr = 0; - -void worker_main() -{ - emscripten_console_log("worker_main"); - emscripten_wasm_worker_sleep(1000 * 1000000ull); // Sleep one second. - emscripten_console_log("worker: addr = 1234"); - emscripten_atomic_store_u32((void*)&addr, 1234); - emscripten_console_log("worker: notify async waiting main thread"); - emscripten_atomic_notify((int32_t*)&addr, 1); -} - - -int numCalled = 0; - -void asyncWaitShouldTimeout(int32_t *ptr, uint32_t val, ATOMICS_WAIT_RESULT_T waitResult, void *userData) -{ - emscripten_console_log("main: asyncWaitShouldTimeout"); - ++numCalled; - assert(numCalled == 1); - assert(ptr == &addr); - assert(val == 1); - assert(userData == (void*)42); - assert(waitResult == ATOMICS_WAIT_TIMED_OUT); -} - -void asyncWaitFinishedShouldNotBeCalled(int32_t *ptr, uint32_t val, ATOMICS_WAIT_RESULT_T waitResult, void *userData) -{ - assert(0); // We should not reach here -} - -void asyncWaitFinishedShouldBeOk(int32_t *ptr, uint32_t val, ATOMICS_WAIT_RESULT_T waitResult, void *userData) -{ - emscripten_console_log("main: asyncWaitFinished"); - assert(ptr == &addr); - assert(val == 1); - assert(userData == (void*)42); - ++numCalled; - assert(numCalled == 2); - assert(waitResult == ATOMICS_WAIT_OK); - emscripten_console_log("test finished"); -#ifdef REPORT_RESULT - REPORT_RESULT(0); -#endif -} - -int main() -{ - emscripten_console_log("main: creating worker"); - emscripten_wasm_worker_t worker = emscripten_malloc_wasm_worker(1024); - emscripten_console_log("main: posting function"); - emscripten_wasm_worker_post_function_v(worker, worker_main); - - addr = 1; - - emscripten_console_log("Async waiting on address with unexpected value should return 'not-equal'"); - ATOMICS_WAIT_TOKEN_T ret = emscripten_atomic_wait_async((int32_t*)&addr, 2, asyncWaitFinishedShouldNotBeCalled, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); - assert(ret == ATOMICS_WAIT_NOT_EQUAL); - - emscripten_console_log("Waiting on address with unexpected value should return 'not-equal' also if timeout==0"); - ret = emscripten_atomic_wait_async((int32_t*)&addr, 2, asyncWaitFinishedShouldNotBeCalled, (void*)42, 0); - assert(ret == ATOMICS_WAIT_NOT_EQUAL); - - emscripten_console_log("Waiting for 0 milliseconds should return 'timed-out'"); - ret = emscripten_atomic_wait_async((int32_t*)&addr, 1, asyncWaitFinishedShouldNotBeCalled, (void*)42, 0); - assert(ret == ATOMICS_WAIT_TIMED_OUT); - - emscripten_console_log("Waiting for >0 milliseconds should return 'ok' (but successfully time out in first call to asyncWaitFinished)"); - ret = emscripten_atomic_wait_async((int32_t*)&addr, 1, asyncWaitShouldTimeout, (void*)42, 10); - assert(EMSCRIPTEN_IS_VALID_WAIT_TOKEN(ret)); - - emscripten_console_log("Waiting for infinitely long should return 'ok' (and return 'ok' in second call to asyncWaitFinished)"); - ret = emscripten_atomic_wait_async((int32_t*)&addr, 1, asyncWaitFinishedShouldBeOk, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); - assert(EMSCRIPTEN_IS_VALID_WAIT_TOKEN(ret)); -}