Skip to content

Commit 7b458fb

Browse files
authored
Add support for the new JavaScript promise integration API. (#17724)
- Enables the new "jspi" pass in binaryen. - Wraps all imports/exports in a suspending/promising. WebAssembly.Function. This currently wraps everything, but we could make it only wrap async functions as needed.
1 parent fff3775 commit 7b458fb

File tree

5 files changed

+44
-29
lines changed

5 files changed

+44
-29
lines changed

emcc.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,8 @@ def check_human_readable_list(items):
600600
if settings.ASYNCIFY_ONLY:
601601
check_human_readable_list(settings.ASYNCIFY_ONLY)
602602
passes += ['--pass-arg=asyncify-onlylist@%s' % ','.join(settings.ASYNCIFY_ONLY)]
603+
elif settings.ASYNCIFY == 2:
604+
passes += ['--jspi']
603605
if settings.BINARYEN_IGNORE_IMPLICIT_TRAPS:
604606
passes += ['--ignore-implicit-traps']
605607
# normally we can assume the memory, if imported, has not been modified

emscripten.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ def update_settings_glue(wasm_file, metadata):
141141

142142
# start with the MVP features, and add any detected features.
143143
settings.BINARYEN_FEATURES = ['--mvp-features'] + metadata['features']
144+
if settings.ASYNCIFY == 2:
145+
settings.BINARYEN_FEATURES += ['--enable-reference-types']
146+
144147
if settings.USE_PTHREADS:
145148
assert '--enable-threads' in settings.BINARYEN_FEATURES
146149
if settings.MEMORY64:

src/library_async.js

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,6 @@ mergeInto(LibraryManager.library, {
5757
// malloc() asyncify does (we could ifdef it out, but it's even more
5858
// ifdefing).
5959

60-
// The global suspender object used with the VM's stack switching Promise
61-
// API.
62-
suspender: null,
6360
// The promise that is being suspended on in the VM atm, or null.
6461
promise: null,
6562
// The function we should call to resolve the promise at the right time.
@@ -79,11 +76,6 @@ mergeInto(LibraryManager.library, {
7976
instrumentWasmImports: function(imports) {
8077
#if ASYNCIFY_DEBUG
8178
err('asyncify instrumenting imports');
82-
#endif
83-
#if ASYNCIFY == 2
84-
// TODO we could perhaps add an init function and put this there, but
85-
// this should work for now.
86-
Asyncify.suspender = new WebAssembly.Suspender();
8779
#endif
8880
var ASYNCIFY_IMPORTS = {{{ JSON.stringify(ASYNCIFY_IMPORTS) }}}.map((x) => x.split('.')[1]);
8981
for (var x in imports) {
@@ -94,24 +86,23 @@ mergeInto(LibraryManager.library, {
9486
var isAsyncifyImport = ASYNCIFY_IMPORTS.indexOf(x) >= 0 ||
9587
x.startsWith('__asyncjs__');
9688
#if ASYNCIFY == 2
97-
if (isAsyncifyImport) {
89+
// Wrap all imports with a suspending WebAssembly function.
90+
// TODO: wrap only async functions.
9891
#if ASSERTIONS
99-
if (!sig) {
100-
throw new Error('Missing __sig for ' + x);
101-
}
92+
assert(sig, 'Missing __sig for ' + x);
10293
#endif
103-
var type = sigToWasmTypes(sig, original);
104-
// Regardless of the original result type of the function, as it
105-
// is now expected to potentially return a Promise, change it to
106-
// an externref.
107-
type.results = ['externref'];
94+
var type = sigToWasmTypes(sig, original);
10895
#if ASYNCIFY_DEBUG
109-
err('asyncify: suspendOnReturnedPromise for', x, original);
96+
err('asyncify: suspendOnReturnedPromise for', x, original);
11097
#endif
111-
imports[x] = original = Asyncify.suspender.suspendOnReturnedPromise(
112-
new WebAssembly.Function(type, original)
113-
);
114-
}
98+
// Add space for the suspender promise that will be used in the
99+
// Wasm wrapper function.
100+
type.parameters.unshift('externref');
101+
imports[x] = original = new WebAssembly.Function(
102+
type,
103+
original,
104+
{ suspending: 'first' }
105+
);
115106
#endif
116107
#if ASSERTIONS && ASYNCIFY != 2 // We cannot apply assertions with stack switching, as the imports must not be modified from suspender.suspendOnReturnedPromise TODO find a way
117108
imports[x] = function() {
@@ -159,16 +150,26 @@ mergeInto(LibraryManager.library, {
159150
for (var x in exports) {
160151
(function(x) {
161152
var original = exports[x];
153+
if (typeof original == 'function') {
162154
#if ASYNCIFY == 2
163-
// TODO: need a list of all suspending exports.
164-
if (x === 'main') {
155+
// Wrap all exports with a promising WebAssembly function.
156+
// TODO: wrap only async functions.
165157
#if ASYNCIFY_DEBUG
166158
err('asyncify: returnPromiseOnSuspend for', x, original);
167159
#endif
168-
ret[x] = original = Asyncify.suspender.returnPromiseOnSuspend(original);
169-
}
160+
var type = WebAssembly.Function.type(original);
161+
var parameters = type.parameters;
162+
var results = type.results;
163+
#if ASSERTIONS
164+
assert(results.length !== 0, 'There must be a return result')
165+
#endif
166+
// Remove the extern ref;
167+
parameters.shift();
168+
original = new WebAssembly.Function(
169+
{ parameters , results: ['externref'] },
170+
original,
171+
{ promising : 'first' });
170172
#endif
171-
if (typeof original == 'function') {
172173
ret[x] = function() {
173174
#if ASYNCIFY_DEBUG >= 2
174175
err('ASYNCIFY: ' + ' '.repeat(Asyncify.exportCallStack.length) + ' try ' + x);

src/postamble.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,20 @@ function callMain(args) {
180180
#if ASSERTIONS
181181
assert(ret == 0, '_emscripten_proxy_main failed to start proxy thread: ' + ret);
182182
#endif
183+
#else
184+
#if ASYNCIFY == 2
185+
// The current spec of JSPI returns a promise only if the function suspends
186+
// and a plain value otherwise. This will likely change:
187+
// https://github.com/WebAssembly/js-promise-integration/issues/11
188+
Promise.resolve(ret).then((result) => {
189+
exitJS(result, /* implicit = */ true);
190+
}).catch((e) => {
191+
handleException(e);
192+
});
183193
#else
184194
// if we're not running an evented main loop, it's time to exit
185195
exitJS(ret, /* implicit = */ true);
196+
#endif // ASYNCIFY == 2
186197
return ret;
187198
}
188199
catch (e) {

test/test_core.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8012,8 +8012,6 @@ def test_vswprintf_utf8(self):
80128012
@no_wasm64('TODO: asyncify for wasm64')
80138013
@with_asyncify_and_stack_switching
80148014
def test_async_hello(self):
8015-
if self.get_setting('ASYNCIFY') == 2:
8016-
self.skipTest('https://github.com/emscripten-core/emscripten/issues/17532')
80178015
# needs to flush stdio streams
80188016
self.set_setting('EXIT_RUNTIME')
80198017

0 commit comments

Comments
 (0)