Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions site/source/docs/porting/asyncify.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 28 additions & 13 deletions src/jsifier.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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`;
});
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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') {
Expand Down Expand Up @@ -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);
Comment thread
guybedford marked this conversation as resolved.
Resolved
}
Expand Down
56 changes: 50 additions & 6 deletions test/test_jslib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -136,33 +136,44 @@ 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;
if (sync) return -1;
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 <assert.h>
#include <stdint.h>
Expand All @@ -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);
Expand All @@ -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 <stdio.h>
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({
Expand Down
Loading