Skip to content
Closed
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
49 changes: 35 additions & 14 deletions site/source/docs/porting/asyncify.rst
Original file line number Diff line number Diff line change
Expand Up @@ -200,30 +200,51 @@ You can use ``__async: 1`` if you just want to include the function in
:ref:`ASYNCIFY_IMPORTS` or ``__async: 'auto'`` if you also want the function to
wrapper in ``Asyncify.handleAsync``.

``__async: 'auto'`` allows the same function to work synchronously without
``ASYNCIFY``. A function whose last parameter is named ``sync`` is told there
whether it must return synchronously. This is ``false`` under
``ASYNCIFY``/JSPI, and when called from a pthread with ``__proxy: 'sync'`` (the
main thread awaits the result on its behalf); it is ``true`` otherwise, where a
returned ``Promise`` could not be waited for:
An ``'auto'`` function may return a Promise only when something can wait for
it. There are two such situations: the function suspends under ``ASYNCIFY``
or JSPI, or it was called from a pthread with ``__proxy: 'sync'``, in which
case the pthread is blocked and the main thread awaits the Promise for it. On
a plain main thread call nothing can wait, and the function has to return a
value right away.

To let one body handle both, give ``__async`` a list of options and include
``'sync'``. The body then takes a trailing ``sync`` parameter: ``true`` means
"return a value now", ``false`` means "you may return a Promise":

.. code-block:: js

addToLibrary({
lookup__sig: 'pp',
lookup__async: 'auto',
lookup__async: ['auto', 'sync'],
lookup__proxy: 'sync',
lookup: (name, sync) => {
if (sync) return -EAGAIN;
return fetchSomething(name); // returns a Promise
if (sync) return -EAGAIN; // main thread, nothing can wait
return fetchSomething(name); // returns a Promise
},
});

In the above example, the same ``lookup`` supports the async promise-returning
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`).
With ``['auto', 'sync']`` the function suspends under ``ASYNCIFY``/JSPI like
any ``'auto'`` function (so it must be called from a stack that can suspend;
under JSPI, within one of the :ref:`JSPI_EXPORTS`), and also waits when called
from a pthread. ``sync`` is ``true`` only on a main thread call without
``ASYNCIFY``/JSPI.

Some functions must never suspend, because they are called from everywhere:
``write()`` on a pipe is behind every ``printf``, and a suspending import there
would instrument or trap every caller. Such a function can still block when
called from a pthread. Listing ``['sync']`` without ``'auto'`` gives exactly
that: the function stays a plain import in every build, and ``sync`` is
``false`` only when a pthread is waiting on it. It requires ``__proxy:
'sync'``.

====================== ============================ =======================
``__async`` ``sync`` on the main thread ``sync`` from a pthread
====================== ============================ =======================
``['auto', 'sync']`` ``false`` under ``false``
``ASYNCIFY``/JSPI, else
``true``
``['sync']`` ``true`` ``false``
====================== ============================ =======================


Ways to use Asyncify APIs in older engines
Expand Down
37 changes: 22 additions & 15 deletions src/jsifier.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,13 @@ function sigToArgs(sig) {
return args.join(',');
}

// Whether the body returns a value or a Promise as it can: an 'auto' function,
// or one told by a `sync` argument.
function isAutoBody(symbol) {
const lib = LibraryManager.library;
return lib[symbol + '__async'] == 'auto' || !!lib[symbol + '__sync_arg'];
}

function handleI64Signatures(symbol, snippet, sig, i53abi, isAsyncFunction) {
// Handle i64 parameters and return values.
//
Expand Down Expand Up @@ -354,7 +361,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 && isAutoBody(symbol);
const convert = (v) => maybePromise ?
`(${v} instanceof Promise ? ${v}.then((v) => ${makeReturn64('v')}) : ${makeReturn64(v)})` :
makeReturn64(await_ + v);
Expand Down Expand Up @@ -395,19 +402,18 @@ ${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' :
// A body listing 'sync' in its `__async` options receives, in its trailing
// `sync` parameter, whether it must complete synchronously on this call, as a
// returned Promise would have nowhere to go: it never must where the function
// itself suspends under ASYNCIFY/JSPI, nor on behalf of a sync-proxied pthread
// caller, which awaits it; otherwise it must.
function handleSyncArg(symbol, snippet, 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);
if (params.at(-1) != 'sync') return snippet;
if (sig && params.length != sig.length) {
error(`JS library error: '${symbol}' must take 'sync' as parameter ${sig.length}, after the ${sig.length - 1} of its '__sig'`);
if (params.at(-1) != 'sync') {
error(`JS library error: '${symbol}' lists 'sync' in '__async' but its last parameter is not named 'sync'`);
return snippet;
}
if (!oneliner) body = `{\n${body}\n}`;
Expand Down Expand Up @@ -560,8 +566,8 @@ function(${args}) {

const proxyingMode = LibraryManager.library[symbol + '__proxy'];

if (LibraryManager.library[symbol + '__async'] == 'auto') {
snippet = handleSyncArg(symbol, snippet, sig, proxyingMode);
if (LibraryManager.library[symbol + '__sync_arg']) {
snippet = handleSyncArg(symbol, snippet, proxyingMode, isAsyncFunction);
}

if (ASYNCIFY && isAsyncFunction == 'auto') {
Expand All @@ -580,8 +586,9 @@ function(${args}) {
}
let proxyMode = PROXY_ASYNC;
if (proxyingMode === 'sync') {
const isAsyncFunction = LibraryManager.library[symbol + '__async'];
if (isAsyncFunction) {
// A body told by `sync` may complete through a Promise on the
// pthread's behalf even where the function never suspends.
if (LibraryManager.library[symbol + '__async'] || isAutoBody(symbol)) {
proxyMode = PROXY_SYNC_ASYNC;
} else {
proxyMode = PROXY_SYNC;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/libsyscall.js
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ var SyscallsLibrary = {
return flags & (events | {{{ cDefs.POLLERR }}} | {{{ cDefs.POLLHUP }}} | {{{ cDefs.POLLNVAL }}});
},
__syscall_poll__proxy: 'sync',
__syscall_poll__async: 'auto',
__syscall_poll__async: ['auto', 'sync'],
__syscall_poll__deps: ['$doPollSync',
#if PTHREADS || ASYNCIFY
'$doPollAsync',
Expand Down Expand Up @@ -735,7 +735,7 @@ var SyscallsLibrary = {
return epollCtl(ep.shared, op, fd, ev);
},
__syscall_epoll_pwait__proxy: 'sync',
__syscall_epoll_pwait__async: 'auto',
__syscall_epoll_pwait__async: ['auto', 'sync'],
__syscall_epoll_pwait__deps: ['$FS', '$epollPwait'],
__syscall_epoll_pwait: (epfd, ev, maxevents, timeout, sigmask, sigsetsize, sync) => {
var ep = FS.getStream(epfd);
Expand Down
36 changes: 36 additions & 0 deletions src/modules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -286,11 +286,47 @@ export const LibraryManager = {
}
timer.stop('executeJS')

this.expandAsyncOptions();
this.addAliasDependencies();

timer.stop('load')
},

/**
* `__async: ['auto', 'sync']` lists async options. 'auto' is as
* `__async: 'auto'`; without it the function never suspends. 'sync' has the
* body told, in a trailing `sync` parameter, whether it must complete
* synchronously (see handleSyncArg in jsifier). Alone, 'sync' leaves the
* sync-proxied pthread path as the only asynchronous one.
*/
expandAsyncOptions() {
for (const [key, options] of Object.entries(this.library)) {
if (!key.endsWith('__async') || typeof options != 'object') continue;
const base = key.slice(0, -'__async'.length);
if (!Array.isArray(options) || !options.length) {
error(`JS library error: '${key}' must be true, 'auto', or a non-empty array of options`);
continue;
}
const unknown = options.find((option) => !['auto', 'sync'].includes(option));
if (unknown) {
error(`JS library error: unknown async option '${unknown}' in '${key}'`);
continue;
}
if (options.includes('auto')) {
this.library[key] = 'auto';
} else {
delete this.library[key];
if (this.library[base + '__proxy'] != 'sync') {
error(`JS library error: '${key}' lists only 'sync', which has an asynchronous path only with '__proxy' set to 'sync'`);
continue;
}
}
if (options.includes('sync')) {
this.library[base + '__sync_arg'] = true;
}
}
},

isAlias(entry) {
return (typeof entry === 'string' && entry[0] != '=' && (this.library.hasOwnProperty(entry) || WASM_EXPORTS.has(entry)));
},
Expand Down
3 changes: 2 additions & 1 deletion src/utility.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export function mergeInto(obj, other, options = null) {
__noleakcheck: 'boolean',
__internal: 'boolean',
__user: 'boolean',
__async: ['string', 'boolean'],
__async: ['string', 'boolean', 'object'],
__i53abi: 'boolean',
__export: 'boolean',
__force: 'boolean',
Expand Down Expand Up @@ -223,6 +223,7 @@ export const decoratorSuffixes = [
'__i53abi',
'__export',
'__force',
'__sync_arg',
];

export function isDecorator(ident) {
Expand Down
36 changes: 23 additions & 13 deletions test/test_jslib.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,32 +132,36 @@ def test_jslib_proxying(self):
'asyncify': (['-sASYNCIFY', '-DASYNC'],),
'jspi': (['-sJSPI', '-DASYNC'],),
'pthread': (['-pthread', '-sPROXY_TO_PTHREAD'],),
'proxy_only': (['-DPROXY_ONLY'],),
'proxy_only_asyncify': (['-sASYNCIFY', '-DASYNC', '-DPROXY_ONLY'],),
'proxy_only_pthread': (['-pthread', '-sPROXY_TO_PTHREAD', '-DPROXY_ONLY'],),
})
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_ONLY (`['sync']`) only a proxied pthread call can wait.
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')
options = "['sync']" if '-DPROXY_ONLY' in args else "['auto', 'sync']"
create_file('lib.js', r'''
addToLibrary({
answer__sig: 'pip',
answer__async: 'auto',
answer__async: OPTIONS,
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: OPTIONS,
wait_ms__proxy: 'sync',
wait_ms: (ms, sync) => {
if (ms && !sync) return new Promise((resolve) => setTimeout(resolve, ms));
},
});
''')
'''.replace('OPTIONS', options))
create_file('src.c', r'''
#include <assert.h>
#include <stdint.h>
Expand All @@ -168,7 +172,7 @@ def test_jslib_async_auto_sync(self, args):

int main() {
assert(answer(0, 7) == 7);
#if defined(__EMSCRIPTEN_PTHREADS__) || defined(ASYNC)
#if defined(__EMSCRIPTEN_PTHREADS__) || (defined(ASYNC) && !defined(PROXY_ONLY))
assert(answer(5, 42) == 42);
#else
assert(answer(5, 42) == -1);
Expand All @@ -181,16 +185,22 @@ def test_jslib_async_auto_sync(self, args):
''')
self.do_runf('src.c', 'done\n', cflags=args + ['-sEXIT_RUNTIME', '--js-library', 'lib.js'])

def test_jslib_async_auto_sync_arity(self):
create_file('lib.js', '''
addToLibrary({
jslibfunc__sig: 'iii',
jslibfunc__async: 'auto',
jslibfunc: (x, sync) => x,
});
@parameterized({
'empty': ('[]', '(x, sync) => x', "'jslibfunc__async' must be true, 'auto', or a non-empty array of options"),
'unknown': ("['foo']", '(x, sync) => x', "unknown async option 'foo' in 'jslibfunc__async'"),
'param': ("['auto', 'sync']", '(x) => x', "'jslibfunc' lists 'sync' in '__async' but its last parameter is not named 'sync'"),
'proxy': ("['sync']", '(x, sync) => x', "'jslibfunc__async' lists only 'sync', which has an asynchronous path only with '__proxy' set to 'sync'"),
})
def test_jslib_async_options_errors(self, options, body, expected):
create_file('lib.js', f'''
addToLibrary({{
jslibfunc__sig: 'ii',
jslibfunc__async: {options},
jslibfunc: {body},
}});
''')
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'")
'error: JS library error: ' + expected)

def test_jslib_method_syntax(self):
create_file('lib.js', r'''
Expand Down
Loading