Skip to content

Commit

Permalink
C++20 co_await support for Embind promises (emscripten-core#20420)
Browse files Browse the repository at this point in the history
  • Loading branch information
RReverser authored Nov 3, 2023
1 parent 3587fbc commit 8ecbdb3
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 12 deletions.
2 changes: 2 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ See docs/process.md for more on how version tagging works.
sidestep some of the issues with legacy cmd.exe, but developers must
explicitly opt-in to running PowerShell scripts in system settings or
via the `Set-ExecutionPolicy` command. (#20416)
- `emscripten::val` now supports C++20 `co_await` operator for JavaScript
`Promise`s. (#20420)

3.1.47 - 10/09/23
-----------------
Expand Down
1 change: 1 addition & 0 deletions emscripten.py
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,7 @@ def create_pointer_conversion_wrappers(metadata):
'stbi_load_from_memory': 'pp_ppp_',
'emscripten_proxy_finish': '_p',
'emscripten_proxy_execute_queue': '_p',
'_emval_coro_resume': '_pp',
}

for function in settings.SIGNATURE_CONVERSIONS:
Expand Down
58 changes: 47 additions & 11 deletions site/source/docs/api_reference/val.h.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ Guide material for this class can be found in :ref:`embind-val-guide`.
}
See :ref:`embind-val-guide` for other examples.


.. warning:: JavaScript values can't be shared across threads, so neither can ``val`` instances that bind them.

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.
The easiest way to do this is with a ``thread_local`` declaration:

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

.. _val_as_handle:
.. cpp:function:: EM_VAL as_handle() const

Returns a raw handle representing this ``val``. This can be used for
passing raw value handles to JavaScript and retrieving the values on the
other side via ``Emval.toValue`` function. Example:

.. code:: cpp
EM_JS(void, log_value, (EM_VAL val_handle), {
Expand All @@ -130,16 +130,16 @@ Guide material for this class can be found in :ref:`embind-val-guide`.
from JavaScript, where the JavaScript side should wrap a value with
``Emval.toHandle``, pass it to C++, and then C++ can use ``take_ownership``
to convert it to a ``val`` instance. Example:

.. code:: cpp
EM_ASYNC_JS(EM_VAL, fetch_json_from_url, (const char *url_ptr), {
var url = UTF8ToString(url);
var response = await fetch(url);
var json = await response.json();
return Emval.toHandle(json);
});
val obj = val::take_ownership(fetch_json_from_url("https://httpbin.org/json"));
std::string author = obj["slideshow"]["author"].as<std::string>();
Expand Down Expand Up @@ -169,12 +169,12 @@ Guide material for this class can be found in :ref:`embind-val-guide`.


.. cpp:function:: val(val&& v)

Moves ownership of a value to a new ``val`` instance.


.. cpp:function:: val(const val& v)

Creates another reference to the same value behind the provided ``val`` instance.


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


.. cpp:function:: val& operator=(val&& v)

Removes a reference to the currently bound value and takes over the provided one.


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


.. cpp:function:: val operator()(Args&&... args) const

Assumes that current value is a function, and invokes it with provided arguments.


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

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

.. cpp:function:: val operator co_await() const

The ``co_await`` operator allows awaiting JavaScript promises represented by ``val``.

It's compatible with any C++20 coroutines, but should be normally used inside
a ``val``-returning coroutine which will also become a ``Promise``.

For example, it allows you to implement the equivalent of this JavaScript ``async``/``await`` function:

.. code:: javascript
async function foo() {
const response = await fetch("http://url");
const json = await response.json();
return json;
}
export { foo };
as a C++ coroutine:

.. code:: cpp
val foo() {
val response = co_await val::global("fetch")(std::string("http://url"));
val json = co_await response.call<val>("json");
return json;
}
EMSCRIPTEN_BINDINGS(module) {
function("foo", &foo);
}
Unlike the ``await()`` method, it doesn't need Asyncify as it uses native C++ coroutine transform.

:returns: A ``val`` representing the fulfilled value of this promise.

.. cpp:type: EMSCRIPTEN_SYMBOL(name)
Expand Down
29 changes: 29 additions & 0 deletions src/embind/emval.js
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,35 @@ var LibraryEmVal = {
var result = iterator.next();
return result.done ? 0 : Emval.toHandle(result.value);
},

_emval_coro_suspend__deps: ['$Emval', '_emval_coro_resume'],
_emval_coro_suspend: (promiseHandle, awaiterPtr) => {
Emval.toValue(promiseHandle).then(result => {
__emval_coro_resume(awaiterPtr, Emval.toHandle(result));
});
},

_emval_coro_make_promise__deps: ['$Emval', '__cxa_rethrow'],
_emval_coro_make_promise: (resolveHandlePtr, rejectHandlePtr) => {
return Emval.toHandle(new Promise((resolve, reject) => {
const rejectWithCurrentException = () => {
try {
// Use __cxa_rethrow which already has mechanism for generating
// user-friendly error message and stacktrace from C++ exception
// if EXCEPTION_STACK_TRACES is enabled and numeric exception
// with metadata optimised out otherwise.
___cxa_rethrow();
} catch (e) {
// But catch it so that it rejects the promise instead of throwing
// in an unpredictable place during async execution.
reject(e);
}
};

{{{ makeSetValue('resolveHandlePtr', '0', 'Emval.toHandle(resolve)', '*') }}};
{{{ makeSetValue('rejectHandlePtr', '0', 'Emval.toHandle(rejectWithCurrentException)', '*') }}};
}));
},
};

addToLibrary(LibraryEmVal);
2 changes: 2 additions & 0 deletions src/library_sigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@ sigs = {
_emval_await__sig: 'pp',
_emval_call__sig: 'dpppp',
_emval_call_method__sig: 'dppppp',
_emval_coro_make_promise__sig: 'ppp',
_emval_coro_suspend__sig: 'vpp',
_emval_decref__sig: 'vp',
_emval_delete__sig: 'ipp',
_emval_equals__sig: 'ipp',
Expand Down
111 changes: 111 additions & 0 deletions system/include/emscripten/val.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
#include <cstdint> // uintptr_t
#include <vector>
#include <type_traits>
#if _LIBCPP_STD_VER >= 20
#include <coroutine>
#include <variant>
#endif


namespace emscripten {
Expand Down Expand Up @@ -110,6 +114,11 @@ EM_VAL _emval_await(EM_VAL promise);
EM_VAL _emval_iter_begin(EM_VAL iterable);
EM_VAL _emval_iter_next(EM_VAL iterator);

#if _LIBCPP_STD_VER >= 20
void _emval_coro_suspend(EM_VAL promise, void* coro_ptr);
EM_VAL _emval_coro_make_promise(EM_VAL *resolve, EM_VAL *reject);
#endif

} // extern "C"

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

#if _LIBCPP_STD_VER >= 20
struct promise_type;
#endif

private:
// takes ownership, assumes handle already incref'd and lives on the same thread
explicit val(EM_VAL handle)
Expand Down Expand Up @@ -646,6 +659,104 @@ inline val::iterator val::begin() const {
return iterator(*this);
}

#if _LIBCPP_STD_VER >= 20
namespace internal {
// Awaiter defines a set of well-known methods that compiler uses
// to drive the argument of the `co_await` operator (regardless
// of the type of the parent coroutine).
// This one is used for Promises represented by the `val` type.
class val_awaiter {
// State machine holding awaiter's current state. One of:
// - initially created with promise
// - waiting with a given coroutine handle
// - completed with a result
std::variant<val, std::coroutine_handle<val::promise_type>, val> state;

constexpr static std::size_t STATE_PROMISE = 0;
constexpr static std::size_t STATE_CORO = 1;
constexpr static std::size_t STATE_RESULT = 2;

public:
val_awaiter(val&& promise)
: state(std::in_place_index<STATE_PROMISE>, std::move(promise)) {}

// just in case, ensure nobody moves / copies this type around
val_awaiter(val_awaiter&&) = delete;

// Promises don't have a synchronously accessible "ready" state.
bool await_ready() { return false; }

// On suspend, store the coroutine handle and invoke a helper that will do
// a rough equivalent of `promise.then(value => this.resume_with(value))`.
void await_suspend(std::coroutine_handle<val::promise_type> handle) {
internal::_emval_coro_suspend(std::get<STATE_PROMISE>(state).as_handle(), this);
state.emplace<STATE_CORO>(handle);
}

// When JS invokes `resume_with` with some value, store that value and resume
// the coroutine.
void resume_with(val&& result) {
auto coro = std::move(std::get<STATE_CORO>(state));
state.emplace<STATE_RESULT>(std::move(result));
coro.resume();
}

// `await_resume` finalizes the awaiter and should return the result
// of the `co_await ...` expression - in our case, the stored value.
val await_resume() { return std::move(std::get<STATE_RESULT>(state)); }
};

extern "C" {
// JS FFI helper for `val_awaiter::resume_with`.
void _emval_coro_resume(val_awaiter* awaiter, EM_VAL result) {
awaiter->resume_with(val::take_ownership(result));
}
}
}

// `promise_type` is a well-known subtype with well-known method names
// that compiler uses to drive the coroutine itself
// (`T::promise_type` is used for any coroutine with declared return type `T`).
class val::promise_type {
val promise, resolve, reject_with_current_exception;

public:
// Create a `new Promise` and store it alongside the `resolve` and `reject`
// callbacks that can be used to fulfill it.
promise_type() {
EM_VAL resolve_handle;
EM_VAL reject_handle;
promise = val(internal::_emval_coro_make_promise(&resolve_handle, &reject_handle));
resolve = val(resolve_handle);
reject_with_current_exception = val(reject_handle);
}

// Return the stored promise as the actual return value of the coroutine.
val get_return_object() { return promise; }

// For similarity with JS async functions, our coroutines are eagerly evaluated.
auto initial_suspend() noexcept { return std::suspend_never{}; }
auto final_suspend() noexcept { return std::suspend_never{}; }

// On an unhandled exception, reject the stored promise instead of throwing
// it asynchronously where it can't be handled.
void unhandled_exception() {
reject_with_current_exception();
}

// Resolve the stored promise on `co_return value`.
template<typename T>
void return_value(T&& value) {
resolve(std::forward<T>(value));
}

// Return our awaiter on `co_await promise`.
internal::val_awaiter await_transform(val promise) {
return {std::move(promise)};
}
};
#endif

// Declare a custom type that can be used in conjunction with
// emscripten::register_type to emit custom TypeScript definitions for val
// types.
Expand Down
41 changes: 41 additions & 0 deletions test/embind/test_val_coro.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <assert.h>
#include <stdexcept>

using namespace emscripten;

EM_JS(EM_VAL, promise_sleep_impl, (int ms, int result), {
let promise = new Promise(resolve => setTimeout(resolve, ms, result));
let handle = Emval.toHandle(promise);
// FIXME. See https://github.com/emscripten-core/emscripten/issues/16975.
#if __wasm64__
handle = BigInt(handle);
#endif
return handle;
});

val promise_sleep(int ms, int result = 0) {
return val::take_ownership(promise_sleep_impl(ms, result));
}

val asyncCoro() {
// check that just sleeping works
co_await promise_sleep(1);
// check that sleeping and receiving value works
val v = co_await promise_sleep(1, 12);
assert(v.as<int>() == 12);
// check that returning value works (checked by JS in tests)
co_return 34;
}

val throwingCoro() {
throw std::runtime_error("bang from throwingCoro!");
co_return 56;
}

EMSCRIPTEN_BINDINGS(test_val_coro) {
function("asyncCoro", asyncCoro);
function("throwingCoro", throwingCoro);
}
18 changes: 18 additions & 0 deletions test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7856,6 +7856,24 @@ def test_embind_val_cross_thread_deleted(self):
''')
self.do_runf('test_embind_val_cross_thread.cpp')

def test_embind_val_coro(self):
create_file('post.js', r'''Module.onRuntimeInitialized = () => {
Module.asyncCoro().then(console.log);
}''')
self.emcc_args += ['-std=c++20', '--bind', '--post-js=post.js']
self.do_runf('embind/test_val_coro.cpp', '34\n')

def test_embind_val_coro_caught(self):
self.set_setting('EXCEPTION_STACK_TRACES')
create_file('post.js', r'''Module.onRuntimeInitialized = () => {
Module.throwingCoro().then(
console.log,
err => console.error(`rejected with: ${err.stack}`)
);
}''')
self.emcc_args += ['-std=c++20', '--bind', '--post-js=post.js', '-fexceptions']
self.do_runf('embind/test_val_coro.cpp', 'rejected with: std::runtime_error: bang from throwingCoro!\n')

def test_embind_dynamic_initialization(self):
self.emcc_args += ['-lembind']
self.do_run_in_out_file_test('embind/test_dynamic_initialization.cpp')
Expand Down
2 changes: 1 addition & 1 deletion tools/maint/gen_sig_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ def main(args):
'USE_SDL': 0,
'MAX_WEBGL_VERSION': 0,
'AUTO_JS_LIBRARIES': 0,
'ASYNCIFY': 1}, cxx=True)
'ASYNCIFY': 1}, cxx=True, extra_cflags=['-std=c++20'])
extract_sig_info(sig_info, {'LEGACY_GL_EMULATION': 1}, ['-DGLES'])
extract_sig_info(sig_info, {'USE_GLFW': 2, 'FULL_ES3': 1, 'MAX_WEBGL_VERSION': 2})
extract_sig_info(sig_info, {'STANDALONE_WASM': 1})
Expand Down

0 comments on commit 8ecbdb3

Please sign in to comment.