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
15 changes: 10 additions & 5 deletions src/jsifier.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import assert from 'node:assert';
import * as fs from 'node:fs/promises';
import { isAsyncFunction } from 'node:util/types';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh.. interesting. TIL about this.

import {
ATMODULES,
ATEXITS,
Expand Down Expand Up @@ -337,7 +338,7 @@ return ${makeReturn64(await_ + body)};
return `\
${async_}function(${args}) {
${argConversions}
var ret = (() => { ${body} })();
var ret = (${async_}() => { ${body} })();
return ${makeReturn64(await_ + 'ret')};
}`;
}
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -775,7 +780,7 @@ function(${args}) {
}
contentText += `\n${mangled}.sig = '${sig}';`;
}
if (ASYNCIFY && isAsyncFunction) {
if (ASYNCIFY && hasAsyncDecorator) {
contentText += `\n${mangled}.isAsync = true;`;
}
if (isStub) {
Expand Down
8 changes: 4 additions & 4 deletions src/lib/libasync.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does flagging this async gain here? (it costs code size)

I understand the rationale for async is that one can await for the return value, but for functions that return void, isn't the async keyword dead code?

Though Asyncify and JSPI build modes itself do not need the functions to be flagged async do they? (in WebGPU JSPI support I went with this)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It really doesn't add anything here, but consistency. We have a few spots where we auto modify the library js (e.g. memory 64). It's much easier in those wrapper functions to assume we're in a async function and emit await code (also the await code is usually shorter).


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));
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/libcore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 + ')');
Expand Down
7 changes: 7 additions & 0 deletions src/lib/libemval.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,12 +402,19 @@ ${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);
});
},
#else
_emval_await: async (promise) => {
var value = await Emval.toValue(promise);
return Emval.toHandle(value);
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really get why this functions needs to looks so different in ASYNCIFY 1 vs 2.

In other places in this PR we have async (...) => Asyncify.handleSleep(..... but not here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, handleAsync currently works slightly differently on old asyncify and JSPI. On JSPI, it returns the promise and on asyncify it pipes the return value magically through the wakeup. Adding the async keyword here for asyncify=1 causes the test to fail.

I'd really like to unify all this code and have it so if you mark a library function with __async you don't have to use handleSleep or handleAsync, but that's a bigger change.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM

#endif
#endif

_emval_iter_begin__deps: ['$Emval'],
Expand Down
14 changes: 7 additions & 7 deletions src/lib/libidbstore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') }}};
Expand All @@ -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} */
Expand All @@ -119,15 +119,15 @@ 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') }}};
wakeUp();
});
}),
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') }}};
Expand All @@ -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') }}};
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/lib/libpromise.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/libsdl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)');
Expand Down
2 changes: 1 addition & 1 deletion src/lib/libwasi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions test/codesize/test_codesize_hello_dylink_all.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 0 additions & 2 deletions test/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
25 changes: 22 additions & 3 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -3242,10 +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']],
'': (['-sDYNAMIC_EXECUTION=1'],),
'no_dynamic': (['-sDYNAMIC_EXECUTION=0'],),
'dyncall': (['-sALLOW_MEMORY_GROWTH', '-sMAXIMUM_MEMORY=4GB'],),
})
@requires_jspi
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about also_with_wasm64 so that we get all 4 cases in wasm64 mode?

def test_embind_jspi(self, args):
Expand Down Expand Up @@ -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 <emscripten.h>
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']],
Expand Down