Skip to content

Commit 7a0a060

Browse files
authored
[Promise] Add a C API for promises (#18598)
Add a new header, emscripten/promise.h, providing a C API for creating, resolving, chaining, and combining promises from C with a focus on providing the full expressive capability of JS promises. To provide maximum flexibility to users, promises can be fulfilled or rejected with arbitrary user-provided `void*` pointers as the value or reason, or they can be resolved by providing the handle of another promise whose eventual state should be matched. The callbacks provided to `emscripten_promise_then` additionally receive a context `data` pointer provided by the user as part of the `emscripten_promise_then` call. Remove the less expressive C API for creating promises we had in eventloop.h. That API allowed promises to be created and resolved, but was more complicated and less capable.
1 parent 00783ae commit 7a0a060

12 files changed

+626
-105
lines changed

src/library_eventloop.js

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -155,61 +155,6 @@ LibraryJSEventLoop = {
155155
{{{ runtimeKeepalivePop() }}}
156156
clearInterval(id);
157157
},
158-
159-
$promiseMap__deps: ['$HandleAllocator'],
160-
$promiseMap: "new HandleAllocator();",
161-
162-
// Create a new promise that can be resolved or rejected by passing a unique
163-
// ID to emscripten_promise_resolve/emscripten_promise_reject. Returns a JS
164-
// object containing the promise, its unique ID, and the associated
165-
// reject/resolve functions.
166-
$newNativePromise__deps: ['$promiseMap'],
167-
$newNativePromise: function(nativeFunc, userData) {
168-
var nativePromise = {};
169-
var promiseId;
170-
var promise = new Promise((resolve, reject) => {
171-
nativePromise.reject = reject;
172-
nativePromise.resolve = resolve;
173-
nativePromise.id = promiseMap.allocate(nativePromise);
174-
#if RUNTIME_DEBUG
175-
dbg('newNativePromise: ' + nativePromise.id);
176-
#endif
177-
nativeFunc(userData, nativePromise.id);
178-
});
179-
nativePromise.promise = promise;
180-
return nativePromise;
181-
},
182-
183-
$getPromise__deps: ['$promiseMap'],
184-
$getPromise: function(id) {
185-
return promiseMap.get(id).promise;
186-
},
187-
188-
emscripten_promise_create__sig: 'ipp',
189-
emscripten_promise_create__deps: ['$newNativePromise'],
190-
emscripten_promise_create: function(funcPtr, userData) {
191-
return newNativePromise({{{ makeDynCall('vpi', 'funcPtr') }}}, userData).id;
192-
},
193-
194-
emscripten_promise_resolve__deps: ['$promiseMap'],
195-
emscripten_promise_resolve__sig: 'vip',
196-
emscripten_promise_resolve: function(id, value) {
197-
#if RUNTIME_DEBUG
198-
err('emscripten_resolve_promise: ' + id);
199-
#endif
200-
promiseMap.get(id).resolve(value);
201-
promiseMap.free(id);
202-
},
203-
204-
emscripten_promise_reject__deps: ['$promiseMap'],
205-
emscripten_promise_reject__sig: 'vi',
206-
emscripten_promise_reject: function(id) {
207-
#if RUNTIME_DEBUG
208-
dbg('emscripten_promise_reject: ' + id);
209-
#endif
210-
promiseMap.get(id).reject();
211-
promiseMap.free(id);
212-
},
213158
};
214159

215160
mergeInto(LibraryManager.library, LibraryJSEventLoop);

src/library_promise.js

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/**
2+
* @license
3+
* Copyright 2023 The Emscripten Authors
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
7+
mergeInto(LibraryManager.library, {
8+
$promiseMap__deps: ['$HandleAllocator'],
9+
$promiseMap: "new HandleAllocator();",
10+
11+
$getPromise__deps: ['$promiseMap'],
12+
$getPromise: function(id) {
13+
return promiseMap.get(id).promise;
14+
},
15+
emscripten_promise_create__deps: ['$promiseMap'],
16+
emscripten_promise_create__sig: 'p',
17+
emscripten_promise_create: function() {
18+
var promiseInfo = {};
19+
promiseInfo.promise = new Promise((resolve, reject) => {
20+
promiseInfo.reject = reject;
21+
promiseInfo.resolve = resolve;
22+
});
23+
var id = promiseMap.allocate(promiseInfo);
24+
#if RUNTIME_DEBUG
25+
dbg('emscripten_promise_create: ' + id);
26+
#endif
27+
return id;
28+
},
29+
30+
emscripten_promise_destroy__deps: ['$promiseMap'],
31+
emscripten_promise_destroy__sig: 'vp',
32+
emscripten_promise_destroy: function(id) {
33+
#if RUNTIME_DEBUG
34+
dbg('emscripten_promise_destroy: ' + id);
35+
#endif
36+
promiseMap.free(id);
37+
},
38+
39+
emscripten_promise_resolve__deps: ['$promiseMap',
40+
'$getPromise',
41+
'emscripten_promise_destroy'],
42+
emscripten_promise_resolve__sig: 'vpip',
43+
emscripten_promise_resolve: function(id, result, value) {
44+
#if RUNTIME_DEBUG
45+
err('emscripten_promise_resolve: ' + id);
46+
#endif
47+
var info = promiseMap.get(id);
48+
switch (result) {
49+
case {{{ cDefine('EM_PROMISE_FULFILL') }}}:
50+
info.resolve(value);
51+
return;
52+
case {{{ cDefine('EM_PROMISE_MATCH') }}}:
53+
info.resolve(getPromise(value));
54+
return;
55+
case {{{ cDefine('EM_PROMISE_MATCH_RELEASE') }}}:
56+
info.resolve(getPromise(value));
57+
_emscripten_promise_destroy(value);
58+
return;
59+
case {{{ cDefine('EM_PROMISE_REJECT') }}}:
60+
info.reject(value);
61+
return;
62+
}
63+
#if ASSERTIONS
64+
abort("unexpected promise callback result " + result);
65+
#endif
66+
},
67+
68+
$makePromiseCallback__deps: ['$getPromise',
69+
'$POINTER_SIZE',
70+
'emscripten_promise_destroy'],
71+
$makePromiseCallback: function(callback, userData) {
72+
return (value) => {
73+
#if RUNTIME_DEBUG
74+
dbg('emscripten promise callback: ' + value);
75+
#endif
76+
var stack = stackSave();
77+
// Allocate space for the result value and initialize it to NULL.
78+
var resultPtr = stackAlloc(POINTER_SIZE);
79+
{{{ makeSetValue('resultPtr', 0, '0', '*') }}};
80+
try {
81+
var result =
82+
{{{ makeDynCall('ippp', 'callback') }}}(resultPtr, userData, value);
83+
var resultVal = {{{ makeGetValue('resultPtr', 0, '*') }}};
84+
} catch (e) {
85+
// If the thrown value is potentially a valid pointer, use it as the
86+
// rejection reason. Otherwise use a null pointer as the reason. If we
87+
// allow arbitrary objects to be thrown here, we will get a TypeError in
88+
// MEMORY64 mode when they are later converted to void* rejection
89+
// values.
90+
#if MEMORY64
91+
if (typeof e !== 'bigint') {
92+
throw 0n;
93+
}
94+
#else
95+
if (typeof e !== 'number') {
96+
throw 0;
97+
}
98+
#endif
99+
throw e;
100+
} finally {
101+
// Thrown errors will reject the promise, but at least we will restore
102+
// the stack first.
103+
stackRestore(stack);
104+
}
105+
switch (result) {
106+
case {{{ cDefine('EM_PROMISE_FULFILL') }}}:
107+
return resultVal;
108+
case {{{ cDefine('EM_PROMISE_MATCH') }}}:
109+
return getPromise(resultVal);
110+
case {{{ cDefine('EM_PROMISE_MATCH_RELEASE') }}}:
111+
var ret = getPromise(resultVal);
112+
_emscripten_promise_destroy(resultVal);
113+
return ret;
114+
case {{{ cDefine('EM_PROMISE_REJECT') }}}:
115+
throw resultVal;
116+
}
117+
#if ASSERTIONS
118+
abort("unexpected promise callback result " + result);
119+
#endif
120+
};
121+
},
122+
123+
emscripten_promise_then__deps: ['$promiseMap',
124+
'$getPromise',
125+
'$makePromiseCallback'],
126+
emscripten_promise_then__sig: 'ppppp',
127+
emscripten_promise_then: function(id,
128+
onFulfilled,
129+
onRejected,
130+
userData) {
131+
#if RUNTIME_DEBUG
132+
dbg('emscripten_promise_then: ' + id);
133+
#endif
134+
var promise = getPromise(id);
135+
var newId = promiseMap.allocate({
136+
promise: promise.then(makePromiseCallback(onFulfilled, userData),
137+
makePromiseCallback(onRejected, userData))
138+
});
139+
#if RUNTIME_DEBUG
140+
dbg('create: ' + newId);
141+
#endif
142+
return newId;
143+
},
144+
145+
emscripten_promise_all__deps: ['$promiseMap', '$getPromise'],
146+
emscripten_promise_all__sig: 'pppp',
147+
emscripten_promise_all: function(idBuf, resultBuf, size) {
148+
var promises = [];
149+
for (var i = 0; i < size; i++) {
150+
var id = {{{ makeGetValue('idBuf', `i*${Runtime.POINTER_SIZE}`, 'i32') }}};
151+
promises[i] = getPromise(id);
152+
}
153+
#if RUNTIME_DEBUG
154+
dbg('emscripten_promise_all: ' + promises);
155+
#endif
156+
var id = promiseMap.allocate({
157+
promise: Promise.all(promises).then((results) => {
158+
if (resultBuf) {
159+
for (var i = 0; i < size; i++) {
160+
var result = results[i];
161+
{{{ makeSetValue('resultBuf', `i*${Runtime.POINTER_SIZE}`, 'result', '*') }}};
162+
}
163+
}
164+
return resultBuf;
165+
})
166+
});
167+
#if RUNTIME_DEBUG
168+
dbg('create: ' + id);
169+
#endif
170+
return id;
171+
},
172+
});

src/modules.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ global.LibraryManager = {
5252
'library_dylink.js',
5353
'library_makeDynCall.js',
5454
'library_eventloop.js',
55+
'library_promise.js',
5556
];
5657

5758
if (LINK_AS_CXX) {

src/struct_info.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,15 @@
10321032
]
10331033
}
10341034
},
1035+
{
1036+
"file": "emscripten/promise.h",
1037+
"defines": [
1038+
"EM_PROMISE_FULFILL",
1039+
"EM_PROMISE_MATCH",
1040+
"EM_PROMISE_MATCH_RELEASE",
1041+
"EM_PROMISE_REJECT"
1042+
]
1043+
},
10351044
{
10361045
"file": "AL/al.h",
10371046
"defines": [

system/include/emscripten/eventloop.h

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ void emscripten_runtime_keepalive_push();
3030
void emscripten_runtime_keepalive_pop();
3131
EM_BOOL emscripten_runtime_keepalive_check();
3232

33-
int emscripten_promise_create(void (*start_async)(void* user_data, int promise_id), void* user_data);
34-
void emscripten_promise_resolve(int promise_id, void* value);
35-
void emscripten_promise_reject(int promise_id);
36-
3733
#ifdef __cplusplus
3834
}
3935
#endif

system/include/emscripten/promise.h

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2023 The Emscripten Authors. All rights reserved.
3+
* Emscripten is available under two separate licenses, the MIT license and the
4+
* University of Illinois/NCSA Open Source License. Both these licenses can be
5+
* found in the LICENSE file.
6+
*/
7+
8+
#pragma once
9+
10+
#include <stdlib.h>
11+
12+
#ifdef __cplusplus
13+
extern "C" {
14+
#endif
15+
16+
// EXPERIMENTAL AND SUBJECT TO CHANGE!
17+
18+
// An opaque handle to a JS Promise object.
19+
typedef struct _em_promise* em_promise_t;
20+
21+
typedef enum em_promise_result_t {
22+
EM_PROMISE_FULFILL,
23+
EM_PROMISE_MATCH,
24+
EM_PROMISE_MATCH_RELEASE,
25+
EM_PROMISE_REJECT,
26+
} em_promise_result_t;
27+
28+
// A callback passed to `emscripten_promise_then` to be invoked once a promise
29+
// is fulfilled or rejected. `data` is arbitrary user-provided data provided
30+
// when `emscripten_promise_then` is called to install the callback and `value`
31+
// is the value the promise was fulfilled or rejected with.
32+
//
33+
// The callback can signal how to resolve the new promise returned from
34+
// `emscripten_promise_then` via its return and by writing a new result to
35+
// outparam `result`. The behavior depends on the returned `em_promise_result_t`
36+
// value:
37+
//
38+
// - `EM_PROMISE_FULFILL`: The new promise is fulfilled with the value written
39+
// to `result` or NULL if no value is written.
40+
//
41+
// - `EM_PROMISE_MATCH` or `EM_PROMISE_MATCH_RELEASE`: The callback must write
42+
// a promise handle to `result` and the new promise is resolved to match the
43+
// eventual state of that promise. `EM_PROMISE_MATCH_RELEASE` will also cause
44+
// the written promise handle to be destroyed so that the user does not have
45+
// to arrange for it to be destroyed after the callback is executed.
46+
//
47+
// - `EM_PROMISE_REJECT`: The new promise is rejected with the reason written
48+
// to `result` or NULL if no reason is written.
49+
//
50+
// If the callback throws a number (or bigint in the case of memory64), the new
51+
// promise will be rejected with that number converted to a pointer as its
52+
// rejection reason. If the callback throws any other value, the new promise
53+
// will be rejected with a NULL rejection reason.
54+
typedef em_promise_result_t (*em_promise_callback_t)(void** result,
55+
void* data,
56+
void* value);
57+
58+
// Create a new promise that can be explicitly resolved or rejected using
59+
// `emscripten_promise_resolve`. The returned promise handle must eventually be
60+
// freed with `emscripten_promise_destroy`.
61+
__attribute__((warn_unused_result)) em_promise_t
62+
emscripten_promise_create(void);
63+
64+
// Release the resources associated with this promise. This must be called on
65+
// every promise handle created, whether by `emscripten_promise_create` or any
66+
// other function that returns a fresh promise, such as
67+
// `emscripten_promise_then`. It is fine to call `emscripten_promise_destroy` on
68+
// a promise handle before the promise is resolved; the configured callbacks
69+
// will still be called.
70+
void emscripten_promise_destroy(em_promise_t promise);
71+
72+
// Explicitly resolve the `promise` created by `emscripten_promise_create`. If
73+
// `result` is `EM_PROMISE_FULFILL`, then the promise is fulfilled with the
74+
// given `value`. If `result` is `EM_PROMISE_MATCH`, then the promise is
75+
// resolved to match the eventual state of `value` interpreted as a promise
76+
// handle. Finally, if `result` is `EM_PROMISE_REJECT`, then the promise is
77+
// rejected with the given value. Promises not created by
78+
// `emscripten_promise_create` should not be passed to this function.
79+
void emscripten_promise_resolve(em_promise_t promise,
80+
em_promise_result_t result,
81+
void* value);
82+
83+
// Install `on_fulfilled` and `on_rejected` callbacks on the given `promise`,
84+
// creating and returning a handle to a new promise. See `em_promise_callback_t`
85+
// for documentation on how the callbacks work. `data` is arbitrary user data
86+
// that will be passed to the callbacks. The returned promise handle must
87+
// eventually be freed with `emscripten_promise_destroy`.
88+
__attribute__((warn_unused_result)) em_promise_t
89+
emscripten_promise_then(em_promise_t promise,
90+
em_promise_callback_t on_fulfilled,
91+
em_promise_callback_t on_rejected,
92+
void* data);
93+
94+
// Call Promise.all to create and return a new promise that is either fulfilled
95+
// once the `num_promises` input promises in the `promises` have been fulfilled
96+
// or is rejected once any of the input promises has been rejected. When the
97+
// returned promise is fulfilled, the values each of the input promises were
98+
// resolved with will be written to the `results` array and the returned promise
99+
// will be fulfilled with the address of that array as well.
100+
__attribute__((warn_unused_result)) em_promise_t emscripten_promise_all(
101+
em_promise_t* promises, void** results, size_t num_promises);
102+
103+
// TODO: emscripten_promise_all_settled
104+
// TODO: emscripten_promise_race
105+
// TODO: emscripten_promise_any
106+
107+
#ifdef __cplusplus
108+
}
109+
#endif

0 commit comments

Comments
 (0)