diff --git a/ChangeLog.md b/ChangeLog.md index c51b463aa0084..4507d8fccf71b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -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 ----------------- diff --git a/emscripten.py b/emscripten.py index e79a9b1366d7f..476d2b8cf0401 100644 --- a/emscripten.py +++ b/emscripten.py @@ -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: diff --git a/site/source/docs/api_reference/val.h.rst b/site/source/docs/api_reference/val.h.rst index 68a0370f15a99..2e8743e77eb92 100644 --- a/site/source/docs/api_reference/val.h.rst +++ b/site/source/docs/api_reference/val.h.rst @@ -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: @@ -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), { @@ -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(); @@ -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. @@ -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. @@ -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. @@ -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("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) diff --git a/src/embind/emval.js b/src/embind/emval.js index 22bc575c805d8..d91ed78351196 100644 --- a/src/embind/emval.js +++ b/src/embind/emval.js @@ -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); diff --git a/src/library_sigs.js b/src/library_sigs.js index 276b32ba1bc10..8e708f4510494 100644 --- a/src/library_sigs.js +++ b/src/library_sigs.js @@ -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', diff --git a/system/include/emscripten/val.h b/system/include/emscripten/val.h index d7d842e127350..302efcf202fb5 100644 --- a/system/include/emscripten/val.h +++ b/system/include/emscripten/val.h @@ -18,6 +18,10 @@ #include // uintptr_t #include #include +#if _LIBCPP_STD_VER >= 20 +#include +#include +#endif namespace emscripten { @@ -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 @@ -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) @@ -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> 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, 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 handle) { + internal::_emval_coro_suspend(std::get(state).as_handle(), this); + state.emplace(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)); + state.emplace(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)); } +}; + +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 + void return_value(T&& value) { + resolve(std::forward(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. diff --git a/test/embind/test_val_coro.cpp b/test/embind/test_val_coro.cpp new file mode 100644 index 0000000000000..88eeff5437813 --- /dev/null +++ b/test/embind/test_val_coro.cpp @@ -0,0 +1,41 @@ +#include +#include +#include +#include +#include + +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() == 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); +} diff --git a/test/test_core.py b/test/test_core.py index 178946806b010..130ebf1d93f2b 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -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') diff --git a/tools/maint/gen_sig_info.py b/tools/maint/gen_sig_info.py index 2912ebae84b86..e610b0ee17d0c 100755 --- a/tools/maint/gen_sig_info.py +++ b/tools/maint/gen_sig_info.py @@ -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})