Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C++20 co_await support for Embind promises #20420

Merged
merged 6 commits into from
Nov 3, 2023
Merged
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
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