From a99adc5a79d3a4bad994c2bee18a37772da23d89 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Mon, 12 Oct 2026 16:02:30 -0700 Subject: [PATCH] Add 'callback' async variants for JS library functions foo_callback(args..., callback, arg) runs the same body and calls callback(err, result, arg) on the calling thread once the result is available, err set if its Promise rejects, always after the call has returned. A pending callback holds the runtime alive like a timer. From a pthread it is emscripten_proxy_callback_with_ctx of the main-thread body, with the result riding in the proxied call's arg as for _promise. --- site/source/docs/porting/asyncify.rst | 25 ++++- site/source/docs/porting/pthreads.rst | 3 +- src/jsifier.mjs | 36 ++++-- src/lib/libcore.js | 14 +++ src/lib/libpthread.js | 14 +++ src/modules.mjs | 29 +++-- system/lib/pthread/proxying.c | 79 ++++++++++++-- test/codesize/test_codesize_hello_O0.json | 8 +- .../test_codesize_minimal_O0.expected.js | 1 + test/codesize/test_codesize_minimal_O0.json | 8 +- test/codesize/test_unoptimized_code_size.json | 12 +- test/test_jslib.py | 103 +++++++++++++++--- tools/native_sigs.py | 1 + 13 files changed, 271 insertions(+), 62 deletions(-) diff --git a/site/source/docs/porting/asyncify.rst b/site/source/docs/porting/asyncify.rst index bc3df29975ce3..2d2187a7a8d4b 100644 --- a/site/source/docs/porting/asyncify.rst +++ b/site/source/docs/porting/asyncify.rst @@ -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 ````) 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 ########################################## diff --git a/site/source/docs/porting/pthreads.rst b/site/source/docs/porting/pthreads.rst index a8f483aa7cba9..b145387d1d24e 100644 --- a/site/source/docs/porting/pthreads.rst +++ b/site/source/docs/porting/pthreads.rst @@ -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. diff --git a/src/jsifier.mjs b/src/jsifier.mjs index d73eb4c16b166..53676fe2812e5 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -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. // @@ -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}`; @@ -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'); } @@ -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); diff --git a/src/lib/libcore.js b/src/lib/libcore.js index d280836d8a75b..d0e79e4f09fe8 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -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', diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index 8ff16770a89a7..4c77c1023781e 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -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: [], diff --git a/src/modules.mjs b/src/modules.mjs index de9bc75599014..e7f49bdb4013c 100644 --- a/src/modules.mjs +++ b/src/modules.mjs @@ -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); @@ -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)) { @@ -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}'`); @@ -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]; } } } diff --git a/system/lib/pthread/proxying.c b/system/lib/pthread/proxying.c index 8404e28617f89..cd024a2ffd15d 100644 --- a/system/lib/pthread/proxying.c +++ b/system/lib/pthread/proxying.c @@ -6,6 +6,7 @@ */ #include +#include #include #include #include @@ -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 @@ -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(), @@ -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); + } +} diff --git a/test/codesize/test_codesize_hello_O0.json b/test/codesize/test_codesize_hello_O0.json index 854681ba3614b..005b82c72771e 100644 --- a/test/codesize/test_codesize_hello_O0.json +++ b/test/codesize/test_codesize_hello_O0.json @@ -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" ], diff --git a/test/codesize/test_codesize_minimal_O0.expected.js b/test/codesize/test_codesize_minimal_O0.expected.js index 098acb976a097..a55b458b43154 100644 --- a/test/codesize/test_codesize_minimal_O0.expected.js +++ b/test/codesize/test_codesize_minimal_O0.expected.js @@ -837,6 +837,7 @@ Module['FS_createPreloadedFile'] = FS.createPreloadedFile; 'runtimeKeepalivePush', 'runtimeKeepalivePop', 'callUserCallback', + 'callbackFromPromise', 'maybeExit', 'asyncLoad', 'asmjsMangle', diff --git a/test/codesize/test_codesize_minimal_O0.json b/test/codesize/test_codesize_minimal_O0.json index c0b14244f1108..26fab1afddc5f 100644 --- a/test/codesize/test_codesize_minimal_O0.json +++ b/test/codesize/test_codesize_minimal_O0.json @@ -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": [ diff --git a/test/codesize/test_unoptimized_code_size.json b/test/codesize/test_unoptimized_code_size.json index d43a5f3f27caa..3547fba41496c 100644 --- a/test/codesize/test_unoptimized_code_size.json +++ b/test/codesize/test_unoptimized_code_size.json @@ -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 } diff --git a/test/test_jslib.py b/test/test_jslib.py index 4c5b56d6da308..05a534bdd1a62 100644 --- a/test/test_jslib.py +++ b/test/test_jslib.py @@ -192,18 +192,11 @@ 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'") - @also_with_wasm64 - @parameterized({ - '': ([],), - 'asyncify': (['-sASYNCIFY', '-DASYNC'],), - 'jspi': (['-sJSPI', '-DASYNC'],), - 'pthread': (['-pthread', '-sPROXY_TO_PTHREAD'],), - }) - def test_jslib_async_auto_variants(self, args): - # An `__async: ['promise']` function `answer(ms, value)` resolving to `value` - # after `ms` milliseconds (synchronously if `ms` is 0, rejecting if `value` - # is 0), and its `answer_promise` variant. Where the caller cannot wait, - # `answer()` is -1. `wait_ms(ms)` is the same without a result. + def setup_async_auto_variants(self, args): + # An `__async: ['promise', 'callback']` function `answer(ms, value)` + # resolving to `value` after `ms` milliseconds (synchronously if `ms` is 0, + # rejecting if `value` is 0). Where the caller cannot wait, `answer()` is -1. + # `wait_ms(ms)` is the same without a result. if '-sJSPI' in args: self.require_jspi() if '-sASYNCIFY' in args and self.get_setting('WASM_ESM_INTEGRATION'): @@ -211,7 +204,7 @@ def test_jslib_async_auto_variants(self, args): create_file('lib.js', r''' addToLibrary({ answer__sig: 'pip', - answer__async: ['promise'], + answer__async: ['promise', 'callback'], answer__proxy: 'sync', answer: (ms, value, sync) => { if (!ms) return value; @@ -220,13 +213,24 @@ def test_jslib_async_auto_variants(self, args): setTimeout(() => value ? resolve(value) : reject(new Error('zero')), ms)); }, wait_ms__sig: 'vi', - wait_ms__async: ['promise'], + wait_ms__async: ['promise', 'callback'], wait_ms__proxy: 'sync', wait_ms: (ms, sync) => { if (ms && !sync) return new Promise((resolve) => setTimeout(resolve, ms)); }, }); ''') + + @also_with_wasm64 + @parameterized({ + '': ([],), + 'asyncify': (['-sASYNCIFY', '-DASYNC'],), + 'jspi': (['-sJSPI', '-DASYNC'],), + 'pthread': (['-pthread', '-sPROXY_TO_PTHREAD'],), + }) + def test_jslib_async_auto_variants(self, args): + # `answer()`, `wait_ms()` and their `_promise` variants. + self.setup_async_auto_variants(args) create_file('src.c', r''' #include #include @@ -327,6 +331,77 @@ def test_jslib_async_variants_errors(self, sig, variants, extra, expected): self.assert_fail([EMCC, test_file('hello_world.c'), '-sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=jslibfunc', '--js-library', 'lib.js'], 'error: JS library error: ' + expected) + @also_with_wasm64 + @parameterized({ + '': ([],), + 'asyncify': (['-sASYNCIFY'],), + 'jspi': (['-sJSPI'],), + 'pthread': (['-pthread', '-sPROXY_TO_PTHREAD'],), + }) + def test_jslib_async_auto_variants_callback(self, args): + # The `answer_callback` and `wait_ms_callback` variants: callback(err, + # result, arg) on the calling thread once the result is available, never + # before the call returns; err if rejected. + self.setup_async_auto_variants(args) + create_file('src.c', r''' +#include +#include +#include +#include +#include +#include + +void answer_callback(int ms, intptr_t value, void (*callback)(bool err, void* result, void* arg), void* arg); +void wait_ms_callback(int ms, void (*callback)(bool err, void* result, void* arg), void* arg); + +int stage; +int returned; + +void on_waited(bool err, void* result, void* arg) { + assert(stage++ == 3); + // A void function is called back with NULL. + assert(!err); + assert(result == NULL); + assert(arg == &stage); + printf("done\n"); + exit(0); +} + +void on_rejected(bool err, void* result, void* arg) { + assert(stage++ == 2); + assert(err); + assert(result == NULL); + assert(arg == &stage); + wait_ms_callback(5, on_waited, &stage); +} + +void on_fulfilled(bool err, void* result, void* arg) { + assert(stage++ == 1); + assert(!err); + assert((intptr_t)result == 42); + assert(arg == &stage); + // A rejected promise. + answer_callback(5, 0, on_rejected, &stage); +} + +void on_sync(bool err, void* result, void* arg) { + assert(stage++ == 0); + assert(returned); + assert(!err); + assert((intptr_t)result == 7); + assert(arg == &stage); + answer_callback(5, 42, on_fulfilled, &stage); +} + +int main() { + // A synchronous completion is still called back after the return. + answer_callback(0, 7, on_sync, &stage); + returned = 1; + return 0; +} +''') + self.do_runf('src.c', 'done\n', cflags=args + ['-sEXIT_RUNTIME', '--js-library', 'lib.js']) + def test_jslib_method_syntax(self): create_file('lib.js', r''' addToLibrary({ diff --git a/tools/native_sigs.py b/tools/native_sigs.py index f492039557e46..8bca914164ec7 100644 --- a/tools/native_sigs.py +++ b/tools/native_sigs.py @@ -533,6 +533,7 @@ '_emscripten_proxy_dlsync_async': '_pp', '_emscripten_run_callback_on_thread': '_pp_ppp', '_emscripten_run_js_on_main_thread': '__p_p_', + '_emscripten_run_js_on_main_thread_callback': '__p_ppp', '_emscripten_run_js_on_main_thread_done': '_pp__', '_emscripten_run_js_on_main_thread_promise': 'p_p_p', '_emscripten_set_offscreencanvas_size_on_thread': '_pp__',