diff --git a/src/jsifier.mjs b/src/jsifier.mjs index 604d204832dcc..157f4538d46b9 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -340,7 +340,7 @@ function(${args}) { return ` ${async_}function(${args}) { if (ENVIRONMENT_IS_PTHREAD) - return ${proxyFunc}(${proxiedFunctionTable.length}, 0, ${+sync}${args ? ', ' : ''}${args}); + return ${proxyFunc}(${proxiedFunctionTable.length}, 0, ${+sync}, 0${args ? ', ' : ''}${args}); ${body} }\n`; }); diff --git a/src/library.js b/src/library.js index 607e18e68de0d..94e275839c765 100644 --- a/src/library.js +++ b/src/library.js @@ -1631,7 +1631,7 @@ addToLibrary({ '$proxyToMainThread' #endif ], - $runMainThreadEmAsm: (emAsmAddr, sigPtr, argbuf, sync) => { + $runMainThreadEmAsm: (emAsmAddr, sigPtr, argbuf, sync, asnycAwait) => { var args = readEmAsmArgs(sigPtr, argbuf); #if PTHREADS if (ENVIRONMENT_IS_PTHREAD) { @@ -1644,7 +1644,7 @@ addToLibrary({ // of using __proxy. (And dor simplicity, do the same in the sync // case as well, even though it's not strictly necessary, to keep the two // code paths as similar as possible on both sides.) - return proxyToMainThread(0, emAsmAddr, sync, ...args); + return proxyToMainThread(0, emAsmAddr, sync, asnycAwait, ...args); } #endif #if ASSERTIONS @@ -1655,6 +1655,19 @@ addToLibrary({ emscripten_asm_const_int_sync_on_main_thread__deps: ['$runMainThreadEmAsm'], emscripten_asm_const_int_sync_on_main_thread: (emAsmAddr, sigPtr, argbuf) => runMainThreadEmAsm(emAsmAddr, sigPtr, argbuf, 1), + emscripten_asm_const_int_await_on_main_thread__deps: ['$runMainThreadEmAsm'], + emscripten_asm_const_int_await_on_main_thread: (emAsmAddr, sigPtr, argbuf) => { + #if PTHREADS + if (ENVIRONMENT_IS_PTHREAD) { + return runMainThreadEmAsm(emAsmAddr, sigPtr, argbuf, /*sync=*/1, /*asyncAwait=*/1); + } + #endif + #if ASSERTIONS + assert((typeof ENVIRONMENT_IS_PTHREAD !== 'undefined' && ENVIRONMENT_IS_PTHREAD), "emscripten_asm_const_int_await_on_main_thread is not available on the main thread"); + #endif + return runMainThreadEmAsm(emAsmAddr, sigPtr, argbuf, /*sync*/1, /*asyncAwait=*/1); + }, + emscripten_asm_const_ptr_sync_on_main_thread__deps: ['$runMainThreadEmAsm'], emscripten_asm_const_ptr_sync_on_main_thread: (emAsmAddr, sigPtr, argbuf) => runMainThreadEmAsm(emAsmAddr, sigPtr, argbuf, 1), diff --git a/src/library_pthread.js b/src/library_pthread.js index 64d276410d3fa..80d4f73e19180 100644 --- a/src/library_pthread.js +++ b/src/library_pthread.js @@ -893,9 +893,9 @@ var LibraryPThread = { $proxyToMainThreadPtr: (...args) => BigInt(proxyToMainThread(...args)), #endif - $proxyToMainThread__deps: ['$stackSave', '$stackRestore', '$stackAlloc', '_emscripten_run_on_main_thread_js', ...i53ConversionDeps], + $proxyToMainThread__deps: ['$stackSave', '$stackRestore', '$stackAlloc', '_emscripten_run_on_main_thread_js', '_emscripten_await_on_main_thread_js', ...i53ConversionDeps], $proxyToMainThread__docs: '/** @type{function(number, (number|boolean), ...number)} */', - $proxyToMainThread: (funcIndex, emAsmAddr, sync, ...callArgs) => { + $proxyToMainThread: (funcIndex, emAsmAddr, sync, asyncAwait, ...callArgs) => { // EM_ASM proxying is done by passing a pointer to the address of the EM_ASM // content as `emAsmAddr`. JS library proxying is done by passing an index // into `proxiedJSCallArgs` as `funcIndex`. If `emAsmAddr` is non-zero then @@ -933,7 +933,12 @@ var LibraryPThread = { HEAPF64[b + i] = arg; #endif } - var rtn = __emscripten_run_on_main_thread_js(funcIndex, emAsmAddr, serializedNumCallArgs, args, sync); + var rtn; + if (asyncAwait) { + rtn = __emscripten_await_on_main_thread_js(funcIndex, emAsmAddr, serializedNumCallArgs, args); + } else { + rtn = __emscripten_run_on_main_thread_js(funcIndex, emAsmAddr, serializedNumCallArgs, args, sync); + } stackRestore(sp); return rtn; }, @@ -944,7 +949,7 @@ var LibraryPThread = { _emscripten_receive_on_main_thread_js__deps: [ '$proxyToMainThread', '$proxiedJSCallArgs'], - _emscripten_receive_on_main_thread_js: (funcIndex, emAsmAddr, callingThread, numCallArgs, args) => { + _emscripten_receive_on_main_thread_js: (funcIndex, emAsmAddr, callingThread, numCallArgs, args, promiseCtx) => { // Sometimes we need to backproxy events to the calling thread (e.g. // HTML5 DOM events handlers such as // emscripten_set_mousemove_callback()), so keep track in a globally @@ -983,6 +988,24 @@ var LibraryPThread = { PThread.currentProxiedOperationCallerThread = callingThread; var rtn = func(...proxiedJSCallArgs); PThread.currentProxiedOperationCallerThread = 0; + if (promiseCtx) { +#if ASSERTIONS + assert(!!rtn.then, 'Return value of proxied function expected to be a Promise but got' + rtn); +#endif + rtn.then(res => { +#if MEMORY64 + // In memory64 mode some proxied functions return bigint/pointer but + // our return type is i53/double. + if (typeof res == "bigint") { + res = bigintToI53Checked(res); + } +#endif + __emscripten_proxy_promise_finish(promiseCtx, res); + }).catch(err => { + __emscripten_proxy_promise_finish(promiseCtx, err); + }); + return; + } #if MEMORY64 // In memory64 mode some proxied functions return bigint/pointer but // our return type is i53/double. diff --git a/src/library_sigs.js b/src/library_sigs.js index 24537748afd4c..061e0509c3bbb 100644 --- a/src/library_sigs.js +++ b/src/library_sigs.js @@ -326,7 +326,7 @@ sigs = { _emscripten_notify_mailbox_postmessage__sig: 'vpp', _emscripten_push_main_loop_blocker__sig: 'vppp', _emscripten_push_uncounted_main_loop_blocker__sig: 'vppp', - _emscripten_receive_on_main_thread_js__sig: 'dippip', + _emscripten_receive_on_main_thread_js__sig: 'dippipp', _emscripten_runtime_keepalive_clear__sig: 'v', _emscripten_set_offscreencanvas_size__sig: 'ipii', _emscripten_system__sig: 'ip', @@ -569,6 +569,7 @@ sigs = { emscripten_asm_const_double__sig: 'dppp', emscripten_asm_const_double_sync_on_main_thread__sig: 'dppp', emscripten_asm_const_int__sig: 'ippp', + emscripten_asm_const_int_await_on_main_thread__sig: 'ippp', emscripten_asm_const_int_sync_on_main_thread__sig: 'ippp', emscripten_asm_const_ptr__sig: 'pppp', emscripten_asm_const_ptr_sync_on_main_thread__sig: 'pppp', diff --git a/system/include/emscripten/em_asm.h b/system/include/emscripten/em_asm.h index 3312ba8e15baf..9abadc34fa20f 100644 --- a/system/include/emscripten/em_asm.h +++ b/system/include/emscripten/em_asm.h @@ -28,6 +28,9 @@ __attribute__((nothrow)) int emscripten_asm_const_int_sync_on_main_thread( const char* code, const char* arg_sigs, ...); __attribute__((nothrow)) +int emscripten_asm_const_int_await_on_main_thread( + const char* code, const char* arg_sigs, ...); +__attribute__((nothrow)) void* emscripten_asm_const_ptr_sync_on_main_thread( const char* code, const char* arg_sigs, ...); __attribute__((nothrow)) @@ -51,6 +54,7 @@ void emscripten_asm_const_async_on_main_thread( #define EM_ASM_PTR(...) EM_ASM_ERROR #define EM_ASM_DOUBLE(...) EM_ASM_ERROR #define MAIN_THREAD_EM_ASM(...) EM_ASM_ERROR +#define MAIN_THREAD_EM_ASM_AWAIT(...) EM_ASM_ERROR #define MAIN_THREAD_EM_ASM_INT(...) EM_ASM_ERROR #define MAIN_THREAD_EM_ASM_PTR(...) EM_ASM_ERROR #define MAIN_THREAD_EM_ASM_DOUBLE(...) EM_ASM_ERROR @@ -250,6 +254,13 @@ const char __em_asm_sig_builder<__em_asm_type_tuple >::buffer[] = { __e // functions. #define MAIN_THREAD_EM_ASM(code, ...) ((void)emscripten_asm_const_int_sync_on_main_thread(CODE_EXPR(#code) _EM_ASM_PREP_ARGS(__VA_ARGS__))) +// Runs the given Javascript code on the main browser thread. +// It must be called from a non-main thread. +// The code must return a promise, and this function will wait for the promise +// to resolve or reject, essentially blocking the calling thread until then. +// In either case the function will return an integer, which is the result of the promise. +#define MAIN_THREAD_EM_ASM_AWAIT(code, ...) emscripten_asm_const_int_await_on_main_thread(CODE_EXPR(#code) _EM_ASM_PREP_ARGS(__VA_ARGS__)) + // Runs the given JavaScript code synchronously on the main browser thread, and // returns an integer back. // The same considerations apply as with MAIN_THREAD_EM_ASM(). diff --git a/system/lib/pthread/proxying.c b/system/lib/pthread/proxying.c index 7617c2ae9de90..1f6a54eb637de 100644 --- a/system/lib/pthread/proxying.c +++ b/system/lib/pthread/proxying.c @@ -13,6 +13,7 @@ #include #include #include +#include #include "em_task_queue.h" #include "thread_mailbox.h" @@ -35,6 +36,20 @@ static em_proxying_queue system_proxying_queue = { .capacity = 0, }; +typedef struct proxied_js_func_t { + int funcIndex; + void* emAsmAddr; + pthread_t callingThread; + int numArgs; + double* argBuffer; + double result; + bool owned; + // Only used when when the underlying JS function is async and ctx need to be + // resolved on promise end. + // should be NULL if the function is sync. + em_proxying_ctx * ctx; +} proxied_js_func_t; + em_proxying_queue* emscripten_proxy_get_system_queue(void) { return &system_proxying_queue; } @@ -397,6 +412,14 @@ static void call_then_finish_task(em_proxying_ctx* ctx, void* arg) { emscripten_proxy_finish(ctx); } +static void call_proxied_js_task_with_ctx(em_proxying_ctx* ctx, void* arg) { + // Stash the context on the proxied_js_func_t argument. + task* t = arg; + proxied_js_func_t* p = t->arg; + p->ctx = ctx; + t->func(t->arg); +} + int emscripten_proxy_sync(em_proxying_queue* q, pthread_t target_thread, void (*func)(void*), @@ -583,20 +606,10 @@ em_promise_t emscripten_proxy_promise(em_proxying_queue* q, &block->promise_ctx); } -typedef struct proxied_js_func_t { - int funcIndex; - void* emAsmAddr; - pthread_t callingThread; - int numArgs; - double* argBuffer; - double result; - bool owned; -} proxied_js_func_t; - static void run_js_func(void* arg) { proxied_js_func_t* f = (proxied_js_func_t*)arg; f->result = _emscripten_receive_on_main_thread_js( - f->funcIndex, f->emAsmAddr, f->callingThread, f->numArgs, f->argBuffer); + f->funcIndex, f->emAsmAddr, f->callingThread, f->numArgs, f->argBuffer, f->ctx); if (f->owned) { free(f->argBuffer); free(f); @@ -615,6 +628,7 @@ double _emscripten_run_on_main_thread_js(int func_index, .numArgs = num_args, .argBuffer = buffer, .owned = false, + .ctx = NULL, }; em_proxying_queue* q = emscripten_proxy_get_system_queue(); @@ -642,3 +656,34 @@ double _emscripten_run_on_main_thread_js(int func_index, } return 0; } + +double _emscripten_await_on_main_thread_js(int func_index, + void* em_asm_addr, + int num_args, + double* buffer) { + em_proxying_queue* q = emscripten_proxy_get_system_queue(); + pthread_t target = emscripten_main_runtime_thread_id(); + + proxied_js_func_t f = { + .funcIndex = func_index, + .emAsmAddr = em_asm_addr, + .callingThread = pthread_self(), + .numArgs = num_args, + .argBuffer = buffer, + .owned = false, + }; + task t = {.func = run_js_func, .arg = &f}; + + if (!emscripten_proxy_sync_with_ctx(q, target, call_proxied_js_task_with_ctx, &t)) { + assert(false && "emscripten_proxy_sync_with_ctx failed"); + return 0; + } + return f.result; +} + +void _emscripten_proxy_promise_finish(em_proxying_ctx* ctx, void* res) { + task* t = (task*)ctx->arg; + proxied_js_func_t* func = (proxied_js_func_t*)t->arg; + func->result = (double)(intptr_t)res; + emscripten_proxy_finish(ctx); +} diff --git a/system/lib/pthread/threading_internal.h b/system/lib/pthread/threading_internal.h index eae455944d578..6d52962c09387 100644 --- a/system/lib/pthread/threading_internal.h +++ b/system/lib/pthread/threading_internal.h @@ -96,7 +96,7 @@ int __pthread_create_js(struct __pthread *thread, const pthread_attr_t *attr, vo int _emscripten_default_pthread_stack_size(); void __set_thread_state(pthread_t ptr, int is_main, int is_runtime, int can_block); -double _emscripten_receive_on_main_thread_js(int funcIndex, void* emAsmAddr, pthread_t callingThread, int numCallArgs, double* args); +double _emscripten_receive_on_main_thread_js(int funcIndex, void* emAsmAddr, pthread_t callingThread, int numCallArgs, double* args, void *ctx); // Return non-zero if the calling thread supports Atomic.wait (For example // if called from the main browser thread, this function will return zero diff --git a/test/core/test_main_thread_async_em_asm_await.cpp b/test/core/test_main_thread_async_em_asm_await.cpp new file mode 100644 index 0000000000000..70020fcff8be9 --- /dev/null +++ b/test/core/test_main_thread_async_em_asm_await.cpp @@ -0,0 +1,25 @@ +// Copyright 2024 The Emscripten Authors. All rights reserved. +// Emscripten is available under two separate licenses, the MIT license and the +// University of Illinois/NCSA Open Source License. Both these licenses can be +// found in the LICENSE file. + +#include +#include + +int main() +{ + printf("Before MAIN_THREAD_EM_ASM_AWAIT\n"); + int res = MAIN_THREAD_EM_ASM_AWAIT({ + out('Inside MAIN_THREAD_EM_ASM_AWAIT: ' + $0 + ' ' + $1); + const asyncOp = new Promise((resolve,reject) => { + setTimeout(() => { + out('Inside asyncOp'); + resolve(2); + }, 1000); + }); + return asyncOp; + }, 42, 3.5); + printf("After MAIN_THREAD_EM_ASM_AWAIT\n"); + printf("result: %d\n", res); + return 0; +} diff --git a/test/core/test_main_thread_async_em_asm_await.out b/test/core/test_main_thread_async_em_asm_await.out new file mode 100644 index 0000000000000..228ed9a2a049b --- /dev/null +++ b/test/core/test_main_thread_async_em_asm_await.out @@ -0,0 +1,5 @@ +Before MAIN_THREAD_EM_ASM_AWAIT +Inside MAIN_THREAD_EM_ASM_AWAIT: 42 3.5 +Inside asyncOp +After MAIN_THREAD_EM_ASM_AWAIT +result: 2 \ No newline at end of file diff --git a/test/core/test_main_thread_async_em_asm_await_reject.cpp b/test/core/test_main_thread_async_em_asm_await_reject.cpp new file mode 100644 index 0000000000000..8c34161ca5760 --- /dev/null +++ b/test/core/test_main_thread_async_em_asm_await_reject.cpp @@ -0,0 +1,34 @@ +// Copyright 2024 The Emscripten Authors. All rights reserved. +// Emscripten is available under two separate licenses, the MIT license and the +// University of Illinois/NCSA Open Source License. Both these licenses can be +// found in the LICENSE file. + +#include +#include +#include + +int main() +{ + // start new thread + pthread_t thread; + pthread_create(&thread, NULL, [](void*) -> void* { + printf("Before MAIN_THREAD_EM_ASM_AWAIT\n"); + int res = MAIN_THREAD_EM_ASM_AWAIT({ + out('Inside MAIN_THREAD_EM_ASM_AWAIT: ' + $0 + ' ' + $1); + const asyncOp = new Promise((resolve,reject) => { + setTimeout(() => { + out('Inside asyncOp'); + reject(2); + }, 1000); + }); + return asyncOp; + }, 42, 3.5); + printf("After MAIN_THREAD_EM_ASM_AWAIT rejected\n"); + printf("result: %d\n", res); + return NULL; + }, NULL); + + // wait for thread to finish + pthread_join(thread, NULL); + return 0; +} diff --git a/test/core/test_main_thread_async_em_asm_await_reject.out b/test/core/test_main_thread_async_em_asm_await_reject.out new file mode 100644 index 0000000000000..cb9a7a7b5c48c --- /dev/null +++ b/test/core/test_main_thread_async_em_asm_await_reject.out @@ -0,0 +1,5 @@ +Before MAIN_THREAD_EM_ASM_AWAIT +Inside MAIN_THREAD_EM_ASM_AWAIT: 42 3.5 +Inside asyncOp +After MAIN_THREAD_EM_ASM_AWAIT rejected +result: 2 \ No newline at end of file diff --git a/test/other/codesize/test_codesize_minimal_pthreads.size b/test/other/codesize/test_codesize_minimal_pthreads.size index 0a5bf4b2a51ac..a02c1cabcd235 100644 --- a/test/other/codesize/test_codesize_minimal_pthreads.size +++ b/test/other/codesize/test_codesize_minimal_pthreads.size @@ -1 +1 @@ -19487 +19495 diff --git a/test/other/test_unoptimized_code_size.js.size b/test/other/test_unoptimized_code_size.js.size index 6f75fa3a9a413..c92d2e5e9cfeb 100644 --- a/test/other/test_unoptimized_code_size.js.size +++ b/test/other/test_unoptimized_code_size.js.size @@ -1 +1 @@ -53709 +53980 diff --git a/test/other/test_unoptimized_code_size_no_asserts.js.size b/test/other/test_unoptimized_code_size_no_asserts.js.size index e55af0c20aeb7..27713e9b085b4 100644 --- a/test/other/test_unoptimized_code_size_no_asserts.js.size +++ b/test/other/test_unoptimized_code_size_no_asserts.js.size @@ -1 +1 @@ -29018 +29400 diff --git a/test/other/test_unoptimized_code_size_strict.js.size b/test/other/test_unoptimized_code_size_strict.js.size index ca6f382d48289..b9b5998b7a2be 100644 --- a/test/other/test_unoptimized_code_size_strict.js.size +++ b/test/other/test_unoptimized_code_size_strict.js.size @@ -1 +1 @@ -52531 +52776 diff --git a/test/test_core.py b/test/test_core.py index cbdf449ec1d0e..78a61b7672d95 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -1878,6 +1878,26 @@ def test_main_thread_em_asm(self, args): def test_main_thread_async_em_asm(self, args, force_c=False): self.do_core_test('test_main_thread_async_em_asm.cpp', emcc_args=args, force_c=force_c) + @needs_dylink + @parameterized({ + '': (['-sASSERTIONS'], False), + 'pthreads': (['-pthread', '-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME'], False), + }) + def test_main_thread_async_em_asm_await(self, args, force_c=False): + if '-sPROXY_TO_PTHREAD' not in args: + # expect runtime to error + output = self.do_runf('core/test_main_thread_async_em_asm_await.cpp', expected_output=None, assert_returncode=NON_ZERO, emcc_args=args) + self.assertContained('emscripten_asm_const_int_await_on_main_thread is not available on the main thread', output) + else: + self.do_core_test('test_main_thread_async_em_asm_await.cpp', emcc_args=args, force_c=force_c) + + @needs_dylink + @parameterized({ + 'pthreads': (['-pthread', '-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME', '-sASSERTIONS'], False), + }) + def test_main_thread_async_em_asm_await_reject(self, args, force_c=False): + self.do_core_test('test_main_thread_async_em_asm_await_reject.cpp', emcc_args=args, force_c=force_c) + # Tests MAIN_THREAD_EM_ASM_INT() function call with different signatures. def test_main_thread_em_asm_signatures(self): self.do_core_test('test_em_asm_signatures.cpp', assert_returncode=NON_ZERO) diff --git a/tools/emscripten.py b/tools/emscripten.py index 250432f5462f7..d1eb466e20486 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -1074,6 +1074,8 @@ def create_pointer_conversion_wrappers(metadata): '_wasmfs_read_file': 'pp', '__dl_seterr': '_pp', '_emscripten_run_on_main_thread_js': '__p_p_', + '_emscripten_await_on_main_thread_js': '__p_p_', + '_emscripten_proxy_promise_finish': 'pp', '_emscripten_proxy_execute_task_queue': '_p', '_emscripten_thread_exit': '_p', '_emscripten_thread_init': '_p_____', diff --git a/tools/link.py b/tools/link.py index 7f75ef4edb435..a7a8c13a1e656 100644 --- a/tools/link.py +++ b/tools/link.py @@ -499,6 +499,12 @@ def setup_pthreads(): settings.REQUIRED_EXPORTS += [ '_emscripten_thread_free_data', '_emscripten_thread_crashed', + 'emscripten_main_runtime_thread_id', + 'emscripten_main_thread_process_queued_calls', + '_emscripten_run_on_main_thread_js', + '_emscripten_await_on_main_thread_js', + '_emscripten_proxy_promise_finish', + 'emscripten_stack_set_limits', ] if settings.EMBIND: