From d6bb5ac2d198a97c351ca8c1c3d796b848d05875 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Fri, 3 Oct 2025 00:17:21 +0000 Subject: [PATCH 1/5] [jspi] Require async js functions when used with __async decorator. The `_emval_await` library function is marked `_emval_await__async: true`, but the js function is not async. With memory64 enabled we auto convert to bigint and look for the async keyword (which is missing) to apply the await before creating the BigInt. With my changes __async will require an async js function, which signals the function is used with JSPI and the appropriate awaits are then inserted. --- src/jsifier.mjs | 13 +++++++++---- src/lib/libasync.js | 8 ++++---- src/lib/libcore.js | 2 +- src/lib/libemval.js | 8 ++++++++ src/lib/libidbstore.js | 14 +++++++------- src/lib/libpromise.js | 2 +- src/lib/libsdl.js | 2 +- src/lib/libwasi.js | 2 +- test/test_browser.py | 2 -- test/test_other.py | 19 +++++++++++++++++++ 10 files changed, 51 insertions(+), 21 deletions(-) diff --git a/src/jsifier.mjs b/src/jsifier.mjs index 6d9f25560f09a..d96f09949a4e6 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -9,6 +9,7 @@ import assert from 'node:assert'; import * as fs from 'node:fs/promises'; +import { isAsyncFunction } from 'node:util/types'; import { ATMODULES, ATEXITS, @@ -540,13 +541,13 @@ function(${args}) { deps.push('setTempRet0'); } - let isAsyncFunction = false; + let hasAsyncDecorator = false; if (ASYNCIFY) { const original = LibraryManager.library[symbol]; if (typeof original == 'function') { - isAsyncFunction = LibraryManager.library[symbol + '__async']; + hasAsyncDecorator = LibraryManager.library[symbol + '__async']; } - if (isAsyncFunction) { + if (hasAsyncDecorator) { asyncFuncs.push(symbol); } } @@ -681,6 +682,10 @@ function(${args}) { snippet = stringifyWithFunctions(snippet); addImplicitDeps(snippet, deps); } else if (isFunction) { + if (ASYNCIFY == 2 && hasAsyncDecorator && !isAsyncFunction(snippet)) { + error(`'${symbol}' is marked with the __async decorator but is not an async JS function.`); + } + snippet = processLibraryFunction(snippet, symbol, mangled, deps, isStub); addImplicitDeps(snippet, deps); if (CHECK_DEPS && !isUserSymbol) { @@ -775,7 +780,7 @@ function(${args}) { } contentText += `\n${mangled}.sig = '${sig}';`; } - if (ASYNCIFY && isAsyncFunction) { + if (ASYNCIFY && hasAsyncDecorator) { contentText += `\n${mangled}.isAsync = true;`; } if (isStub) { diff --git a/src/lib/libasync.js b/src/lib/libasync.js index c79e4f6bfccbf..3eb5f288f5b9f 100644 --- a/src/lib/libasync.js +++ b/src/lib/libasync.js @@ -476,11 +476,11 @@ addToLibrary({ emscripten_sleep__deps: ['$safeSetTimeout'], emscripten_sleep__async: true, - emscripten_sleep: (ms) => Asyncify.handleSleep((wakeUp) => safeSetTimeout(wakeUp, ms)), + emscripten_sleep: async (ms) => Asyncify.handleSleep((wakeUp) => safeSetTimeout(wakeUp, ms)), emscripten_wget_data__deps: ['$asyncLoad', 'malloc'], emscripten_wget_data__async: true, - emscripten_wget_data: (url, pbuffer, pnum, perror) => Asyncify.handleAsync(async () => { + emscripten_wget_data: async (url, pbuffer, pnum, perror) => Asyncify.handleAsync(async () => { /* no need for run dependency, this is async but will not do any prepare etc. step */ try { const byteArray = await asyncLoad(UTF8ToString(url)); @@ -497,7 +497,7 @@ addToLibrary({ emscripten_scan_registers__deps: ['$safeSetTimeout'], emscripten_scan_registers__async: true, - emscripten_scan_registers: (func) => { + emscripten_scan_registers: async (func) => { return Asyncify.handleSleep((wakeUp) => { // We must first unwind, so things are spilled to the stack. Then while // we are pausing we do the actual scan. After that we can resume. Note @@ -585,7 +585,7 @@ addToLibrary({ emscripten_fiber_swap__deps: ["$Asyncify", "$Fibers", '$stackSave'], emscripten_fiber_swap__async: true, - emscripten_fiber_swap: (oldFiber, newFiber) => { + emscripten_fiber_swap: async (oldFiber, newFiber) => { if (ABORT) return; #if ASYNCIFY_DEBUG dbg('ASYNCIFY/FIBER: swap', oldFiber, '->', newFiber, 'state:', Asyncify.state); diff --git a/src/lib/libcore.js b/src/lib/libcore.js index 9c2f828c43920..8fa000cf2eb03 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -2622,7 +2622,7 @@ function wrapSyscallFunction(x, library, isWasi) { post = handler + post; if (pre || post) { - t = modifyJSFunction(t, (args, body) => `function (${args}) {\n${pre}${body}${post}}\n`); + t = modifyJSFunction(t, (args, body, async_) => `${async_} function (${args}) {\n${pre}${body}${post}}\n`); } library[x] = eval('(' + t + ')'); diff --git a/src/lib/libemval.js b/src/lib/libemval.js index e9e8553cd49d4..69a4dd8c5b85e 100644 --- a/src/lib/libemval.js +++ b/src/lib/libemval.js @@ -402,12 +402,20 @@ ${functionBody} #if ASYNCIFY _emval_await__deps: ['$Emval', '$Asyncify'], _emval_await__async: true, +#if ASYNCIFY == 1 _emval_await: (promise) => { return Asyncify.handleAsync(async () => { var value = await Emval.toValue(promise); return Emval.toHandle(value); }); }, +#endif +#if ASYNCIFY == 2 + _emval_await: async (promise) => { + var value = await Emval.toValue(promise); + return Emval.toHandle(value); + }, +#endif #endif _emval_iter_begin__deps: ['$Emval'], diff --git a/src/lib/libidbstore.js b/src/lib/libidbstore.js index 224d1d1d434a8..a66809290f7c0 100644 --- a/src/lib/libidbstore.js +++ b/src/lib/libidbstore.js @@ -94,7 +94,7 @@ var LibraryIDBStore = { #if ASYNCIFY emscripten_idb_load__async: true, emscripten_idb_load__deps: ['malloc'], - emscripten_idb_load: (db, id, pbuffer, pnum, perror) => Asyncify.handleSleep((wakeUp) => { + emscripten_idb_load: async (db, id, pbuffer, pnum, perror) => Asyncify.handleSleep((wakeUp) => { IDBStore.getFile(UTF8ToString(db), UTF8ToString(id), (error, byteArray) => { if (error) { {{{ makeSetValue('perror', 0, '1', 'i32') }}}; @@ -110,7 +110,7 @@ var LibraryIDBStore = { }); }), emscripten_idb_store__async: true, - emscripten_idb_store: (db, id, ptr, num, perror) => Asyncify.handleSleep((wakeUp) => { + emscripten_idb_store: async (db, id, ptr, num, perror) => Asyncify.handleSleep((wakeUp) => { IDBStore.setFile(UTF8ToString(db), UTF8ToString(id), new Uint8Array(HEAPU8.subarray(ptr, ptr+num)), (error) => { // Closure warns about storing booleans in TypedArrays. /** @suppress{checkTypes} */ @@ -119,7 +119,7 @@ var LibraryIDBStore = { }); }), emscripten_idb_delete__async: true, - emscripten_idb_delete: (db, id, perror) => Asyncify.handleSleep((wakeUp) => { + emscripten_idb_delete: async (db, id, perror) => Asyncify.handleSleep((wakeUp) => { IDBStore.deleteFile(UTF8ToString(db), UTF8ToString(id), (error) => { /** @suppress{checkTypes} */ {{{ makeSetValue('perror', 0, '!!error', 'i32') }}}; @@ -127,7 +127,7 @@ var LibraryIDBStore = { }); }), emscripten_idb_exists__async: true, - emscripten_idb_exists: (db, id, pexists, perror) => Asyncify.handleSleep((wakeUp) => { + emscripten_idb_exists: async (db, id, pexists, perror) => Asyncify.handleSleep((wakeUp) => { IDBStore.existsFile(UTF8ToString(db), UTF8ToString(id), (error, exists) => { /** @suppress{checkTypes} */ {{{ makeSetValue('pexists', 0, '!!exists', 'i32') }}}; @@ -137,7 +137,7 @@ var LibraryIDBStore = { }); }), emscripten_idb_clear__async: true, - emscripten_idb_clear: (db, perror) => Asyncify.handleSleep((wakeUp) => { + emscripten_idb_clear: async (db, perror) => Asyncify.handleSleep((wakeUp) => { IDBStore.clearStore(UTF8ToString(db), (error) => { /** @suppress{checkTypes} */ {{{ makeSetValue('perror', 0, '!!error', 'i32') }}}; @@ -146,7 +146,7 @@ var LibraryIDBStore = { }), // extra worker methods - proxied emscripten_idb_load_blob__async: true, - emscripten_idb_load_blob: (db, id, pblob, perror) => Asyncify.handleSleep((wakeUp) => { + emscripten_idb_load_blob: async (db, id, pblob, perror) => Asyncify.handleSleep((wakeUp) => { #if ASSERTIONS assert(!IDBStore.pending); #endif @@ -174,7 +174,7 @@ var LibraryIDBStore = { }); }), emscripten_idb_store_blob__async: true, - emscripten_idb_store_blob: (db, id, ptr, num, perror) => Asyncify.handleSleep((wakeUp) => { + emscripten_idb_store_blob: async (db, id, ptr, num, perror) => Asyncify.handleSleep((wakeUp) => { #if ASSERTIONS assert(!IDBStore.pending); #endif diff --git a/src/lib/libpromise.js b/src/lib/libpromise.js index db4838e09cc22..0be6872d09c52 100644 --- a/src/lib/libpromise.js +++ b/src/lib/libpromise.js @@ -261,7 +261,7 @@ addToLibrary({ #if ASYNCIFY emscripten_promise_await__deps: ['$getPromise', '$setPromiseResult'], #endif - emscripten_promise_await: (returnValuePtr, id) => { + emscripten_promise_await: async (returnValuePtr, id) => { #if ASYNCIFY #if RUNTIME_DEBUG dbg(`emscripten_promise_await: ${id}`); diff --git a/src/lib/libsdl.js b/src/lib/libsdl.js index c810a2c5e0302..f380329b0dec8 100644 --- a/src/lib/libsdl.js +++ b/src/lib/libsdl.js @@ -1746,7 +1746,7 @@ var LibrarySDL = { #if ASYNCIFY SDL_Delay__deps: ['emscripten_sleep'], SDL_Delay__async: true, - SDL_Delay: (delay) => _emscripten_sleep(delay), + SDL_Delay: async (delay) => _emscripten_sleep(delay), #else SDL_Delay: (delay) => { if (!ENVIRONMENT_IS_WORKER) abort('SDL_Delay called on the main thread! Potential infinite loop, quitting. (consider building with async support like ASYNCIFY)'); diff --git a/src/lib/libwasi.js b/src/lib/libwasi.js index 2d3b143f0969a..316a189ba9c38 100644 --- a/src/lib/libwasi.js +++ b/src/lib/libwasi.js @@ -532,7 +532,7 @@ var WasiLibrary = { return 0; }, - fd_sync: (fd) => { + fd_sync: {{{ asyncIf(ASYNCIFY) }}} (fd) => { #if SYSCALLS_REQUIRE_FILESYSTEM var stream = SYSCALLS.getStreamFromFD(fd); #if ASYNCIFY diff --git a/test/test_browser.py b/test/test_browser.py index 7b8a868ada5f6..923c86b883053 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -4982,8 +4982,6 @@ def test_embind_with_pthreads(self): def test_embind(self, args): if is_jspi(args) and not is_chrome(): self.skipTest(f'Current browser ({common.EMTEST_BROWSER}) does not support JSPI. Only chromium-based browsers ({CHROMIUM_BASED_BROWSERS}) support JSPI today.') - if is_jspi(args) and self.is_wasm64(): - self.skipTest('_emval_await fails') self.btest('embind_with_asyncify.cpp', '1', cflags=['-lembind'] + args) diff --git a/test/test_other.py b/test/test_other.py index 8caf035a2f2b6..3805ddcd7a938 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -3246,6 +3246,7 @@ def test_embind_asyncify(self): '': [['-sDYNAMIC_EXECUTION=1']], 'no_dynamic': [['-sDYNAMIC_EXECUTION=0']], 'dyncall': [['-sALLOW_MEMORY_GROWTH', '-sMAXIMUM_MEMORY=4GB']], + 'wasm64': (['-sMEMORY64'],), }) @requires_jspi def test_embind_jspi(self, args): @@ -3484,6 +3485,24 @@ def test_jspi_async_function(self): '-Wno-experimental', '--post-js=post.js']) + @requires_jspi + def test_jspi_bad_library_function(self): + create_file('lib.js', r''' + addToLibrary({ + foo__async: true, + foo: function(f) {}, + }); + ''') + create_file('main.c', r''' + #include + extern void foo(); + EMSCRIPTEN_KEEPALIVE void test() { + foo(); + } + ''') + err = self.expect_fail([EMCC, 'main.c', '-o', 'out.js', '-sJSPI', '--js-library=lib.js', '-Wno-experimental',]) + self.assertContained('error: foo is marked with the __async decorator but is not an async JS function.', err) + @requires_dev_dependency('typescript') @parameterized({ 'commonjs': [['-sMODULARIZE'], ['--module', 'commonjs', '--moduleResolution', 'node']], From 9244de844abf787129ceb8c76fa10c8c5407d5a3 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Mon, 6 Oct 2025 21:37:59 +0000 Subject: [PATCH 2/5] comments --- src/lib/libemval.js | 3 +-- test/test_other.py | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/lib/libemval.js b/src/lib/libemval.js index 69a4dd8c5b85e..708f8130949a7 100644 --- a/src/lib/libemval.js +++ b/src/lib/libemval.js @@ -409,8 +409,7 @@ ${functionBody} return Emval.toHandle(value); }); }, -#endif -#if ASYNCIFY == 2 +#else _emval_await: async (promise) => { var value = await Emval.toValue(promise); return Emval.toHandle(value); diff --git a/test/test_other.py b/test/test_other.py index 3805ddcd7a938..84cea3194ed02 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -3242,11 +3242,11 @@ def test_embind_asyncify(self): ''') self.do_runf('main.cpp', 'done', cflags=['-lembind', '-sASYNCIFY', '--post-js', 'post.js']) + @also_with_wasm64 @parameterized({ - '': [['-sDYNAMIC_EXECUTION=1']], - 'no_dynamic': [['-sDYNAMIC_EXECUTION=0']], - 'dyncall': [['-sALLOW_MEMORY_GROWTH', '-sMAXIMUM_MEMORY=4GB']], - 'wasm64': (['-sMEMORY64'],), + '': (['-sDYNAMIC_EXECUTION=1'],), + 'no_dynamic': (['-sDYNAMIC_EXECUTION=0'],), + 'dyncall': (['-sALLOW_MEMORY_GROWTH', '-sMAXIMUM_MEMORY=4GB'],), }) @requires_jspi def test_embind_jspi(self, args): @@ -3500,8 +3500,8 @@ def test_jspi_bad_library_function(self): foo(); } ''') - err = self.expect_fail([EMCC, 'main.c', '-o', 'out.js', '-sJSPI', '--js-library=lib.js', '-Wno-experimental',]) - self.assertContained('error: foo is marked with the __async decorator but is not an async JS function.', err) + err = self.expect_fail([EMCC, 'main.c', '-o', 'out.js', '-sJSPI', '--js-library=lib.js', '-Wno-experimental']) + self.assertContained('error: \'foo\' is marked with the __async decorator but is not an async JS function.', err) @requires_dev_dependency('typescript') @parameterized({ From c8e1794a0e4d9e841581bb0427ca15ac192bb392 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Mon, 6 Oct 2025 22:05:46 +0000 Subject: [PATCH 3/5] Automatic rebaseline of codesize expectations. NFC This is an automatic change generated by tools/maint/rebaseline_tests.py. The following (1) test expectation files were updated by running the tests with `--rebaseline`: ``` codesize/test_codesize_hello_dylink_all.json: 843587 => 843592 [+5 bytes / +0.00%] Average change: +0.00% (+0.00% - +0.00%) ``` --- test/codesize/test_codesize_hello_dylink_all.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index f07e4dfa5ee9d..346c9fb9c43b9 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 245841, + "a.out.js": 245846, "a.out.nodebug.wasm": 597746, - "total": 843587, + "total": 843592, "sent": [ "IMG_Init", "IMG_Load", From 13da1e5eed80fb0e8beea9dfd74478f8ca3bc4b7 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Mon, 6 Oct 2025 23:35:28 +0000 Subject: [PATCH 4/5] Add missing async. --- src/jsifier.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsifier.mjs b/src/jsifier.mjs index d96f09949a4e6..f9738f9e88948 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -338,7 +338,7 @@ return ${makeReturn64(await_ + body)}; return `\ ${async_}function(${args}) { ${argConversions} -var ret = (() => { ${body} })(); +var ret = (${async_}() => { ${body} })(); return ${makeReturn64(await_ + 'ret')}; }`; } From 233ee6423a90cd7047cd71c857828bd210fa0075 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Tue, 7 Oct 2025 18:17:53 +0000 Subject: [PATCH 5/5] move test --- test/test_other.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index 84cea3194ed02..cb1afa722a2f4 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -3485,24 +3485,6 @@ def test_jspi_async_function(self): '-Wno-experimental', '--post-js=post.js']) - @requires_jspi - def test_jspi_bad_library_function(self): - create_file('lib.js', r''' - addToLibrary({ - foo__async: true, - foo: function(f) {}, - }); - ''') - create_file('main.c', r''' - #include - extern void foo(); - EMSCRIPTEN_KEEPALIVE void test() { - foo(); - } - ''') - err = self.expect_fail([EMCC, 'main.c', '-o', 'out.js', '-sJSPI', '--js-library=lib.js', '-Wno-experimental']) - self.assertContained('error: \'foo\' is marked with the __async decorator but is not an async JS function.', err) - @requires_dev_dependency('typescript') @parameterized({ 'commonjs': [['-sMODULARIZE'], ['--module', 'commonjs', '--moduleResolution', 'node']], @@ -5031,6 +5013,24 @@ def test_jslib_system_lib_name(self): ''') self.do_runf('src.c', 'jslibfunc: 12', cflags=['--js-library', 'libcore.js']) + @requires_jspi + def test_jslib_jspi_missing_async(self): + create_file('lib.js', r''' + addToLibrary({ + foo__async: true, + foo: function(f) {}, + }); + ''') + create_file('main.c', r''' + #include + extern void foo(); + EMSCRIPTEN_KEEPALIVE void test() { + foo(); + } + ''') + err = self.expect_fail([EMCC, 'main.c', '-o', 'out.js', '-sJSPI', '--js-library=lib.js', '-Wno-experimental']) + self.assertContained('error: \'foo\' is marked with the __async decorator but is not an async JS function.', err) + def test_EMCC_BUILD_DIR(self): # EMCC_BUILD_DIR was necessary in the past since we used to force the cwd to be src/ for # technical reasons.