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
25 changes: 19 additions & 6 deletions site/source/docs/porting/asyncify.rst
Original file line number Diff line number Diff line change
Expand Up @@ -233,23 +233,36 @@ Async auto call variants

Instead of ``'auto'``, ``__async`` can be given a list of call variants to
generate alongside the function, which otherwise behaves as ``__async:
'auto'``. The ``'promise'`` variant is the function called with a ``_promise``
suffix, obtaining the promise value directly without suspending, so it can be
called from any stack.
'auto'``. The ``'promise'`` and ``'callback'`` variants are the function called
with a ``_promise`` or ``_callback`` suffix, obtaining the result as a promise or
through a callback directly without suspending, so they can be called from any
stack.

Changing the ``lookup`` definition from the example in the previous section
above to ``lookup__async: ['promise']`` defines both C variants:
above to ``lookup__async: ['promise', 'callback']`` defines all three C
variants:

.. code-block:: c

intptr_t lookup(const char* name);
em_promise_t lookup_promise(const char* name);
void lookup_callback(const char* name,
void (*callback)(bool err, void* result, void* arg),
void* arg);

Calling ``lookup_promise()`` runs the same body (with ``sync`` always
``false``) and returns a promise handle (``em_promise_t``, see
``<emscripten/promise.h>``) on the calling thread, fulfilled with the result or
rejected with ``NULL``. The function may also return ``void``, in which case
the result is ``NULL``. These variants are only generated when explicitly used.
rejected with ``NULL``.

Calling ``lookup_callback()`` likewise runs the body and calls
``callback(err, result, arg)`` on the calling thread once the result is
available, with ``err`` set (and ``result`` ``NULL``) if the promise was
rejected, in either case after the call has returned. A pending callback keeps
the runtime alive, like a timer.

The function may also return ``void``, in which case the result is ``NULL``.
These variants are only generated when explicitly used.

Ways to use Asyncify APIs in older engines
##########################################
Expand Down
3 changes: 2 additions & 1 deletion site/source/docs/porting/pthreads.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ thread) to be called without :ref:`ASYNCIFY` from a background thread. In other
words, background threads can block on proxied work even without :ref:`ASYNCIFY`
enabled. :ref:`async_auto_call_variants` can be generated for such functions
to support non-suspending callers, for example to obtain a promise handle
(``em_promise_t``) for the main thread's result directly.
(``em_promise_t``) for the main thread's result directly, or to have a callback
called with it on the calling thread.

.. note:: Proxying can cause problems, such as deadlocks, in certain cases, see
the section on blocking below.
Expand Down
36 changes: 29 additions & 7 deletions src/jsifier.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,11 @@ function asyncVariantOf(symbol) {
return LibraryManager.library[symbol + '__async_variant'];
}

// The __sig of the 'auto' function an async variant was expanded from.
function asyncVariantBaseSig(symbol, variant) {
return LibraryManager.library[symbol.slice(0, -variant.length - 1) + '__sig'];
}

function handleI64Signatures(symbol, snippet, sig, i53abi, isAsyncFunction) {
// Handle i64 parameters and return values.
//
Expand Down Expand Up @@ -547,11 +552,12 @@ function(${args}) {
// it is.
const sync = ASYNCIFY || asyncVariant ? 'false' :
PTHREADS && proxyingMode == 'sync' ? '!PThread.currentProxiedOperationCallerThread' : 'true';
const baseSig = asyncVariant ? asyncVariantBaseSig(symbol, asyncVariant) : sig;
snippet = 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 (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.length != baseSig.length) {
error(`JS library error: '${symbol}' must take 'sync' as parameter ${baseSig.length}, after the ${baseSig.length - 1} of its '__sig'`);
return snippet;
}
if (!oneliner) body = `{\n${body}\n}`;
Expand All @@ -563,8 +569,23 @@ function(${outer}) {
});
}

// A async variant hands its Promise over rather than suspending on it.
if (!asyncVariant && isAsyncFunction == 'auto') {
// An async variant hands its Promise over rather than suspending on it.
if (asyncVariant == 'callback') {
// The result of the 'auto' body to callback(err, result, arg) on the
// calling thread (see $callbackFromPromise). The two trailing parameters
// are the variant's own. This wraps inside the proxying, which carries
// them along from a pthread; on the main thread running the body on a
// pthread's behalf (PThread.currentProxiedOperationCallerThread set) the
// bare result is what crosses back. A void body's undefined result is
// NULL.
snippet = modifyJSFunction(snippet, (args, body, async_, oneliner) => {
if (!oneliner) body = `(${async_}() => {\n${body}\n})()`;
const bare = PTHREADS && proxyingMode == 'sync' ?
'if (PThread.currentProxiedOperationCallerThread) return r;\n ' : '';
return `function(${args}${args ? ', ' : ''}callback_, arg_) {\n var r = Promise.resolve(${body}).then((v) => v ?? 0);\n ${bare}callbackFromPromise(r, callback_, arg_);\n}\n`;
});
deps.push('$callbackFromPromise');
} else if (!asyncVariant && isAsyncFunction == 'auto') {
snippet = handleAsyncFunction(snippet, sig, proxyingMode == 'sync');
}

Expand All @@ -590,10 +611,11 @@ function(${outer}) {
const rtnType = sig?.[0];
let proxyFunc = MEMORY64 && rtnType == 'p' ? 'proxyToMainThreadPtr' : 'proxyToMainThread';
let modeArg = `, ${proxyMode}`;
if (asyncVariant == 'promise') {
if (asyncVariant) {
// The body returns a value or a Promise; a pthread caller gets an
// em_promise_t of the main-thread result.
proxyFunc = 'proxyToMainThreadPromise';
// em_promise_t of the main-thread result, or its callback called
// with it.
proxyFunc = asyncVariant == 'promise' ? 'proxyToMainThreadPromise' : 'proxyToMainThreadCallback';
modeArg = '';
}
deps.push('$' + proxyFunc);
Expand Down
14 changes: 14 additions & 0 deletions src/lib/libcore.js
Original file line number Diff line number Diff line change
Expand Up @@ -2182,6 +2182,20 @@ addToLibrary({
}
},

// The `_callback` variant of an __async: 'auto' function on the main thread
// (see jsifier): once the body's value or Promise settles, callback(err,
// value, arg), err set if it rejected, never before the call returns. A
// pending callback holds the runtime alive like a timer.
$callbackFromPromise__deps: ['$callUserCallback'],
$callbackFromPromise: (result, callback, arg) => {
{{{ runtimeKeepalivePush() }}}
var settle = (err, value) => {
{{{ runtimeKeepalivePop() }}}
callUserCallback(() => {{{ makeDynCall('vipp', 'callback') }}}(err, value, arg));
};
Promise.resolve(result).then((value) => settle(0, value), () => settle(1, 0));
},

$maybeExit__deps: ['exit', '$handleException', '$keepRuntimeAlive',
#if PTHREADS
'_emscripten_thread_exit',
Expand Down
14 changes: 14 additions & 0 deletions src/lib/libpthread.js
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,20 @@ var LibraryPThread = {
return rtn;
},

// As proxyToMainThread for the _callback variant, whose trailing callback
// and arg are called on this thread with the main-thread result once it next
// runs its proxying queue. (They are serialized along too, unused, so the
// main-thread call has the function's full arity.)
$proxyToMainThreadCallback__deps: ['$stackSave', '$stackRestore', '$serializeProxiedArgs', '_emscripten_run_js_on_main_thread_callback'],
$proxyToMainThreadCallback__docs: '/** @type{function(number, (number|boolean), ...number)} */',
$proxyToMainThreadCallback: (funcIndex, emAsmAddr, ...callArgs) => {
var [callback, arg] = callArgs.slice(-2);
var bufSize = 8 * callArgs.length {{{ WASM_BIGINT ? "* 2" : "" }}};
var sp = stackSave();
__emscripten_run_js_on_main_thread_callback(funcIndex, emAsmAddr, bufSize, serializeProxiedArgs(callArgs, bufSize), callback, arg);
stackRestore(sp);
},

// Reuse global JS array to avoid creating JS garbage for each proxied call
$proxiedJSCallArgs: [],

Expand Down
29 changes: 17 additions & 12 deletions src/modules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ export const nativeAliases = {};
const srcDir = fileURLToPath(new URL('.', import.meta.url));
const systemLibdir = path.join(srcDir, 'lib');

// Return type of each async variant by suffix (see expandAsyncVariants).
const ASYNC_VARIANTS = new Map([['promise', 'p']]);
// Return type and trailing parameters of each async variant by suffix (see
// expandAsyncVariants).
const ASYNC_VARIANTS = new Map([['promise', ['p', '']], ['callback', ['v', 'pp']]]);

function isBeneath(childPath, parentPath) {
const relativePath = path.relative(parentPath, childPath);
Expand Down Expand Up @@ -296,12 +297,15 @@ export const LibraryManager = {
},

/**
* `__async: ['promise', ...]` is `__async: 'auto'` along with the listed
* variants. An 'auto' function returns a pointer-sized value or nothing, or
* a Promise of either, unless its trailing `sync` argument forbids (see
* jsifier). Its `foo_promise` variant runs the same body, returning an
* em_promise_t of the result (NULL for a void function) on the calling
* thread, which can always wait. Variants are emitted only where used.
* `__async: ['promise', 'callback', ...]` is `__async: 'auto'` along with the
* listed variants. An 'auto' function returns a pointer-sized value or
* nothing, or a Promise of either, unless its trailing `sync` argument
* forbids (see jsifier). Its `foo_promise` variant runs the same body,
* returning an em_promise_t of the result (NULL for a void function) on the
* calling thread, and its `foo_callback` variant takes
* `callback(err, result, arg)` and `arg` to call on the calling thread with it
* (see $callbackFromPromise); both can always wait. Variants are emitted only
* where used.
*/
expandAsyncVariants() {
for (const [key, variants] of Object.entries(this.library)) {
Expand All @@ -318,7 +322,7 @@ export const LibraryManager = {
continue;
}
for (const variant of variants) {
const rtn = ASYNC_VARIANTS.get(variant);
const [rtn, params] = ASYNC_VARIANTS.get(variant) ?? [];
const name = `${base}_${variant}`;
if (!rtn) {
error(`JS library error: unknown async variant '${variant}' in '${key}'`);
Expand All @@ -330,14 +334,15 @@ export const LibraryManager = {
}
this.library[name] = this.library[base];
this.library[name + '__async_variant'] = variant;
this.library[name + '__sig'] = rtn + sig.slice(1);
this.library[name + '__sig'] = rtn + sig.slice(1) + params;
for (const decorator of ['__async', '__proxy', '__deps', '__i53abi', '__user']) {
if (this.library.hasOwnProperty(base + decorator)) {
this.library[name + decorator] = this.library[base + decorator];
}
}
if (PTHREADS && variant == 'promise' && this.library[base + '__proxy'] == 'sync') {
this.library[name + '__deps'] = [...(this.library[name + '__deps'] ?? []), '$proxyToMainThreadPromise'];
if (PTHREADS && this.library[base + '__proxy'] == 'sync') {
const proxyFunc = variant == 'promise' ? '$proxyToMainThreadPromise' : '$proxyToMainThreadCallback';
this.library[name + '__deps'] = [...(this.library[name + '__deps'] ?? []), proxyFunc];
}
}
}
Expand Down
79 changes: 71 additions & 8 deletions system/lib/pthread/proxying.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

#include <assert.h>
#include <emscripten/eventloop.h>
#include <emscripten/proxying.h>
#include <emscripten/threading.h>
#include <pthread.h>
Expand Down Expand Up @@ -732,6 +733,20 @@ static em_promise_result_t js_promise_settle(void** result, void* data, void* va
return rtn;
}

// A heap copy of a call's arguments, outliving the call.
static proxied_js_func_t
js_func_copy(int func_index, void* em_asm_addr, int buf_size, double* buffer) {
proxied_js_func_t f = {
.funcIndex = func_index,
.emAsmAddr = em_asm_addr,
.callingThread = pthread_self(),
.bufSize = buf_size,
.argBuffer = malloc(buf_size),
};
memcpy(f.argBuffer, buffer, buf_size);
return f;
}

// Returns immediately on the calling thread with an em_promise_t; async on the
// main thread, whose result settles it once the calling thread runs its queue
// (emscripten_proxy_promise_with_ctx). Rejected with NULL if the function's
Expand All @@ -741,14 +756,7 @@ em_promise_t _emscripten_run_js_on_main_thread_promise(int func_index,
int buf_size,
double* buffer) {
proxied_js_func_t* arg = malloc(sizeof(proxied_js_func_t));
*arg = (proxied_js_func_t){
.funcIndex = func_index,
.emAsmAddr = em_asm_addr,
.callingThread = pthread_self(),
.bufSize = buf_size,
.argBuffer = malloc(buf_size),
};
memcpy(arg->argBuffer, buffer, buf_size);
*arg = js_func_copy(func_index, em_asm_addr, buf_size, buffer);
em_promise_t done = emscripten_proxy_promise_with_ctx(
emscripten_proxy_get_system_queue(),
emscripten_main_runtime_thread_id(),
Expand All @@ -759,3 +767,58 @@ em_promise_t _emscripten_run_js_on_main_thread_promise(int func_index,
emscripten_promise_destroy(done);
return promise;
}

typedef struct proxied_js_callback_t {
// First, as the arg of run_js_func_with_ctx.
proxied_js_func_t func;
void (*callback)(bool err, void* result, void* arg);
void* arg;
} proxied_js_callback_t;

static void js_callback_settle(proxied_js_callback_t* c, bool err) {
c->callback(err, (void*)(intptr_t)c->func.result, c->arg);
free(c->func.argBuffer);
free(c);
emscripten_runtime_keepalive_pop();
}

static void js_callback_call(void* data) {
proxied_js_callback_t* c = data;
js_callback_settle(c, !c->func.fulfilled);
}

static void js_callback_cancel(void* data) {
proxied_js_callback_t* c = data;
c->func.result = 0;
js_callback_settle(c, true);
}

// Calls `callback(err, result, arg)` on the calling thread with the
// main-thread result once it runs its queue
// (emscripten_proxy_callback_with_ctx); `err` if the function's Promise
// rejected or the main thread exited. The pending callback holds this
// thread's runtime alive like a timer.
void _emscripten_run_js_on_main_thread_callback(int func_index,
void* em_asm_addr,
int buf_size,
double* buffer,
void (*callback)(bool,
void*,
void*),
void* arg) {
emscripten_runtime_keepalive_push();
proxied_js_callback_t* c = malloc(sizeof(*c));
*c = (proxied_js_callback_t){
.func = js_func_copy(func_index, em_asm_addr, buf_size, buffer),
.callback = callback,
.arg = arg,
};
if (!emscripten_proxy_callback_with_ctx(emscripten_proxy_get_system_queue(),
emscripten_main_runtime_thread_id(),
run_js_func_with_ctx,
js_callback_call,
js_callback_cancel,
c)) {
js_callback_cancel(c);
}
}
8 changes: 4 additions & 4 deletions test/codesize/test_codesize_hello_O0.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"a.out.js": 23533,
"a.out.js.gz": 8550,
"a.out.js": 23553,
"a.out.js.gz": 8554,
"a.out.nodebug.wasm": 14279,
"a.out.nodebug.wasm.gz": 7101,
"total": 37812,
"total_gz": 15651,
"total": 37832,
"total_gz": 15655,
"sent": [
"fd_write"
],
Expand Down
1 change: 1 addition & 0 deletions test/codesize/test_codesize_minimal_O0.expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,7 @@ Module['FS_createPreloadedFile'] = FS.createPreloadedFile;
'runtimeKeepalivePush',
'runtimeKeepalivePop',
'callUserCallback',
'callbackFromPromise',
'maybeExit',
'asyncLoad',
'asmjsMangle',
Expand Down
8 changes: 4 additions & 4 deletions test/codesize/test_codesize_minimal_O0.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"a.out.js": 18720,
"a.out.js.gz": 6723,
"a.out.js": 18740,
"a.out.js.gz": 6729,
"a.out.nodebug.wasm": 1071,
"a.out.nodebug.wasm.gz": 636,
"total": 19791,
"total_gz": 7359,
"total": 19811,
"total_gz": 7365,
"sent": [],
"imports": [],
"exports": [
Expand Down
12 changes: 6 additions & 6 deletions test/codesize/test_unoptimized_code_size.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"hello_world.js": 54111,
"hello_world.js.gz": 17152,
"hello_world.js": 54136,
"hello_world.js.gz": 17158,
"hello_world.wasm": 14279,
"hello_world.wasm.gz": 7101,
"no_asserts.js": 23077,
"no_asserts.js.gz": 8099,
"no_asserts.wasm": 11393,
"no_asserts.wasm.gz": 5646,
"strict.js": 51239,
"strict.js.gz": 16135,
"strict.js": 51264,
"strict.js.gz": 16142,
"strict.wasm": 14279,
"strict.wasm.gz": 7098,
"total": 168378,
"total_gz": 61231
"total": 168428,
"total_gz": 61244
}
Loading
Loading