diff --git a/site/source/docs/porting/asyncify.rst b/site/source/docs/porting/asyncify.rst index 06c71d2819b32..2ff980780c997 100644 --- a/site/source/docs/porting/asyncify.rst +++ b/site/source/docs/porting/asyncify.rst @@ -224,6 +224,16 @@ path for ``ASYNCIFY`` and a pthread, while returning the ``EAGAIN`` error when called synchronously. As ``sync`` is always ``false`` under ``ASYNCIFY``/JSPI, the function must then be called from a stack that can suspend, as with any other async import (under JSPI, within one of the :ref:`JSPI_EXPORTS`). +Returning a Promise when ``sync`` is ``true`` is an error under +``ASSERTIONS``, as the result could not be waited for. + +``__async: 'proxy'`` is for a function that must block when called from a +pthread but can never be a suspending import. ``write()`` on a pipe sits +behind every ``printf``, and a suspending import there would instrument or +trap every caller. The function stays a plain import in every build, +including under ``ASYNCIFY``/JSPI, where ``sync`` is then ``true`` as well; +``sync`` is ``false`` only when called from a pthread with ``__proxy: 'sync'``, +which such a function requires. Ways to use Asyncify APIs in older engines diff --git a/src/jsifier.mjs b/src/jsifier.mjs index 6a05135ce6e5e..34e75e9d18fc6 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -466,7 +466,7 @@ function handleI64Signatures(symbol, snippet, sig, i53abi, isAsyncFunction) { // An 'auto' body may return a Promise even without ASYNCIFY (on a // pthread's behalf, which awaits it), which must not be cast but // resolve to the cast value. - const maybePromise = !isAsyncFunction && LibraryManager.library[symbol + '__async'] == 'auto'; + const maybePromise = !isAsyncFunction && ['auto', 'proxy'].includes(LibraryManager.library[symbol + '__async']); const convert = (v) => maybePromise ? `(${v} instanceof Promise ? ${v}.then((v) => ${makeReturn64('v')}) : ${makeReturn64(v)})` : makeReturn64(await_ + v); @@ -507,13 +507,14 @@ ${body}; }); } -// An `__async: 'auto'` body returns a value or a Promise, as it can. A body -// whose last parameter is named `sync` receives there whether it must complete -// synchronously on this call, as a returned Promise could not be waited for. -// That is never the case under ASYNCIFY/JSPI, nor on behalf of a sync-proxied -// pthread caller, which awaits it; otherwise it is. -function handleSyncArg(symbol, snippet, sig, proxyingMode) { - const sync = ASYNCIFY ? 'false' : +// An `__async: 'auto'` or `'proxy'` body returns a value or a Promise, as it +// can. A body whose last parameter is named `sync` receives there whether it +// must complete synchronously on this call, as a returned Promise could not be +// waited for. That is never the case for an 'auto' function under +// ASYNCIFY/JSPI, nor on behalf of a sync-proxied pthread caller, which awaits +// it; otherwise it is. +function handleSyncArg(symbol, snippet, sig, proxyingMode, canWait) { + const sync = canWait ? 'false' : PTHREADS && proxyingMode == 'sync' ? '!PThread.currentProxiedOperationCallerThread' : 'true'; return modifyJSFunction(snippet, (args, body, async_, oneliner) => { const params = args.split(',').map((a) => a.trim()).filter((a) => a); @@ -524,9 +525,16 @@ function handleSyncArg(symbol, snippet, sig, proxyingMode) { } if (!oneliner) body = `{\n${body}\n}`; const outer = params.slice(0, -1).join(', '); + const call = `(${async_}(${args}) => ${body})(${outer}${outer ? ', ' : ''}${sync})`; + if (!ASSERTIONS || sync == 'false') { + return `function(${outer}) { return ${call}; }\n`; + } + // A Promise returned where the caller cannot wait would be coerced to 0. return `\ function(${outer}) { - return (${async_}(${args}) => ${body})(${outer}${outer ? ', ' : ''}${sync}); + var rtn = ${call}; + assert(!(${sync == 'true' ? '' : sync + ' && '}rtn?.then), "'${symbol}' returned a Promise where the caller cannot wait (sync is true)"); + return rtn; }\n`; }); } @@ -651,7 +659,10 @@ function(${args}) { } const sig = LibraryManager.library[symbol + '__sig']; - const isAsyncFunction = ASYNCIFY && LibraryManager.library[symbol + '__async']; + const asyncMode = LibraryManager.library[symbol + '__async']; + // A 'proxy' function never suspends: a pthread's proxied call is its only + // asynchronous path, so it stays a plain import under ASYNCIFY/JSPI. + const isAsyncFunction = ASYNCIFY && asyncMode != 'proxy' && asyncMode; const i53abi = LibraryManager.library[symbol + '__i53abi']; if (i53abi) { @@ -671,8 +682,11 @@ function(${args}) { const proxyingMode = LibraryManager.library[symbol + '__proxy']; - if (LibraryManager.library[symbol + '__async'] == 'auto') { - snippet = handleSyncArg(symbol, snippet, sig, proxyingMode); + if (asyncMode == 'proxy' && proxyingMode != 'sync') { + error(`JS library error: '${symbol}' is '__async: 'proxy'' and requires '__proxy: 'sync''`); + } + if (asyncMode == 'auto' || asyncMode == 'proxy') { + snippet = handleSyncArg(symbol, snippet, sig, proxyingMode, isAsyncFunction); } if (ASYNCIFY && isAsyncFunction == 'auto') { @@ -775,7 +789,8 @@ function(${args}) { deps.push('setTempRet0'); } - const isAsyncFunction = LibraryManager.library[symbol + '__async']; + const asyncMode = LibraryManager.library[symbol + '__async']; + const isAsyncFunction = asyncMode && asyncMode != 'proxy'; if (ASYNCIFY && isAsyncFunction) { asyncFuncs.push(symbol); } diff --git a/test/test_jslib.py b/test/test_jslib.py index 759a61209a589..a1afd2652af7e 100644 --- a/test/test_jslib.py +++ b/test/test_jslib.py @@ -5,7 +5,7 @@ from subprocess import PIPE -from common import EMCC, RunnerCore, copy_asset, create_file, read_file, test_file +from common import EMCC, NON_ZERO, RunnerCore, copy_asset, create_file, read_file, test_file from decorators import ( also_with_wasm2js, also_with_wasm64, @@ -136,19 +136,30 @@ def test_jslib_proxying(self): '': ([],), 'asyncify': (['-sASYNCIFY', '-DASYNC'],), 'jspi': (['-sJSPI', '-DASYNC'],), - 'pthread': (['-pthread', '-sPROXY_TO_PTHREAD'],), + 'pthread': (['-pthread', '-sPROXY_TO_PTHREAD', '-DWAITS'],), + 'pthread_main': (['-pthread'],), + 'proxy': (['-DPROXY'],), + 'proxy_asyncify': (['-sASYNCIFY', '-DASYNC', '-DPROXY'],), + 'proxy_jspi': (['-sJSPI', '-DASYNC', '-DPROXY'],), + 'proxy_pthread': (['-pthread', '-sPROXY_TO_PTHREAD', '-DPROXY', '-DWAITS'],), + 'proxy_pthread_main': (['-pthread', '-DPROXY'],), + 'proxy_pthread_asyncify': (['-pthread', '-sPROXY_TO_PTHREAD', '-sASYNCIFY', '-DASYNC', '-DPROXY', '-DWAITS'],), + 'proxy_pthread_jspi': (['-pthread', '-sPROXY_TO_PTHREAD', '-sJSPI', '-DASYNC', '-DPROXY', '-DWAITS'],), }) def test_jslib_async_auto_sync(self, args): # `answer(ms, value)` resolves to `value` after `ms` milliseconds # (synchronously if `ms` is 0), or is -1 where the caller cannot wait. + # With PROXY (`__async: 'proxy'`) only a proxied pthread call can wait + # (WAITS: main() runs on a pthread). if '-sJSPI' in args: self.require_jspi() if '-sASYNCIFY' in args and self.get_setting('WASM_ESM_INTEGRATION'): self.skipTest('WASM_ESM_INTEGRATION is not compatible with ASYNCIFY') + mode = "'proxy'" if '-DPROXY' in args else "'auto'" create_file('lib.js', r''' addToLibrary({ answer__sig: 'pip', - answer__async: 'auto', + answer__async: MODE, answer__proxy: 'sync', answer: (ms, value, sync) => { if (!ms) return value; @@ -156,13 +167,13 @@ def test_jslib_async_auto_sync(self, args): return new Promise((resolve) => setTimeout(() => resolve(value), ms)); }, // No __sig: `sync` is found by name alone. - wait_ms__async: 'auto', + wait_ms__async: MODE, wait_ms__proxy: 'sync', wait_ms: (ms, sync) => { if (ms && !sync) return new Promise((resolve) => setTimeout(resolve, ms)); }, }); -''') +'''.replace('MODE', mode)) create_file('src.c', r''' #include #include @@ -173,7 +184,7 @@ def test_jslib_async_auto_sync(self, args): int main() { assert(answer(0, 7) == 7); -#if defined(__EMSCRIPTEN_PTHREADS__) || defined(ASYNC) +#if defined(WAITS) || (defined(ASYNC) && !defined(PROXY)) assert(answer(5, 42) == 42); #else assert(answer(5, 42) == -1); @@ -197,6 +208,39 @@ def test_jslib_async_auto_sync_arity(self): self.assert_fail([EMCC, test_file('hello_world.c'), '-sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=jslibfunc', '--js-library', 'lib.js'], "error: JS library error: 'jslibfunc' must take 'sync' as parameter 3, after the 2 of its '__sig'") + def test_jslib_async_auto_sync_promise(self): + # A body that ignores `sync` and returns a Promise where the caller cannot + # wait is caught, rather than coerced to 0. + create_file('lib.js', ''' +addToLibrary({ + jslibfunc__sig: 'ii', + jslibfunc__async: 'auto', + jslibfunc: (x, sync) => Promise.resolve(x), +}); +''') + create_file('src.c', ''' +#include +int jslibfunc(int x); +int main() { + printf("%d\\n", jslibfunc(1)); + printf("done\\n"); +} +''') + self.do_runf('src.c', "'jslibfunc' returned a Promise where the caller cannot wait (sync is true)", + assert_returncode=NON_ZERO, cflags=['--js-library', 'lib.js']) + self.do_runf('src.c', '1\ndone\n', cflags=['--js-library', 'lib.js', '-sASYNCIFY']) + + def test_jslib_async_proxy_requires_proxying(self): + create_file('lib.js', ''' +addToLibrary({ + jslibfunc__sig: 'ii', + jslibfunc__async: 'proxy', + jslibfunc: (x, sync) => x, +}); +''') + self.assert_fail([EMCC, test_file('hello_world.c'), '-sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=jslibfunc', '--js-library', 'lib.js'], + "error: JS library error: 'jslibfunc' is '__async: 'proxy'' and requires '__proxy: 'sync''") + def test_jslib_method_syntax(self): create_file('lib.js', r''' addToLibrary({