From be10d9866fed0a65f370b76db3fb953a7d1eca2a 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 627c740fe5c1c..fbf3ae3e9e196 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); } } @@ -676,6 +677,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) { @@ -770,7 +775,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 ef8c6d9893819..0349d671ac0fa 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -2619,7 +2619,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 a9f9b71d86be2..29eb5b00eb8d3 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5026,8 +5026,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 2089a7da5e79e..ca37e74f37fd5 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -3258,6 +3258,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): @@ -3496,6 +3497,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 0847c3f9ef232ef156d1944449a1fae60624d752 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 ca37e74f37fd5..a1db846a12836 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -3254,11 +3254,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): @@ -3512,8 +3512,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 262046785c014a5afb0a57240a760a8e8bedeb7d Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Mon, 6 Oct 2025 23:35:28 +0000 Subject: [PATCH 3/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 fbf3ae3e9e196..98b8c3c363d89 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 92b4a660f382947f020fd9abe8912f3d60497c3e Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Tue, 7 Oct 2025 18:17:53 +0000 Subject: [PATCH 4/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 a1db846a12836..f9276116fb667 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -3497,24 +3497,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']], @@ -5072,6 +5054,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. From 98c6feb0bd196e695dd2243f957f9016059531ef Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Tue, 14 Oct 2025 22:56:35 +0000 Subject: [PATCH 5/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: 843553 => 843558 [+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 aa27e63c7d228..8431703ac9eea 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": 245829, + "a.out.js": 245834, "a.out.nodebug.wasm": 597724, - "total": 843553, + "total": 843558, "sent": [ "IMG_Init", "IMG_Load",