diff --git a/system/include/emscripten/proxying.h b/system/include/emscripten/proxying.h index bdcc328a55e8a..db58f27386049 100644 --- a/system/include/emscripten/proxying.h +++ b/system/include/emscripten/proxying.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #ifdef __cplusplus @@ -102,6 +103,18 @@ int emscripten_proxy_callback_with_ctx(em_proxying_queue* q, void (*cancel)(void*), void* arg); +__attribute__((warn_unused_result)) em_promise_t +emscripten_proxy_promise(em_proxying_queue* q, + pthread_t target_thread, + void (*func)(void*), + void* arg); + +__attribute__((warn_unused_result)) em_promise_t +emscripten_proxy_promise_with_ctx(em_proxying_queue* q, + pthread_t target_thread, + void (*func)(em_proxying_ctx*, void*), + void* arg); + #ifdef __cplusplus } // extern "C" diff --git a/system/lib/pthread/proxying.c b/system/lib/pthread/proxying.c index 1fdd69c38d895..5a10c08a6e758 100644 --- a/system/lib/pthread/proxying.c +++ b/system/lib/pthread/proxying.c @@ -389,7 +389,7 @@ int emscripten_proxy_sync_with_ctx(em_proxying_queue* q, } // Helper for signaling the end of the task after the user function returns. -static void call_then_finish_sync(em_proxying_ctx* ctx, void* arg) { +static void call_then_finish_task(em_proxying_ctx* ctx, void* arg) { task* t = arg; t->func(t->arg); emscripten_proxy_finish(ctx); @@ -401,7 +401,7 @@ int emscripten_proxy_sync(em_proxying_queue* q, void* arg) { task t = {.func = func, .arg = arg}; return emscripten_proxy_sync_with_ctx( - q, target_thread, call_then_finish_sync, &t); + q, target_thread, call_then_finish_task, &t); } static int do_proxy_callback(em_proxying_queue* q, @@ -465,7 +465,8 @@ int emscripten_proxy_callback(em_proxying_queue* q, void (*callback)(void*), void (*cancel)(void*), void* arg) { - // Allocate the em_proxying_ctx and the user ctx as a single block. + // Allocate the em_proxying_ctx and the user ctx as a single block that will + // be freed when the `em_proxying_ctx` is freed. struct block { em_proxying_ctx ctx; callback_ctx cb_ctx; @@ -483,3 +484,99 @@ int emscripten_proxy_callback(em_proxying_queue* q, &block->cb_ctx, &block->ctx); } + +typedef struct promise_ctx { + void (*func)(em_proxying_ctx*, void*); + void* arg; + em_promise_t promise; +} promise_ctx; + +static void promise_call(em_proxying_ctx* ctx, void* arg) { + promise_ctx* promise_ctx = arg; + promise_ctx->func(ctx, promise_ctx->arg); +} + +static void promise_fulfill(void* arg) { + promise_ctx* promise_ctx = arg; + emscripten_promise_resolve(promise_ctx->promise, EM_PROMISE_FULFILL, NULL); + emscripten_promise_destroy(promise_ctx->promise); +} + +static void promise_reject(void* arg) { + promise_ctx* promise_ctx = arg; + emscripten_promise_resolve(promise_ctx->promise, EM_PROMISE_REJECT, NULL); + emscripten_promise_destroy(promise_ctx->promise); +} + +static em_promise_t do_proxy_promise(em_proxying_queue* q, + pthread_t target_thread, + void (*func)(em_proxying_ctx*, void*), + void* arg, + em_promise_t promise, + em_proxying_ctx* ctx, + promise_ctx* promise_ctx) { + *promise_ctx = (struct promise_ctx){func, arg, promise}; + if (!do_proxy_callback(q, + target_thread, + promise_call, + promise_fulfill, + promise_reject, + promise_ctx, + ctx)) { + emscripten_promise_resolve(promise, EM_PROMISE_REJECT, NULL); + return promise; + } + // Return a separate promise to ensure that the internal promise will stay + // alive until the callbacks are called. + em_promise_t ret = emscripten_promise_create(); + emscripten_promise_resolve(ret, EM_PROMISE_MATCH, promise); + return ret; +} + +em_promise_t emscripten_proxy_promise_with_ctx(em_proxying_queue* q, + pthread_t target_thread, + void (*func)(em_proxying_ctx*, + void*), + void* arg) { + em_promise_t promise = emscripten_promise_create(); + // Allocate the em_proxying_ctx and promise ctx as a single block that will be + // freed when the `em_proxying_ctx` is freed. + struct block { + em_proxying_ctx ctx; + promise_ctx promise_ctx; + }; + struct block* block = malloc(sizeof(*block)); + if (block == NULL) { + emscripten_promise_resolve(promise, EM_PROMISE_REJECT, NULL); + return promise; + } + return do_proxy_promise( + q, target_thread, func, arg, promise, &block->ctx, &block->promise_ctx); +} + +em_promise_t emscripten_proxy_promise(em_proxying_queue* q, + pthread_t target_thread, + void (*func)(void*), + void* arg) { + em_promise_t promise = emscripten_promise_create(); + // Allocate the em_proxying_ctx, promise ctx, and user task as a single block + // that will be freed when the `em_proxying_ctx` is freed. + struct block { + em_proxying_ctx ctx; + promise_ctx promise_ctx; + task task; + }; + struct block* block = malloc(sizeof(*block)); + if (block == NULL) { + emscripten_promise_resolve(promise, EM_PROMISE_REJECT, NULL); + return promise; + } + block->task = (task){.func = func, .arg = arg}; + return do_proxy_promise(q, + target_thread, + call_then_finish_task, + &block->task, + promise, + &block->ctx, + &block->promise_ctx); +} diff --git a/test/pthread/test_pthread_proxying.c b/test/pthread/test_pthread_proxying.c index 2e931f64e47c4..8920da0be653e 100644 --- a/test/pthread/test_pthread_proxying.c +++ b/test/pthread/test_pthread_proxying.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,9 @@ pthread_t main_thread; pthread_t looper; pthread_t returner; +// Used as the main test runner thread in the proxy_promise* tests. +pthread_t worker; + // The queue used to send work to both `looper` and `returner`. em_proxying_queue* proxy_queue = NULL; @@ -51,7 +55,7 @@ typedef struct widget { // Nonzero iff the widget has been run. int done; - // Only used for async_as_sync tests. + // Only used for *_with_ctx tests. em_proxying_ctx* ctx; } widget; @@ -75,6 +79,7 @@ void run_widget(widget* w) { const char* name = pthread_equal(self, main_thread) ? "main" : pthread_equal(self, looper) ? "looper" : pthread_equal(self, returner) ? "returner" + : pthread_equal(self, worker) ? "worker" : "unknown"; printf("running widget %d on %s\n", w->val, name); pthread_mutex_lock(&w->mutex); @@ -115,6 +120,13 @@ void start_and_finish_running_widget(em_proxying_ctx* ctx, void* arg) { finish_running_widget(arg); } +em_promise_result_t check_widget(void** result, void* data, void* value) { + widget* w = data; + assert(w->done); + assert(*w->out == w->val); + return EM_PROMISE_FULFILL; +} + // Main test functions void test_proxy_async(void) { @@ -220,7 +232,6 @@ void test_proxy_callback(void) { init_widget(&w10, &i, 10); // Proxy to ourselves. - emscripten_proxy_callback( proxy_queue, pthread_self(), do_run_widget, set_j, NULL, &w8); assert(!w8.done); @@ -272,7 +283,6 @@ void test_proxy_callback_with_ctx(void) { init_widget(&w13, &i, 13); // Proxy to ourselves. - emscripten_proxy_callback_with_ctx( proxy_queue, pthread_self(), start_and_finish_running_widget, set_j, NULL, &w11); assert(!w11.done); @@ -314,6 +324,186 @@ void test_proxy_callback_with_ctx(void) { destroy_widget(&w13); } +em_promise_result_t explode(void** result, void* data, void* value) { + printf("error!"); + abort(); + return EM_PROMISE_REJECT; +} + +em_promise_result_t test_promise_self(void** result, void* data, void* value) { + widget* w14 = data; + + em_promise_t promise = + emscripten_proxy_promise(proxy_queue, pthread_self(), do_run_widget, w14); + + *result = emscripten_promise_then(promise, check_widget, explode, w14); + emscripten_promise_destroy(promise); + + return EM_PROMISE_MATCH_RELEASE; +} + +em_promise_result_t +test_promise_looper(void** result, void* data, void* value) { + widget* w15 = data; + + em_promise_t promise = + emscripten_proxy_promise(proxy_queue, looper, do_run_widget, w15); + + *result = emscripten_promise_then(promise, check_widget, explode, w15); + emscripten_promise_destroy(promise); + + return EM_PROMISE_MATCH_RELEASE; +} + +em_promise_result_t +test_promise_returner(void** result, void* data, void* value) { + widget* w16 = data; + + em_promise_t promise = + emscripten_proxy_promise(proxy_queue, returner, do_run_widget, w16); + + *result = emscripten_promise_then(promise, check_widget, explode, w16); + emscripten_promise_destroy(promise); + + return EM_PROMISE_MATCH_RELEASE; +} + +void do_exit_thread(void* arg) { emscripten_runtime_keepalive_pop(); } + +em_promise_result_t exit_thread(void** result, void* data, void* value) { + // TODO: exit the thread directly rather than on the next turn of the event + // loop. + emscripten_async_call(do_exit_thread, NULL, 0); + return EM_PROMISE_FULFILL; +} + +struct promise_widgets { + widget w14, w15, w16; +}; + +void* do_test_proxy_promise(void* arg) { + struct promise_widgets* widgets = arg; + + em_promise_t start = emscripten_promise_create(); + em_promise_t test1 = + emscripten_promise_then(start, test_promise_self, explode, &widgets->w14); + em_promise_t test2 = + emscripten_promise_then(test1, test_promise_looper, explode, &widgets->w15); + em_promise_t test3 = emscripten_promise_then( + test2, test_promise_returner, explode, &widgets->w16); + em_promise_t end = emscripten_promise_then(test3, exit_thread, explode, NULL); + + emscripten_promise_resolve(start, EM_PROMISE_FULFILL, NULL); + emscripten_promise_destroy(start); + emscripten_promise_destroy(test1); + emscripten_promise_destroy(test2); + emscripten_promise_destroy(test3); + emscripten_promise_destroy(end); + + emscripten_runtime_keepalive_push(); + return NULL; +} + +void test_proxy_promise() { + printf("Testing promise proxying\n"); + + int i = 0; + struct promise_widgets widgets; + init_widget(&widgets.w14, &i, 14); + init_widget(&widgets.w15, &i, 15); + init_widget(&widgets.w16, &i, 16); + + pthread_create(&worker, NULL, do_test_proxy_promise, &widgets); + pthread_join(worker, NULL); + + destroy_widget(&widgets.w14); + destroy_widget(&widgets.w15); + destroy_widget(&widgets.w16); +} + +em_promise_result_t +test_promise_ctx_self(void** result, void* data, void* value) { + widget* w17 = data; + + em_promise_t promise = emscripten_proxy_promise_with_ctx( + proxy_queue, pthread_self(), start_running_widget, w17); + + *result = emscripten_promise_then(promise, check_widget, explode, w17); + emscripten_promise_destroy(promise); + + return EM_PROMISE_MATCH_RELEASE; +} + +em_promise_result_t +test_promise_ctx_looper(void** result, void* data, void* value) { + widget* w18 = data; + + em_promise_t promise = emscripten_proxy_promise_with_ctx( + proxy_queue, looper, start_and_finish_running_widget, w18); + + *result = emscripten_promise_then(promise, check_widget, explode, w18); + emscripten_promise_destroy(promise); + + return EM_PROMISE_MATCH_RELEASE; +} + +em_promise_result_t +test_promise_ctx_returner(void** result, void* data, void* value) { + widget* w19 = data; + + em_promise_t promise = emscripten_proxy_promise_with_ctx( + proxy_queue, returner, start_running_widget, w19); + + *result = emscripten_promise_then(promise, check_widget, explode, w19); + emscripten_promise_destroy(promise); + + return EM_PROMISE_MATCH_RELEASE; +} + +struct promise_ctx_widgets { + widget w17, w18, w19; +}; + +void* do_test_proxy_promise_with_ctx(void* arg) { + struct promise_ctx_widgets* widgets = arg; + + em_promise_t start = emscripten_promise_create(); + em_promise_t test1 = emscripten_promise_then( + start, test_promise_ctx_self, explode, &widgets->w17); + em_promise_t test2 = emscripten_promise_then( + test1, test_promise_ctx_looper, explode, &widgets->w18); + em_promise_t test3 = emscripten_promise_then( + test2, test_promise_ctx_returner, explode, &widgets->w19); + em_promise_t end = emscripten_promise_then(test3, exit_thread, explode, NULL); + + emscripten_promise_resolve(start, EM_PROMISE_FULFILL, NULL); + emscripten_promise_destroy(start); + emscripten_promise_destroy(test1); + emscripten_promise_destroy(test2); + emscripten_promise_destroy(test3); + emscripten_promise_destroy(end); + + emscripten_runtime_keepalive_push(); + return NULL; +} + +void test_proxy_promise_with_ctx() { + printf("Testing promise_with_ctx proxying\n"); + + int i = 0; + struct promise_ctx_widgets widgets; + init_widget(&widgets.w17, &i, 17); + init_widget(&widgets.w18, &i, 18); + init_widget(&widgets.w19, &i, 19); + + pthread_create(&worker, NULL, do_test_proxy_promise_with_ctx, &widgets); + pthread_join(worker, NULL); + + destroy_widget(&widgets.w17); + destroy_widget(&widgets.w18); + destroy_widget(&widgets.w19); +} + typedef struct increment_to_arg { em_proxying_queue* queue; int* ip; @@ -446,6 +636,8 @@ int main(int argc, char* argv[]) { test_proxy_sync_with_ctx(); test_proxy_callback(); test_proxy_callback_with_ctx(); + test_proxy_promise(); + test_proxy_promise_with_ctx(); should_quit = 1; pthread_join(looper, NULL); diff --git a/test/pthread/test_pthread_proxying.out b/test/pthread/test_pthread_proxying.out index 9b445fbb4342e..4f1862dad0b4b 100644 --- a/test/pthread/test_pthread_proxying.out +++ b/test/pthread/test_pthread_proxying.out @@ -16,6 +16,14 @@ Testing callback_with_ctx proxying running widget 11 on main running widget 12 on looper running widget 13 on returner +Testing promise proxying +running widget 14 on worker +running widget 15 on looper +running widget 16 on returner +Testing promise_with_ctx proxying +running widget 17 on worker +running widget 18 on looper +running widget 19 on returner Testing tasks queue growth Testing proxying queue growth work diff --git a/test/pthread/test_pthread_proxying_canceled_work.c b/test/pthread/test_pthread_proxying_canceled_work.c index 816dcfbb23ad7..c21a19553f495 100644 --- a/test/pthread/test_pthread_proxying_canceled_work.c +++ b/test/pthread/test_pthread_proxying_canceled_work.c @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -7,6 +9,30 @@ em_proxying_queue* queue; void explode(void* arg) { assert(0 && "the work should not be run!"); } +em_promise_result_t explode_reject(void** result, void* data, void* val) { + assert(0 && "unexpected promise result"); + return EM_PROMISE_REJECT; +} + +em_promise_result_t fulfill(void** result, void* data, void* val) { + return EM_PROMISE_FULFILL; +} + +void fulfill_promise(void* arg) { + em_promise_t promise = arg; + emscripten_promise_resolve(promise, EM_PROMISE_FULFILL, NULL); +} + +// The promises we need to wait for at the end of the test. +#define MAX_PROMISES 6 +int promise_count = 0; +em_promise_t promises[MAX_PROMISES]; + +void add_promise(em_promise_t promise) { + assert(promise_count < MAX_PROMISES); + promises[promise_count++] = promise; +} + void set_flag(void* flag) { // Schedule the flag to be set on the next turn of the event loop so that we // can be sure cleanup has finished first. We need to use EM_ASM and JS here @@ -53,6 +79,11 @@ void test_cancel_then_proxy() { emscripten_proxy_callback(queue, thread, explode, explode, explode, NULL); assert(ret == 0); + // Or should result in a rejected promise. + em_promise_t promise = emscripten_proxy_promise(queue, thread, explode, NULL); + add_promise(emscripten_promise_then(promise, explode_reject, fulfill, NULL)); + emscripten_promise_destroy(promise); + pthread_join(thread, NULL); } @@ -74,6 +105,11 @@ void test_exit_then_proxy() { emscripten_proxy_callback(queue, thread, explode, explode, explode, NULL); assert(ret == 0); + // Or should result in a rejected promise. + em_promise_t promise = emscripten_proxy_promise(queue, thread, explode, NULL); + add_promise(emscripten_promise_then(promise, explode_reject, fulfill, NULL)); + emscripten_promise_destroy(promise); + pthread_join(thread, NULL); } @@ -117,7 +153,17 @@ void test_proxy_then_cancel() { } // The pending proxied work should be canceled when the thread is canceled. - int ret = emscripten_proxy_sync(queue, thread, explode, NULL); + em_promise_t promise = emscripten_proxy_promise(queue, thread, explode, NULL); + add_promise(emscripten_promise_then(promise, explode_reject, fulfill, NULL)); + emscripten_promise_destroy(promise); + + promise = emscripten_promise_create(); + int ret = emscripten_proxy_callback( + queue, thread, explode, explode, fulfill_promise, promise); + assert(ret == 1); + add_promise(promise); + + ret = emscripten_proxy_sync(queue, thread, explode, NULL); assert(ret == 0); pthread_join(thread, NULL); @@ -134,104 +180,20 @@ void test_proxy_then_exit() { } // The pending proxied work should be canceled when the thread exits. - int ret = emscripten_proxy_sync(queue, thread, explode, NULL); - assert(ret == 0); - - pthread_join(thread, NULL); -} - -struct callback_info { - pthread_t worker; - pthread_t proxier; - _Atomic int worker_running; - _Atomic int should_exit; - _Atomic int callback_called; -}; - -void* report_running_then_cancel(void* arg) { - struct callback_info* info = arg; - - info->worker_running = 1; - - while (!info->should_exit) { - } - - // The callback will never be dequeued because we exit before returning to the - // event loop. - pthread_cancel(pthread_self()); - pthread_testcancel(); - assert(0 && "thread should have been canceled!"); - return NULL; -} - -void* report_running_then_exit(void* arg) { - struct callback_info* info = arg; - - info->worker_running = 1; - - while (!info->should_exit) { - } - - // The callback will never be dequeued because we exit before returning to the - // event loop. - pthread_exit(NULL); - assert(0 && "thread should have been canceled!"); - return NULL; -} - -void cancel_callback(void* arg) { - struct callback_info* info = arg; - info->callback_called = 1; -} - -void* proxy_with_callback(void* arg) { - struct callback_info* info = arg; - - while (!info->worker_running) { - } + em_promise_t promise = emscripten_proxy_promise(queue, thread, explode, NULL); + add_promise(emscripten_promise_then(promise, explode_reject, fulfill, NULL)); + emscripten_promise_destroy(promise); + promise = emscripten_promise_create(); int ret = emscripten_proxy_callback( - queue, info->worker, explode, explode, cancel_callback, info); + queue, thread, explode, explode, fulfill_promise, promise); assert(ret == 1); + add_promise(promise); - info->should_exit = 1; - - // Keep runtime alive so we can receive the cancellation callback. - emscripten_exit_with_live_runtime(); -} - -void test_proxy_callback_then_cancel() { - printf("testing callback proxy followed by cancel\n"); - - struct callback_info info = {0}; - - pthread_create(&info.worker, NULL, report_running_then_cancel, &info); - pthread_create(&info.proxier, NULL, proxy_with_callback, &info); - - while (!info.callback_called) { - } - - pthread_join(info.worker, NULL); - - pthread_cancel(info.proxier); - pthread_join(info.proxier, NULL); -} - -void test_proxy_callback_then_exit() { - printf("testing callback proxy followed by exit\n"); - - struct callback_info info = {0}; - - pthread_create(&info.worker, NULL, report_running_then_exit, &info); - pthread_create(&info.proxier, NULL, proxy_with_callback, &info); - - while (!info.callback_called) { - } - - pthread_join(info.worker, NULL); + ret = emscripten_proxy_sync(queue, thread, explode, NULL); + assert(ret == 0); - pthread_cancel(info.proxier); - pthread_join(info.proxier, NULL); + pthread_join(thread, NULL); } enum proxy_kind { SYNC, CALLBACK }; @@ -369,6 +331,19 @@ void test_cancel_in_progress() { } } +void do_exit(void* arg) { emscripten_runtime_keepalive_pop(); } + +em_promise_result_t cleanup(void** result, void* data, void* val) { + for (int i = 0; i < MAX_PROMISES; i++) { + emscripten_promise_destroy(promises[i]); + } + em_proxying_queue_destroy(queue); + // TODO: exit directly here + emscripten_async_call(do_exit, NULL, 0); + printf("done\n"); + return EM_PROMISE_FULFILL; +} + int main() { queue = em_proxying_queue_create(); pthread_key_create(&dtor_key, set_flag); @@ -377,12 +352,14 @@ int main() { test_exit_then_proxy(); test_proxy_then_cancel(); test_proxy_then_exit(); - test_proxy_callback_then_cancel(); - test_proxy_callback_then_exit(); test_cancel_in_progress(); - em_proxying_queue_destroy(queue); - - printf("done\n"); + // Wait for the promises to resolve. + assert(promise_count == MAX_PROMISES); + em_promise_t done = emscripten_promise_all(promises, NULL, MAX_PROMISES); + emscripten_promise_destroy( + emscripten_promise_then(done, cleanup, explode_reject, NULL)); + emscripten_promise_destroy(done); + emscripten_runtime_keepalive_push(); } diff --git a/test/pthread/test_pthread_proxying_canceled_work.out b/test/pthread/test_pthread_proxying_canceled_work.out index 07e0a9bc8d04c..861beed8b0f42 100644 --- a/test/pthread/test_pthread_proxying_canceled_work.out +++ b/test/pthread/test_pthread_proxying_canceled_work.out @@ -2,8 +2,6 @@ testing cancel followed by proxy testing exit followed by proxy testing proxy followed by cancel testing proxy followed by exit -testing callback proxy followed by cancel -testing callback proxy followed by exit testing cancellation of in-progress work checking pattern 0 finishing task 0