Skip to content

Commit 8ecbdb3

Browse files
authored
C++20 co_await support for Embind promises (#20420)
1 parent 3587fbc commit 8ecbdb3

File tree

9 files changed

+252
-12
lines changed

9 files changed

+252
-12
lines changed

ChangeLog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ See docs/process.md for more on how version tagging works.
4141
sidestep some of the issues with legacy cmd.exe, but developers must
4242
explicitly opt-in to running PowerShell scripts in system settings or
4343
via the `Set-ExecutionPolicy` command. (#20416)
44+
- `emscripten::val` now supports C++20 `co_await` operator for JavaScript
45+
`Promise`s. (#20420)
4446

4547
3.1.47 - 10/09/23
4648
-----------------

emscripten.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,7 @@ def create_pointer_conversion_wrappers(metadata):
944944
'stbi_load_from_memory': 'pp_ppp_',
945945
'emscripten_proxy_finish': '_p',
946946
'emscripten_proxy_execute_queue': '_p',
947+
'_emval_coro_resume': '_pp',
947948
}
948949

949950
for function in settings.SIGNATURE_CONVERSIONS:

site/source/docs/api_reference/val.h.rst

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ Guide material for this class can be found in :ref:`embind-val-guide`.
4646
}
4747
4848
See :ref:`embind-val-guide` for other examples.
49-
49+
5050

5151
.. warning:: JavaScript values can't be shared across threads, so neither can ``val`` instances that bind them.
52-
52+
5353
For example, if you want to cache some JavaScript global as a ``val``, you need to retrieve and bind separate instances of that global by its name in each thread.
5454
The easiest way to do this is with a ``thread_local`` declaration:
5555

@@ -108,11 +108,11 @@ Guide material for this class can be found in :ref:`embind-val-guide`.
108108

109109
.. _val_as_handle:
110110
.. cpp:function:: EM_VAL as_handle() const
111-
111+
112112
Returns a raw handle representing this ``val``. This can be used for
113113
passing raw value handles to JavaScript and retrieving the values on the
114114
other side via ``Emval.toValue`` function. Example:
115-
115+
116116
.. code:: cpp
117117
118118
EM_JS(void, log_value, (EM_VAL val_handle), {
@@ -130,16 +130,16 @@ Guide material for this class can be found in :ref:`embind-val-guide`.
130130
from JavaScript, where the JavaScript side should wrap a value with
131131
``Emval.toHandle``, pass it to C++, and then C++ can use ``take_ownership``
132132
to convert it to a ``val`` instance. Example:
133-
133+
134134
.. code:: cpp
135-
135+
136136
EM_ASYNC_JS(EM_VAL, fetch_json_from_url, (const char *url_ptr), {
137137
var url = UTF8ToString(url);
138138
var response = await fetch(url);
139139
var json = await response.json();
140140
return Emval.toHandle(json);
141141
});
142-
142+
143143
val obj = val::take_ownership(fetch_json_from_url("https://httpbin.org/json"));
144144
std::string author = obj["slideshow"]["author"].as<std::string>();
145145
@@ -169,12 +169,12 @@ Guide material for this class can be found in :ref:`embind-val-guide`.
169169

170170

171171
.. cpp:function:: val(val&& v)
172-
172+
173173
Moves ownership of a value to a new ``val`` instance.
174174

175175

176176
.. cpp:function:: val(const val& v)
177-
177+
178178
Creates another reference to the same value behind the provided ``val`` instance.
179179

180180

@@ -184,7 +184,7 @@ Guide material for this class can be found in :ref:`embind-val-guide`.
184184

185185

186186
.. cpp:function:: val& operator=(val&& v)
187-
187+
188188
Removes a reference to the currently bound value and takes over the provided one.
189189

190190

@@ -217,7 +217,7 @@ Guide material for this class can be found in :ref:`embind-val-guide`.
217217

218218

219219
.. cpp:function:: val operator()(Args&&... args) const
220-
220+
221221
Assumes that current value is a function, and invokes it with provided arguments.
222222

223223

@@ -262,6 +262,42 @@ Guide material for this class can be found in :ref:`embind-val-guide`.
262262

263263
.. note:: This method requires :ref:`Asyncify` to be enabled.
264264

265+
.. cpp:function:: val operator co_await() const
266+
267+
The ``co_await`` operator allows awaiting JavaScript promises represented by ``val``.
268+
269+
It's compatible with any C++20 coroutines, but should be normally used inside
270+
a ``val``-returning coroutine which will also become a ``Promise``.
271+
272+
For example, it allows you to implement the equivalent of this JavaScript ``async``/``await`` function:
273+
274+
.. code:: javascript
275+
276+
async function foo() {
277+
const response = await fetch("http://url");
278+
const json = await response.json();
279+
return json;
280+
}
281+
282+
export { foo };
283+
284+
as a C++ coroutine:
285+
286+
.. code:: cpp
287+
288+
val foo() {
289+
val response = co_await val::global("fetch")(std::string("http://url"));
290+
val json = co_await response.call<val>("json");
291+
return json;
292+
}
293+
294+
EMSCRIPTEN_BINDINGS(module) {
295+
function("foo", &foo);
296+
}
297+
298+
Unlike the ``await()`` method, it doesn't need Asyncify as it uses native C++ coroutine transform.
299+
300+
:returns: A ``val`` representing the fulfilled value of this promise.
265301

266302
.. cpp:type: EMSCRIPTEN_SYMBOL(name)
267303

src/embind/emval.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,35 @@ var LibraryEmVal = {
462462
var result = iterator.next();
463463
return result.done ? 0 : Emval.toHandle(result.value);
464464
},
465+
466+
_emval_coro_suspend__deps: ['$Emval', '_emval_coro_resume'],
467+
_emval_coro_suspend: (promiseHandle, awaiterPtr) => {
468+
Emval.toValue(promiseHandle).then(result => {
469+
__emval_coro_resume(awaiterPtr, Emval.toHandle(result));
470+
});
471+
},
472+
473+
_emval_coro_make_promise__deps: ['$Emval', '__cxa_rethrow'],
474+
_emval_coro_make_promise: (resolveHandlePtr, rejectHandlePtr) => {
475+
return Emval.toHandle(new Promise((resolve, reject) => {
476+
const rejectWithCurrentException = () => {
477+
try {
478+
// Use __cxa_rethrow which already has mechanism for generating
479+
// user-friendly error message and stacktrace from C++ exception
480+
// if EXCEPTION_STACK_TRACES is enabled and numeric exception
481+
// with metadata optimised out otherwise.
482+
___cxa_rethrow();
483+
} catch (e) {
484+
// But catch it so that it rejects the promise instead of throwing
485+
// in an unpredictable place during async execution.
486+
reject(e);
487+
}
488+
};
489+
490+
{{{ makeSetValue('resolveHandlePtr', '0', 'Emval.toHandle(resolve)', '*') }}};
491+
{{{ makeSetValue('rejectHandlePtr', '0', 'Emval.toHandle(rejectWithCurrentException)', '*') }}};
492+
}));
493+
},
465494
};
466495

467496
addToLibrary(LibraryEmVal);

src/library_sigs.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,8 @@ sigs = {
339339
_emval_await__sig: 'pp',
340340
_emval_call__sig: 'dpppp',
341341
_emval_call_method__sig: 'dppppp',
342+
_emval_coro_make_promise__sig: 'ppp',
343+
_emval_coro_suspend__sig: 'vpp',
342344
_emval_decref__sig: 'vp',
343345
_emval_delete__sig: 'ipp',
344346
_emval_equals__sig: 'ipp',

system/include/emscripten/val.h

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
#include <cstdint> // uintptr_t
1919
#include <vector>
2020
#include <type_traits>
21+
#if _LIBCPP_STD_VER >= 20
22+
#include <coroutine>
23+
#include <variant>
24+
#endif
2125

2226

2327
namespace emscripten {
@@ -110,6 +114,11 @@ EM_VAL _emval_await(EM_VAL promise);
110114
EM_VAL _emval_iter_begin(EM_VAL iterable);
111115
EM_VAL _emval_iter_next(EM_VAL iterator);
112116

117+
#if _LIBCPP_STD_VER >= 20
118+
void _emval_coro_suspend(EM_VAL promise, void* coro_ptr);
119+
EM_VAL _emval_coro_make_promise(EM_VAL *resolve, EM_VAL *reject);
120+
#endif
121+
113122
} // extern "C"
114123

115124
template<const char* address>
@@ -586,6 +595,10 @@ class val {
586595
// our iterators are sentinel-based range iterators; use nullptr as the end sentinel
587596
constexpr nullptr_t end() const { return nullptr; }
588597

598+
#if _LIBCPP_STD_VER >= 20
599+
struct promise_type;
600+
#endif
601+
589602
private:
590603
// takes ownership, assumes handle already incref'd and lives on the same thread
591604
explicit val(EM_VAL handle)
@@ -646,6 +659,104 @@ inline val::iterator val::begin() const {
646659
return iterator(*this);
647660
}
648661

662+
#if _LIBCPP_STD_VER >= 20
663+
namespace internal {
664+
// Awaiter defines a set of well-known methods that compiler uses
665+
// to drive the argument of the `co_await` operator (regardless
666+
// of the type of the parent coroutine).
667+
// This one is used for Promises represented by the `val` type.
668+
class val_awaiter {
669+
// State machine holding awaiter's current state. One of:
670+
// - initially created with promise
671+
// - waiting with a given coroutine handle
672+
// - completed with a result
673+
std::variant<val, std::coroutine_handle<val::promise_type>, val> state;
674+
675+
constexpr static std::size_t STATE_PROMISE = 0;
676+
constexpr static std::size_t STATE_CORO = 1;
677+
constexpr static std::size_t STATE_RESULT = 2;
678+
679+
public:
680+
val_awaiter(val&& promise)
681+
: state(std::in_place_index<STATE_PROMISE>, std::move(promise)) {}
682+
683+
// just in case, ensure nobody moves / copies this type around
684+
val_awaiter(val_awaiter&&) = delete;
685+
686+
// Promises don't have a synchronously accessible "ready" state.
687+
bool await_ready() { return false; }
688+
689+
// On suspend, store the coroutine handle and invoke a helper that will do
690+
// a rough equivalent of `promise.then(value => this.resume_with(value))`.
691+
void await_suspend(std::coroutine_handle<val::promise_type> handle) {
692+
internal::_emval_coro_suspend(std::get<STATE_PROMISE>(state).as_handle(), this);
693+
state.emplace<STATE_CORO>(handle);
694+
}
695+
696+
// When JS invokes `resume_with` with some value, store that value and resume
697+
// the coroutine.
698+
void resume_with(val&& result) {
699+
auto coro = std::move(std::get<STATE_CORO>(state));
700+
state.emplace<STATE_RESULT>(std::move(result));
701+
coro.resume();
702+
}
703+
704+
// `await_resume` finalizes the awaiter and should return the result
705+
// of the `co_await ...` expression - in our case, the stored value.
706+
val await_resume() { return std::move(std::get<STATE_RESULT>(state)); }
707+
};
708+
709+
extern "C" {
710+
// JS FFI helper for `val_awaiter::resume_with`.
711+
void _emval_coro_resume(val_awaiter* awaiter, EM_VAL result) {
712+
awaiter->resume_with(val::take_ownership(result));
713+
}
714+
}
715+
}
716+
717+
// `promise_type` is a well-known subtype with well-known method names
718+
// that compiler uses to drive the coroutine itself
719+
// (`T::promise_type` is used for any coroutine with declared return type `T`).
720+
class val::promise_type {
721+
val promise, resolve, reject_with_current_exception;
722+
723+
public:
724+
// Create a `new Promise` and store it alongside the `resolve` and `reject`
725+
// callbacks that can be used to fulfill it.
726+
promise_type() {
727+
EM_VAL resolve_handle;
728+
EM_VAL reject_handle;
729+
promise = val(internal::_emval_coro_make_promise(&resolve_handle, &reject_handle));
730+
resolve = val(resolve_handle);
731+
reject_with_current_exception = val(reject_handle);
732+
}
733+
734+
// Return the stored promise as the actual return value of the coroutine.
735+
val get_return_object() { return promise; }
736+
737+
// For similarity with JS async functions, our coroutines are eagerly evaluated.
738+
auto initial_suspend() noexcept { return std::suspend_never{}; }
739+
auto final_suspend() noexcept { return std::suspend_never{}; }
740+
741+
// On an unhandled exception, reject the stored promise instead of throwing
742+
// it asynchronously where it can't be handled.
743+
void unhandled_exception() {
744+
reject_with_current_exception();
745+
}
746+
747+
// Resolve the stored promise on `co_return value`.
748+
template<typename T>
749+
void return_value(T&& value) {
750+
resolve(std::forward<T>(value));
751+
}
752+
753+
// Return our awaiter on `co_await promise`.
754+
internal::val_awaiter await_transform(val promise) {
755+
return {std::move(promise)};
756+
}
757+
};
758+
#endif
759+
649760
// Declare a custom type that can be used in conjunction with
650761
// emscripten::register_type to emit custom TypeScript definitions for val
651762
// types.

test/embind/test_val_coro.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#include <emscripten.h>
2+
#include <emscripten/bind.h>
3+
#include <emscripten/val.h>
4+
#include <assert.h>
5+
#include <stdexcept>
6+
7+
using namespace emscripten;
8+
9+
EM_JS(EM_VAL, promise_sleep_impl, (int ms, int result), {
10+
let promise = new Promise(resolve => setTimeout(resolve, ms, result));
11+
let handle = Emval.toHandle(promise);
12+
// FIXME. See https://github.com/emscripten-core/emscripten/issues/16975.
13+
#if __wasm64__
14+
handle = BigInt(handle);
15+
#endif
16+
return handle;
17+
});
18+
19+
val promise_sleep(int ms, int result = 0) {
20+
return val::take_ownership(promise_sleep_impl(ms, result));
21+
}
22+
23+
val asyncCoro() {
24+
// check that just sleeping works
25+
co_await promise_sleep(1);
26+
// check that sleeping and receiving value works
27+
val v = co_await promise_sleep(1, 12);
28+
assert(v.as<int>() == 12);
29+
// check that returning value works (checked by JS in tests)
30+
co_return 34;
31+
}
32+
33+
val throwingCoro() {
34+
throw std::runtime_error("bang from throwingCoro!");
35+
co_return 56;
36+
}
37+
38+
EMSCRIPTEN_BINDINGS(test_val_coro) {
39+
function("asyncCoro", asyncCoro);
40+
function("throwingCoro", throwingCoro);
41+
}

test/test_core.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7856,6 +7856,24 @@ def test_embind_val_cross_thread_deleted(self):
78567856
''')
78577857
self.do_runf('test_embind_val_cross_thread.cpp')
78587858

7859+
def test_embind_val_coro(self):
7860+
create_file('post.js', r'''Module.onRuntimeInitialized = () => {
7861+
Module.asyncCoro().then(console.log);
7862+
}''')
7863+
self.emcc_args += ['-std=c++20', '--bind', '--post-js=post.js']
7864+
self.do_runf('embind/test_val_coro.cpp', '34\n')
7865+
7866+
def test_embind_val_coro_caught(self):
7867+
self.set_setting('EXCEPTION_STACK_TRACES')
7868+
create_file('post.js', r'''Module.onRuntimeInitialized = () => {
7869+
Module.throwingCoro().then(
7870+
console.log,
7871+
err => console.error(`rejected with: ${err.stack}`)
7872+
);
7873+
}''')
7874+
self.emcc_args += ['-std=c++20', '--bind', '--post-js=post.js', '-fexceptions']
7875+
self.do_runf('embind/test_val_coro.cpp', 'rejected with: std::runtime_error: bang from throwingCoro!\n')
7876+
78597877
def test_embind_dynamic_initialization(self):
78607878
self.emcc_args += ['-lembind']
78617879
self.do_run_in_out_file_test('embind/test_dynamic_initialization.cpp')

tools/maint/gen_sig_info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ def main(args):
391391
'USE_SDL': 0,
392392
'MAX_WEBGL_VERSION': 0,
393393
'AUTO_JS_LIBRARIES': 0,
394-
'ASYNCIFY': 1}, cxx=True)
394+
'ASYNCIFY': 1}, cxx=True, extra_cflags=['-std=c++20'])
395395
extract_sig_info(sig_info, {'LEGACY_GL_EMULATION': 1}, ['-DGLES'])
396396
extract_sig_info(sig_info, {'USE_GLFW': 2, 'FULL_ES3': 1, 'MAX_WEBGL_VERSION': 2})
397397
extract_sig_info(sig_info, {'STANDALONE_WASM': 1})

0 commit comments

Comments
 (0)