From 264736013b85962b75ba202060f2b20a126fcbf9 Mon Sep 17 00:00:00 2001 From: Isaac de Wolff Date: Fri, 3 Jan 2025 12:21:12 +0100 Subject: [PATCH 01/13] Add uc_cfunction_ex_t type with as extra above uc_cfunction_t a feedback function which is called on destruction, plus a way to allocate extra user bytes in the creator function. --- include/ucode/types.h | 60 ++++++++++++++++++++++++++++++++++++++++ lib/debug.c | 6 ++-- types.c | 64 +++++++++++++++++++++++++++++++++++++++++-- vm.c | 2 +- 4 files changed, 126 insertions(+), 6 deletions(-) diff --git a/include/ucode/types.h b/include/ucode/types.h index f149b98b..c371078d 100644 --- a/include/ucode/types.h +++ b/include/ucode/types.h @@ -192,6 +192,16 @@ typedef struct { char name[]; } uc_cfunction_t; +// feedback will be called on destroy of uc_cfunction_ex_t +typedef void (*uc_cfn_feedback_t)( uc_value_t * ); + +typedef struct { + uc_cfunction_t header; + intptr_t magic; + uc_cfn_feedback_t feedback; + char name[]; +} uc_cfunction_ex_t; + typedef struct { const char *name; uc_value_t *proto; @@ -423,6 +433,56 @@ size_t ucv_object_length(uc_value_t *); uc_value_t *ucv_cfunction_new(const char *, uc_cfn_ptr_t); +// Do not call ucv_cfunction_ex_helper() directly, use one of the helper functions below +bool +ucv_cfunction_ex_helper( int, void * ); + +static inline uc_value_t * +ucv_cfunction_ex_new( const char *name, uc_cfn_ptr_t fn, uc_cfn_feedback_t fb, size_t extra_user_bytes ) +{ + void *args[ 4 ] = { (void *)name, fn, fb, (void *)extra_user_bytes }; + if( ucv_cfunction_ex_helper( 0, args ) ) + return args[ 0 ]; + return 0; +} + +static inline intptr_t +ucv_cfunction_ex_get_magic() +{ + intptr_t ret; + if( ucv_cfunction_ex_helper( 1, &ret ) ) + return ret; + return -1; +} + +static inline bool +ucv_is_cfunction_ex( uc_value_t *uv ) +{ + if( ucv_type(uv) != UC_CFUNCTION ) + return false; + const char *magic; + if( !ucv_cfunction_ex_helper( 2, &magic ) ) + return false; + return 0 == strcmp( ((uc_cfunction_t *)uv)->name, magic ); +} + +static inline const char * +uvc_cfunction_get_name( uc_cfunction_t *uv ) +{ + if( ucv_is_cfunction_ex( &uv->header ) ) + return( ((uc_cfunction_ex_t *)uv)->name ); + return uv->name; +} + +static inline void * +ucv_cfunction_ex_get_user( uc_value_t *uv ) +{ + if( !ucv_is_cfunction_ex( uv ) ) + return 0; + uc_cfunction_ex_t *fn = (uc_cfunction_ex_t *)uv; + return (fn->name + ALIGN( strlen(fn->name)+1) ); +} + uc_value_t *ucv_closure_new(uc_vm_t *, uc_function_t *, bool); uc_resource_type_t *ucv_resource_type_add(uc_vm_t *, const char *, uc_value_t *, void (*)(void *)); diff --git a/lib/debug.c b/lib/debug.c index 0b227d9f..bd6c0c14 100644 --- a/lib/debug.c +++ b/lib/debug.c @@ -419,7 +419,7 @@ print_memdump(uc_vm_t *vm, FILE *out) print_function_srcpos(ctx.out, frame->closure); else fprintf(ctx.out, " @ [C function \"%s\"]", - frame->cfunction->name); + uvc_cfunction_get_name( frame->cfunction ) ); } else if (i == frame->stackframe) { fprintf(ctx.out, " (callee)"); @@ -428,7 +428,7 @@ print_memdump(uc_vm_t *vm, FILE *out) print_function_srcpos(ctx.out, frame->closure); else fprintf(ctx.out, " @ [C function \"%s\"]", - frame->cfunction->name); + uvc_cfunction_get_name( frame->cfunction ) ); } else if (i > frame->stackframe) { fprintf(ctx.out, " (argument #%zu)", @@ -438,7 +438,7 @@ print_memdump(uc_vm_t *vm, FILE *out) print_function_srcpos(ctx.out, frame->closure); else fprintf(ctx.out, " @ [C function \"%s\"]", - frame->cfunction->name); + uvc_cfunction_get_name( frame->cfunction ) ); } } diff --git a/types.c b/types.c index 5dbf6a86..364d1b7f 100644 --- a/types.c +++ b/types.c @@ -288,6 +288,15 @@ ucv_free(uc_value_t *uv, bool retain) ucv_put_value(upval->value, retain); break; + case UC_CFUNCTION: + if( ucv_is_cfunction_ex( uv ) ) + { + uc_cfunction_ex_t *cfn = (uc_cfunction_ex_t *)uv; + if( cfn->feedback ) + cfn->feedback( uv ); + } + break; + case UC_PROGRAM: program = (uc_program_t *)uv; @@ -1150,6 +1159,54 @@ ucv_cfunction_new(const char *name, uc_cfn_ptr_t fptr) return &cfn->header; } +bool +ucv_cfunction_ex_helper( int cmd, void *args ) +{ + static const char *strmagic = +#if INTPTR_MAX == INT64_MAX + "\xFF" "FEEDBA"; // 8 bytes, including closing zero +#else + "\xFF" "FB"; // 4 bytes, including closing zero +#endif + + if( 0 == cmd ) // ucv_cfunction_ex_new() + { + void **pargs = args; + uc_cfunction_ex_t *cfn; + + size_t namelen = 0; + const char *name = (const char *)pargs[ 0 ]; + + if (name) + namelen = strlen(name); + + cfn = xalloc(sizeof(*cfn) + ALIGN( namelen + 1 ) + (size_t)pargs[3] ); + cfn->header.header.type = UC_CFUNCTION; + cfn->header.header.refcount = 1; + cfn->header.cfn = pargs[ 1 ]; + cfn->magic = *(intptr_t *)strmagic; + cfn->feedback = pargs[ 2 ]; + + if( name ) + strcpy( cfn->name, name ); + + pargs[ 0 ] = cfn; + return true; + } + if( 1 == cmd ) // ucv_cfunction_ex_get_magic() + { + intptr_t *ret = args; + *ret = *(intptr_t *)strmagic; + return true; + } + if( 2 == cmd ) // ucv_cfunction_ex_get_magic string + { + const char **ret = args; + *ret = strmagic; + return true; + } + return false; +} uc_value_t * ucv_closure_new(uc_vm_t *vm, uc_function_t *function, bool arrow_fn) @@ -1851,15 +1908,18 @@ ucv_to_stringbuf_formatted(uc_vm_t *vm, uc_stringbuf_t *pb, uc_value_t *uv, size break; case UC_CFUNCTION: + { cfunction = (uc_cfunction_t *)uv; + const char *name = uvc_cfunction_get_name( cfunction ); ucv_stringbuf_printf(pb, "%sfunction%s%s(...) { [native code] }%s", json ? "\"" : "", - cfunction->name[0] ? " " : "", - cfunction->name[0] ? cfunction->name : "", + name[0] ? " " : "", + name[0] ? name : "", json ? "\"" : ""); break; + } case UC_RESOURCE: resource = (uc_resource_t *)uv; diff --git a/vm.c b/vm.c index fc322c5d..a3dc05b6 100644 --- a/vm.c +++ b/vm.c @@ -949,7 +949,7 @@ uc_vm_capture_stacktrace(uc_vm_t *vm, size_t i) name = "[anonymous function]"; } else { - name = frame->cfunction->name; + name = (char *)uvc_cfunction_get_name( frame->cfunction ); } ucv_object_add(entry, "function", ucv_string_new(name)); From 05aa68030c89e01dac8e2f775997226637fe0c74 Mon Sep 17 00:00:00 2001 From: Isaac de Wolff Date: Fri, 3 Jan 2025 12:59:51 +0100 Subject: [PATCH 02/13] Add a way to access the TLS value in libucode from outside --- include/ucode/types.h | 21 ++++++++++++++++++++- types.c | 27 ++++++++++++++++++++------- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/include/ucode/types.h b/include/ucode/types.h index c371078d..09aa248c 100644 --- a/include/ucode/types.h +++ b/include/ucode/types.h @@ -285,7 +285,26 @@ typedef struct { uc_list_t object_iterators; } uc_thread_context_t; -__hidden uc_thread_context_t *uc_thread_context_get(void); +/* Do not call uc_thread_context_helper() directly, use one of the helper functions below */ +uc_thread_context_t *uc_thread_context_helper( int cmd, void * ); + +static inline uc_thread_context_t * +uc_thread_context_get(void) +{ + return uc_thread_context_helper( 0, 0 ); +} + +static inline uc_thread_context_t * +uc_thread_context_peek(void) +{ + return uc_thread_context_helper( 1, 0 ); +} + +static inline uc_thread_context_t * +uc_thread_context_exchange( uc_thread_context_t *tc ) +{ + return uc_thread_context_helper( 2, tc ); +} /* VM definitions */ diff --git a/types.c b/types.c index 364d1b7f..f323d983 100644 --- a/types.c +++ b/types.c @@ -2541,13 +2541,26 @@ uc_search_path_init(uc_search_path_t *search_path) static __thread uc_thread_context_t *tls_ctx; uc_thread_context_t * -uc_thread_context_get(void) +uc_thread_context_helper( int cmd, void *args ) { - if (tls_ctx == NULL) { - tls_ctx = xalloc(sizeof(*tls_ctx)); - tls_ctx->object_iterators.prev = &tls_ctx->object_iterators; - tls_ctx->object_iterators.next = &tls_ctx->object_iterators; + if( 0 == cmd ) // uc_thread_context_get() + { + if (tls_ctx == NULL) { + tls_ctx = xalloc(sizeof(*tls_ctx)); + tls_ctx->object_iterators.prev = &tls_ctx->object_iterators; + tls_ctx->object_iterators.next = &tls_ctx->object_iterators; + } + return tls_ctx; } - - return tls_ctx; + if( 1 == cmd ) // uc_thread_context_peek() + { + return tls_ctx; + } + if( 2 == cmd ) // uc_thread_context_exchange() + { + uc_thread_context_t *cur = tls_ctx; + tls_ctx = args; + return cur; + } + return 0; } From 21f8ff2d7b7423a17b48f6d48de6edd494955f22 Mon Sep 17 00:00:00 2001 From: Isaac de Wolff Date: Fri, 3 Jan 2025 14:32:35 +0100 Subject: [PATCH 03/13] lib: Introduce async library This library adds setTimout(), setInterval(), setImmediate(), clearTimeout(), Promise(), PromiseAll(), PromiseAny(), PromiseRace(), PromiseAllSettled(), throw(), uptime() and PumpEvents(). And it adds a c API to offload work to another thread. This is (partly) demonstrated in examples/async-worker.c. Further is adds a c API to call directly into the script from another thread. ('Alien' calls). This is demonstrated in examples/async-alien.c. This c API adds no link-time dependencies, everything is handled through a registry entry. The script functions (are supposed to) behave the same as their ECMAscript counterparts, except throw(). throw() is a function, not a keyword. I injected 1 inline function in uc_vm_execute() in libucode, after the central uc_vm_execute_chunk() to pump events until no more timers and promises are pending, It's also possible to explicitly pump inside the script, using PumpEvents(), which in that case can also be used as delay() function. --- CMakeLists.txt | 8 + examples/async-alien.c | 204 ++++++ examples/async-worker.c | 189 +++++ include/ucode/async.h | 354 ++++++++++ lib/async/alien.c | 225 ++++++ lib/async/alien.h | 71 ++ lib/async/callback.c | 157 +++++ lib/async/callback.h | 70 ++ lib/async/manager.c | 625 +++++++++++++++++ lib/async/manager.h | 177 +++++ lib/async/promise.c | 1459 +++++++++++++++++++++++++++++++++++++++ lib/async/promise.h | 56 ++ lib/async/queuer.c | 439 ++++++++++++ lib/async/queuer.h | 65 ++ lib/async/timer.c | 335 +++++++++ lib/async/timer.h | 58 ++ vm.c | 3 + 17 files changed, 4495 insertions(+) create mode 100644 examples/async-alien.c create mode 100644 examples/async-worker.c create mode 100644 include/ucode/async.h create mode 100644 lib/async/alien.c create mode 100644 lib/async/alien.h create mode 100644 lib/async/callback.c create mode 100644 lib/async/callback.h create mode 100644 lib/async/manager.c create mode 100644 lib/async/manager.h create mode 100644 lib/async/promise.c create mode 100644 lib/async/promise.h create mode 100644 lib/async/queuer.c create mode 100644 lib/async/queuer.h create mode 100644 lib/async/timer.c create mode 100644 lib/async/timer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index bdf07383..47320a1f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,6 +74,7 @@ option(SOCKET_SUPPORT "Socket plugin support" ON) option(ZLIB_SUPPORT "Zlib plugin support" ${DEFAULT_ZLIB_SUPPORT}) option(DIGEST_SUPPORT "Digest plugin support" ${DEFAULT_DIGEST_SUPPORT}) option(DIGEST_SUPPORT_EXTENDED "Enable additional hash algorithms" ${DEFAULT_DIGEST_SUPPORT}) +option(ASYNC_SUPPORT "Async plugin support" ON) set(LIB_SEARCH_PATH "${CMAKE_INSTALL_PREFIX}/lib/ucode/*.so:${CMAKE_INSTALL_PREFIX}/share/ucode/*.uc:./*.so:./*.uc" CACHE STRING "Default library search path") string(REPLACE ":" "\", \"" LIB_SEARCH_DEFINE "${LIB_SEARCH_PATH}") @@ -310,6 +311,13 @@ if(DIGEST_SUPPORT) target_link_libraries(digest_lib ${libmd}) endif() +if(ASYNC_SUPPORT) + set(LIBRARIES ${LIBRARIES} async_lib) + add_library(async_lib MODULE lib/async/manager.c lib/async/alien.c lib/async/promise.c lib/async/timer.c lib/async/callback.c lib/async/queuer.c) + set_target_properties(async_lib PROPERTIES OUTPUT_NAME async PREFIX "") + target_link_options(async_lib PRIVATE ${UCODE_MODULE_LINK_OPTIONS}) +endif() + if(UNIT_TESTING) enable_testing() add_definitions(-DUNIT_TESTING) diff --git a/examples/async-alien.c b/examples/async-alien.c new file mode 100644 index 00000000..a850b19a --- /dev/null +++ b/examples/async-alien.c @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/******* + * This example demonstrates how to do 'alien' calls from outside threads + * to the script. + */ + +#include +#include + +#include +#include +#include +#include + + +#define MULTILINE_STRING(...) #__VA_ARGS__ + +static const char *program_code = MULTILINE_STRING( + {% + import * as async from 'async'; + + function CalledFromAlien( name, counter ) + { + print( `${async.uptime()} '${name}' ${counter}\n` ); + return counter + 7; + } + + init_alien( 'alienA', CalledFromAlien ); + init_alien( 'alienB', CalledFromAlien ); + init_alien( 'alienC', CalledFromAlien ); + %} +); + +static uc_parse_config_t config = { + .strict_declarations = false, + .lstrip_blocks = true, + .trim_blocks = true +}; + +static uc_value_t * +init_alien(uc_vm_t *vm, size_t nargs); + +int main(int argc, char **argv) +{ + int exit_code = 0; + + /* create a source buffer containing the program code */ + uc_source_t *src = uc_source_new_buffer("my program", strdup(program_code), strlen(program_code)); + + /* initialize default module search path */ + uc_search_path_init(&config.module_search_path); + uc_search_path_add(&config.module_search_path,"../" ); + + /* compile source buffer into function */ + char *syntax_error = NULL; + uc_program_t *program = uc_compile(&config, src, &syntax_error); + + /* release source buffer */ + uc_source_put(src); + + /* check if compilation failed */ + if (!program) { + fprintf(stderr, "Failed to compile program: %s\n", syntax_error); + /* free search module path vector */ + uc_search_path_free(&config.module_search_path); + return 1; + } + + /* initialize VM context */ + uc_vm_t vm = { 0 }; + uc_vm_init(&vm, &config); + + /* load standard library into global VM scope */ + uc_stdlib_load(uc_vm_scope_get(&vm)); + + /* register our native function as "init_alien" */ + uc_function_register(uc_vm_scope_get(&vm), "init_alien", init_alien ); + + /* execute program function */ + int return_code = uc_vm_execute(&vm, program, NULL); + + /* release program */ + uc_program_put(program); + + /* handle return status */ + if (return_code == ERROR_COMPILE || return_code == ERROR_RUNTIME) { + printf("An error occurred while running the program\n"); + exit_code = 1; + } + + /* free VM context */ + uc_vm_free(&vm); + + /* free search module path vector */ + uc_search_path_free(&config.module_search_path); + + return exit_code; +} + +struct alien_data +{ + char *name; + uc_value_t *callback; + int counter; + int return_value; + const uc_async_alient_t *alien_manager; + pthread_t tid; +}; + +static int alien_call( uc_vm_t *vm, void *p, int flags ) +{ + struct alien_data *worker = p; + if( flags & UC_ASYNC_CALLBACK_FLAG_EXECUTE ) + { + uc_value_push(ucv_get( worker->callback)); + uc_value_push(ucv_string_new( worker->name ) ); + uc_value_push(ucv_int64_new( worker->counter ) ); + + int ex = uc_call(2); + + if( EXCEPTION_NONE == ex ) + { + uc_value_t *ret = uc_value_pop(); + worker->return_value = ucv_int64_get( ret ); + ucv_put( ret ); + return 0; + } + return INT_MIN; + } + return INT_MIN; +} + +// Function runs in worker thread +static void *worker_thread( void *p ) +{ + struct alien_data *worker = p; + printf( "In worker thread %llu\n", (long long unsigned)worker->tid ); + + // Call the callback function 1000 times + for( int i=0; i<1000; i++ ) + { + worker->counter = i; + uc_async_alien_call( worker->alien_manager, alien_call, worker ); + printf( "outside script %s: func(%d) -> %d\n", worker->name, worker->counter, worker->return_value ); + // and wait for a msec + struct timespec wait; + wait.tv_sec = 0; + wait.tv_nsec = 1000000; + nanosleep(&wait, 0); + } + + // Cleanup + free( worker->name ); + uc_async_alien_free( &worker->alien_manager ); + free( worker ); + + // Alas we'll leak some thread data, as we can't thread_join() from here. + return 0; +} + + +static uc_value_t * +init_alien(uc_vm_t *vm, size_t nargs) +{ + char *name = ucv_to_string( vm, uc_fn_arg( 0 ) ); + uc_value_t *callback = uc_fn_arg( 1 ); + + // try to create an alien manager + const uc_async_alient_t *alien_manager = uc_async_alien_new( vm ); + if( !alien_manager ) + { + uc_vm_raise_exception( vm, EXCEPTION_RUNTIME, "Cannot create alien manager" ); + return 0; + } + + // setup worker struct + struct alien_data *worker = xalloc( sizeof( struct alien_data )); + worker->name = name; + worker->callback = ucv_get( callback ); + worker->alien_manager = alien_manager; + + // start thread + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_create( &worker->tid,&attr,worker_thread,worker); + pthread_attr_destroy(&attr); + + return 0; +} diff --git a/examples/async-worker.c b/examples/async-worker.c new file mode 100644 index 00000000..6debf069 --- /dev/null +++ b/examples/async-worker.c @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/******* + * This example demonstrates how to offload work to another thread to fullfill + * a promise. + */ + +#include +#include + +#include +#include +#include +#include + + +#define MULTILINE_STRING(...) #__VA_ARGS__ + +static const char *program_code = MULTILINE_STRING( + {% + import * as async from 'async'; + + let promises = [ worker( 1000 ), worker( 2000 ), worker( 3000 ) ]; + + async.PromiseAll( promises ) + .then( (res)=> + { + print( 'result:', res, '\n' ); + }); + %} +); + +static uc_parse_config_t config = { + .strict_declarations = false, + .lstrip_blocks = true, + .trim_blocks = true +}; + +static uc_value_t * +worker(uc_vm_t *vm, size_t nargs); + +int main(int argc, char **argv) +{ + int exit_code = 0; + + /* create a source buffer containing the program code */ + uc_source_t *src = uc_source_new_buffer("my program", strdup(program_code), strlen(program_code)); + + /* initialize default module search path */ + uc_search_path_init(&config.module_search_path); + uc_search_path_add(&config.module_search_path,"../" ); + + /* compile source buffer into function */ + char *syntax_error = NULL; + uc_program_t *program = uc_compile(&config, src, &syntax_error); + + /* release source buffer */ + uc_source_put(src); + + /* check if compilation failed */ + if (!program) { + fprintf(stderr, "Failed to compile program: %s\n", syntax_error); + /* free search module path vector */ + uc_search_path_free(&config.module_search_path); + return 1; + } + + /* initialize VM context */ + uc_vm_t vm = { 0 }; + uc_vm_init(&vm, &config); + + /* load standard library into global VM scope */ + uc_stdlib_load(uc_vm_scope_get(&vm)); + + /* register our native function as "worker" */ + uc_function_register(uc_vm_scope_get(&vm), "worker", worker); + + /* execute program function */ + int return_code = uc_vm_execute(&vm, program, NULL); + + /* release program */ + uc_program_put(program); + + /* handle return status */ + if (return_code == ERROR_COMPILE || return_code == ERROR_RUNTIME) { + printf("An error occurred while running the program\n"); + exit_code = 1; + } + + /* free VM context */ + uc_vm_free(&vm); + + /* free search module path vector */ + uc_search_path_free(&config.module_search_path); + + return exit_code; +} + +struct worker_data +{ + int value; + uc_async_promise_resolver_t *resolver; + const uc_async_callback_queuer_t *queuer; + pthread_t tid; +}; + +// function runs in script thread +static int worker_finish( uc_vm_t *vm, void *user, int flags ) +{ + struct worker_data *worker = user; + if( flags & UC_ASYNC_CALLBACK_FLAG_EXECUTE ) + { + printf( "Inside the callback for thread %llu\n", (long long unsigned)worker->tid ); + + // resolve the promise + uc_async_promise_resolve( vm, &worker->resolver, ucv_int64_new( worker->value ) ); + } + if( flags & UC_ASYNC_CALLBACK_FLAG_CLEANUP ) + { + uc_async_callback_queuer_free( &worker->queuer ); + void *res; + pthread_join( worker->tid, &res ); + free( worker ); + } + return EXCEPTION_NONE; +} + +// Function runs in worker thread +static void *worker_thread( void *p ) +{ + struct worker_data *worker = p; + printf( "In worker thread %llu\n", (long long unsigned)worker->tid ); + + // do some work (but make sure you don't get tired ;) + struct timespec wait; + wait.tv_sec = worker->value / 1000; + wait.tv_nsec = (worker->value % 1000) * 1000000; + nanosleep(&wait, 0); + + // and request a callback in the script thread. + printf( "About to request a callback from thread %llu\n", (long long unsigned)worker->tid ); + uc_async_request_callback( worker->queuer, worker_finish, worker ); + return 0; +} + + +static uc_value_t * +worker(uc_vm_t *vm, size_t nargs) +{ + int value = ucv_to_integer( uc_fn_arg(0) ); + + // try to create a promise + uc_async_promise_resolver_t *resolver = 0; + uc_value_t *promise = uc_async_promise_new( vm, &resolver ); + if( 0 == promise ) + { + uc_vm_raise_exception( vm, EXCEPTION_RUNTIME, "need the 'async' plugin to be loaded" ); + return 0; + } + + // setup worker struct + struct worker_data *worker = xalloc( sizeof( struct worker_data )); + worker->value = value; + worker->resolver = resolver; + worker->queuer = uc_async_callback_queuer_new( vm ); + + // start thread + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_create( &worker->tid,&attr,worker_thread,worker); + pthread_attr_destroy(&attr); + + // and return promise + return promise; +} diff --git a/include/ucode/async.h b/include/ucode/async.h new file mode 100644 index 00000000..1231e1d9 --- /dev/null +++ b/include/ucode/async.h @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/** + * # Async function API + * + * The 'async' module provides asynchronous functionality. + * + * The c API of this module does not cause or need any linktime + * dependencies. All is handled via a struct uc_async_manager + * pointer in the uc_vm_t object, which is initialized in the + * async.so uc_module_init() function, and which contains function pointers. + * + * This file contains static inline wrapper functions. + * + * @module async + */ + + +#ifndef UCODE_ASYNC_H +#define UCODE_ASYNC_H +#include "types.h" +#include + + +/* This is the 'public' interface of the async module */ +typedef struct uc_async_promise_resolver{} uc_async_promise_resolver_t; + +typedef struct uc_async_timer {} uc_async_timer_t; + +typedef struct uc_async_callback_queuer +{ + void (*free)( struct uc_async_callback_queuer const ** ); + + bool (*request_callback)( struct uc_async_callback_queuer const *, + int (*)( uc_vm_t *, void *, int flags ), void * ); + + uc_async_timer_t *(*create_timer)( struct uc_async_callback_queuer const *, + int (*)( uc_vm_t *, void *, int flags ), void *, uint32_t msec, bool periodic ); + + void (*free_timer)( struct uc_async_callback_queuer const *, + uc_async_timer_t **, bool clear ); +} uc_async_callback_queuer_t; + +typedef struct uc_async_alien +{ + void (*free)( struct uc_async_alien const ** ); + int (*call)( const struct uc_async_alien *, int (*)( uc_vm_t *, void *, int flags ), void * ); + + // Internally used, but you are free to queue your own callbacks with it. + const uc_async_callback_queuer_t *queuer; +} uc_async_alient_t; + +struct uc_async_manager +{ + // pump timers, promises and callbacks. + int (*event_pump)( struct uc_async_manager *, unsigned max_wait, int flags ); + + // promise functions + struct uc_value *(*new_promise)( struct uc_async_manager *, struct uc_async_promise_resolver ** ); + void (*resolve_reject)( struct uc_async_manager *, struct uc_async_promise_resolver **, struct uc_value *, bool ); + + // callback queuer functions + uc_async_callback_queuer_t const *(*new_callback_queuer)( struct uc_async_manager * ); + + // alien call function + uc_async_alient_t const *(*new_alien)( struct uc_async_manager * ); +}; + +/*** + * Function returns a pointer to an uc_async_manager if the async plugin is active. + * It is not active when it's not loaded, or when it has already finished. + * */ +static inline struct uc_async_manager *uc_async_manager_get( uc_vm_t *vm ) +{ + if( !vm ) return 0; + return (struct uc_async_manager *)ucv_resource_data(uc_vm_registry_get(vm, "async.manager"), NULL); +} + +/** + * Flags which are passed to a callback or timer function + * When 'UC_ASYNC_CALLBACK_FLAG_EXECUTE' is set, + * the function should do it's job. + * When 'UC_ASYNC_CALLBACK_FLAG_CLEANUP' is set, + * the function should cleanup resources. + * The function will not be called again. + * + * The flags can be set both in the same call. It is possible that + * 'UC_ASYNC_CALLBACK_FLAG_EXECUTE' will never be set, when the target + * vm already ended. In that case the function will only be called once, + * with the 'UC_ASYNC_CALLBACK_FLAG_CLEANUP' flag set. + */ +enum{ + UC_ASYNC_CALLBACK_FLAG_EXECUTE = 1, + UC_ASYNC_CALLBACK_FLAG_CLEANUP = 2, +}; + +/** + * Flags to be passed to vm->async_manager->pump_event(). + * Normally you'll not have to use them, the wrapper functions + * will do. + */ +enum{ + UC_ASYNC_PUMP_PUMP = 1, + UC_ASYNC_PUMP_CYCLIC = 2, + UC_ASYNC_PUMP_CLEANUP = 4, +}; + +/** + * Do one event pump cycle (handle all currently pending timers, callbacks and + * resolved promises) and then wait max msec. The wait will be shorter + * if either a timer is due, or a callback is requested. + * + * So when the function returns before msec have elapsed, a timer or + * callback is waiting. +*/ +static inline int uc_async_pump_once( uc_vm_t *vm, unsigned timeout ) +{ + struct uc_async_manager *manager = uc_async_manager_get( vm ); + if( !manager ) + return STATUS_OK; + return (*manager->event_pump)( manager, timeout, UC_ASYNC_PUMP_PUMP ); +} + +/** + * Pump events during msec. +*/ +static inline int uc_async_pump_cyclic( uc_vm_t *vm, unsigned timeout ) +{ + struct uc_async_manager *manager = uc_async_manager_get( vm ); + if( !manager ) + return STATUS_OK; + return (*manager->event_pump)( manager, timeout, UC_ASYNC_PUMP_PUMP|UC_ASYNC_PUMP_CYCLIC ); +} + +/** + * Create an (async) callback queuer. This queuer can queue callbacks to be + * executed in the script thread. + * + * This function has to be called from within the script thread. + * + * The callback queuer can queue callbacks from any thread. + * + * You have to call uc_async_callback_queuer_free() to cleanup resources. + * This can be done from any thread. +*/ +static inline uc_async_callback_queuer_t const *uc_async_callback_queuer_new( uc_vm_t *vm ) +{ + struct uc_async_manager *manager = uc_async_manager_get( vm ); + if( !manager ) + return 0; + return (*manager->new_callback_queuer)( manager ); +} + +/** + * Queue a callback to be executed in the script thread. + * It can be called from any thread, and will wake up the event pump. + * If the function returns false, the callback is not queued because the + * target vm already stopped. + * If the function returns true, the callback will be called. + * When 'flags' has UC_ASYNC_CALLBACK_EXECUTE set, the callback should be executed, + * when UC_ASYNC_CALLBACK_CLEANUP is set, you should free up resources in *user. + * When only UC_ASYNC_CALLBACK_CLEANUP is set, + * it is possible the script is no longer running, and you shouldn't access vm. +*/ +static inline bool +uc_async_request_callback( + uc_async_callback_queuer_t const *queuer, + int (*cb)( uc_vm_t *vm, void *user, int flags ), + void *user ) +{ + if( !queuer ) + return false; + return (*queuer->request_callback)( queuer, cb, user ); +} + +/** + * The counterpart of setTimeout() and setInterval(). + * For the function flags see above. + * + * The return value must eventually be free'd with uc_async_free_timer. +*/ +static inline uc_async_timer_t * +uc_async_create_timer( + uc_async_callback_queuer_t const *queuer, + int (*cb)( uc_vm_t *vm, void *user, int flags ), + void *user, uint32_t msec, bool periodic ) +{ + if( !queuer ) + return 0; + return (*queuer->create_timer)( queuer, cb, user, msec, periodic ); +} + +/** + * Free the timer object. + * If 'clear == true' this is the counterpart of clearTimeout(). + * If clear != true, the local resources are freed, but the timer + * is still active (if not expired yet). + * You will not have a way to stop the timer. The script will run until the + * timer expires, or forever if it's a periodic timer. +*/ +static inline void +uc_async_free_timer( + uc_async_callback_queuer_t const *queuer, + uc_async_timer_t **pptimer, + bool clear ) +{ + if( !queuer ) + return; + (*queuer->free_timer)( queuer, pptimer, clear ); +} + +/** + * Cleanup the callback queuer. This function can be called from any thread. +*/ +static inline void +uc_async_callback_queuer_free( + uc_async_callback_queuer_t const **queuer ) +{ + if( !queuer || !*queuer ) + return; + (*(*queuer)->free)( queuer ); +} + +/** + * Create a new promise. The return value is the promise, + * and the reolver pointer will have a resolver handle. + * + * This function has to be called from within the script thread. +*/ +static inline uc_value_t * +uc_async_promise_new( uc_vm_t *vm, uc_async_promise_resolver_t **resolver ) +{ + struct uc_async_manager *manager = uc_async_manager_get( vm ); + if( !manager ) + return 0; + return (*manager->new_promise)( manager, resolver ); +} + +/** + * Resolve the promise. The function will cleanup the resolver and + * reset the pointer. + * You are not allowed to use the resolver again. + * Normally you will call this function from a callback which is + * queued by a callback queuer. +*/ +static inline void +uc_async_promise_resolve( uc_vm_t *vm, uc_async_promise_resolver_t **resolver, uc_value_t *value ) +{ + struct uc_async_manager *manager = uc_async_manager_get( vm ); + if( !manager ) + return; + if( !resolver || !*resolver ) + return; + (*manager->resolve_reject)( manager, resolver, value, true ); +} + +/** + * Resolve the promise. The function will cleanup the resolver and + * reset the pointer. + * You are not allowed to use the resolver again. + * Normally you will call this function from a callback which is + * queued by a callback queuer. +*/ +static inline void +uc_async_promise_reject( uc_vm_t *vm, uc_async_promise_resolver_t **resolver, uc_value_t *value ) +{ + struct uc_async_manager *manager = uc_async_manager_get( vm ); + if( !manager ) + return; + if( !resolver || !*resolver ) + return; + (*manager->resolve_reject)( manager, resolver, value, false ); +} + +/**** + * Create an 'alien' object, that is an object which can call script-native + * function from an external thread + * This function has to be called from the script thread. + */ +static inline const uc_async_alient_t * +uc_async_alien_new( uc_vm_t *vm ) +{ + struct uc_async_manager *manager = uc_async_manager_get( vm ); + if( !manager ) + return 0; + + return (*manager->new_alien)( manager ); +} + +/**** + * Free the alien object. As long as an alien object is + * around, the script will not finish + */ +static inline void +uc_async_alien_free( const uc_async_alient_t **alien ) +{ + if( !alien || !*alien ) + return; + (*(*alien)->free)( alien ); +} + +/**** + * Call a script function from another thread. This call will block if the + * script is not in 'event pumping' mode. + * The return value is the return value of func. + * When the vm is already exited, or alien is NULL, the return value is INT_MIN + */ +static inline int +uc_async_alien_call( const uc_async_alient_t *alien, int (*func)( uc_vm_t *, void *, int flags ), void *user ) +{ + if( !alien ) + return INT_MIN; + return (*alien->call)( alien, func, user ); +} + +/* 'private' interface. Designed to be called from vm.c */ + +/** + * Pump events during msec, or until event loop is empty, + * (UINT_MAX is forever), + * and then clean up the async module. + * + * This function is designed to run in uc_vm_execute(), + * after the internal uc_vm_execute_chunk(), + * to keep the event pump active until all promises are resolved + * and no more timers are active. + * + * The return value is the new status. +*/ +static inline int uc_async_finish( uc_vm_t *vm, int status, unsigned timeout ) +{ + if( STATUS_OK != status ) + return status; + struct uc_async_manager *manager = uc_async_manager_get( vm ); + if( !manager ) + return status; + return (*manager->event_pump)( manager, timeout, UC_ASYNC_PUMP_PUMP|UC_ASYNC_PUMP_CYCLIC|UC_ASYNC_PUMP_CLEANUP ); +} + +#endif // UCODE_ASYNC_H \ No newline at end of file diff --git a/lib/async/alien.c b/lib/async/alien.c new file mode 100644 index 00000000..a3572265 --- /dev/null +++ b/lib/async/alien.c @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +This file is part of the async plugin for ucode +*/ +#include +#include +#include +#include +#include +#include +#include + +#include "ucode/lib.h" +#include "ucode/vm.h" +#include "ucode/types.h" +#include "ucode/async.h" + +#include "manager.h" +#include "callback.h" +#include "alien.h" +#include "queuer.h" + +#ifdef ASYNC_HAS_ALIENS + +static int +async_futex(uint32_t *uaddr, int futex_op, uint32_t val, + const struct timespec *timeout, uint32_t *uaddr2, uint32_t val3) +{ + return syscall(SYS_futex, uaddr, futex_op, val, + timeout, uaddr2, val3); +} + +static void +async_alien_enter_futex( uint32_t *futex_addr ) +{ + while (1) + { + const uint32_t zero = 0; + if( atomic_compare_exchange_strong( futex_addr, &zero, 1 ) ) + return; + + int futex_rc = async_futex(futex_addr, FUTEX_WAIT, 0, NULL, NULL, 0); + if (futex_rc == -1) + { + if (errno != EAGAIN) + { + perror("futex wait"); + exit(1); + } + } + } + +} + +void +async_alien_enter( async_manager_t *manager ) +{ + if( !manager->alien ) + return; + async_alien_enter_futex( &manager->alien->the_futex ); +} + +static void +async_alien_release_futex( uint32_t *futex_addr ) +{ + const uint32_t zero = 0; + atomic_store( futex_addr, zero ); + if( -1 == async_futex(futex_addr, FUTEX_WAKE, 0, NULL, NULL, 0) ) + { + perror( "futex wake" ); + exit( 1 ); + } +} + +void +async_alien_release( async_manager_t *manager ) +{ + if( !manager->alien ) + return; + async_alien_release_futex( &manager->alien->the_futex ); +} + +static async_alien_t * +async_alien_cast( const struct uc_async_alien *_a ) +{ + return (async_alien_t *)_a; +} + + +static void +_uc_async_alien_free( struct uc_async_alien const ** palien ) +{ + if( 0 == palien || 0 == *palien ) + return; + async_alien_t *alien = async_alien_cast( *palien ); + *palien = 0; + + async_alien_enter_futex( &alien->the_futex ); + + if( 0 == --alien->refcount ) + { + if( alien->manager ) + { + async_wakeup( alien->header.queuer ); + } + else + { + uc_async_callback_queuer_free( &alien->header.queuer ); + free( alien ); + return; + } + } + + async_alien_release_futex( &alien->the_futex ); +} + +static int +_uc_async_alien_call( const struct uc_async_alien *_alien, int (*func)( uc_vm_t *, void *, int flags ), void *user ) +{ + if( !_alien ) + return INT_MIN; + + async_alien_t *alien = async_alien_cast( _alien ); + + async_alien_enter_futex( &alien->the_futex ); + if( !alien->manager ) + { + // vm already stopped + async_alien_release_futex( &alien->the_futex ); + return INT_MIN; + } + + int todo_seq_before = alien->todo_seq; + uc_thread_context_t *push = uc_thread_context_exchange( alien->thread_context ); + uc_vm_t *other_vm = alien->thread_context->signal_handler_vm; + alien->thread_context->signal_handler_vm = push ? push->signal_handler_vm : 0; + + int ret = (func)( alien->manager->vm, user, UC_ASYNC_CALLBACK_FLAG_EXECUTE|UC_ASYNC_CALLBACK_FLAG_CLEANUP ); + + uc_thread_context_exchange( push ); + alien->thread_context->signal_handler_vm = other_vm; + + if( todo_seq_before != alien->todo_seq ) + { + // Something is added to the todolist. + // Wakeup script thread to let it reconsider it's sleep duration + async_wakeup( alien->header.queuer ); + } + + async_alien_release_futex( &alien->the_futex ); + return ret; +} + +static const uc_async_alient_t * +_uc_async_new_alien( struct uc_async_manager *_manager ) +{ + async_manager_t *manager = async_manager_cast( _manager ); + async_alien_t *alien = manager->alien; + if( 0 == alien ) + { + alien = manager->alien = xalloc( sizeof( struct async_alien ) ); + alien->thread_context = uc_thread_context_get(); + alien->the_futex = 1; + alien->manager = async_manager_link( manager ); + alien->header.queuer = manager->header.new_callback_queuer( &manager->header ); + alien->header.free = _uc_async_alien_free; + alien->header.call = _uc_async_alien_call; + } + alien->refcount++; + + return &alien->header; +} + +void +async_alien_free( async_manager_t *manager, struct async_alien *alien ) +{ + if( !alien ) + return; + + // The futex is locked, + async_manager_unlink( alien->manager ); + alien->manager = 0; + + if( 0 == alien->refcount ) + { + uc_async_callback_queuer_free( &alien->header.queuer ); + free( alien ); + return; + } + + // release it, there are still aliens around which will try to enter it. + async_alien_release_futex( &alien->the_futex ); +} + +#else // defined ASYNC_HAS_ALIENS + +static const uc_async_alient_t * +_uc_async_new_alien( struct uc_async_manager * ) +{ + return 0; +} + +#endif // nded ASYNC_HAS_ALIENS + +void +async_alien_init( async_manager_t *manager, uc_value_t *scope ) +{ + manager->header.new_alien = _uc_async_new_alien; +} + diff --git a/lib/async/alien.h b/lib/async/alien.h new file mode 100644 index 00000000..cb0074fe --- /dev/null +++ b/lib/async/alien.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +This file is part of the async plugin for ucode +*/ +#ifndef UC_ASYNC_ALIEN_H +#define UC_ASYNC_ALIEN_H + +#include "manager.h" + +#ifdef ASYNC_HAS_ALIENS +extern __hidden void +async_alien_enter( async_manager_t *manager ); + +extern __hidden void +async_alien_release( async_manager_t *manager ); + +typedef struct async_alien +{ + uc_async_alient_t header; + + // Counter which is incremented at each todo added, + // to know if an alien call should interrupt the sleep + int todo_seq; + + uint32_t the_futex; + uint32_t refcount; + + // zero if vm ended + async_manager_t *manager; + + uc_thread_context_t *thread_context; +} async_alien_t; + +extern __hidden void +async_alien_free( async_manager_t *, struct async_alien * ); + +# define ASYNC_ALIEN_ENTER(manager) async_alien_enter( manager ) +# define ASYNC_ALIEN_LEAVE(manager) async_alien_release( manager ) +# define ASYNC_ALIEN_TODO_INCREMENT(manager) if( manager->alien ) manager->alien->todo_seq++ +# define IF_NO_MORE_ALIENS(manager) \ + if( 0 == manager->alien || 0 == manager->alien->refcount ) // no more aliens + + +#else // ASYNC_HAS_ALIENS + +# define ASYNC_ALIEN_ENTER(...) do{}while(0) +# define ASYNC_ALIEN_LEAVE(...) do{}while(0) +# define ASYNC_ALIEN_TODO_INCREMENT(...) do{}while(0) +# define IF_NO_MORE_ALIENS(...) +#endif // + +extern __hidden void +async_alien_init( async_manager_t *manager, uc_value_t *scope ); + +#endif //ndef UC_ASYNC_ALIEN_H + diff --git a/lib/async/callback.c b/lib/async/callback.c new file mode 100644 index 00000000..a28888e8 --- /dev/null +++ b/lib/async/callback.c @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +This file is part of the async plugin for ucode +*/ +#include "ucode/lib.h" +#include "ucode/vm.h" +#include "ucode/types.h" +#include "ucode/async.h" + +#include "manager.h" +#include "callback.h" + + +void +async_callback_destroy( uc_vm_t *vm, struct async_callback *pcb) +{ + switch ((async_callback_type_t)pcb->callback_type) + { + case callbackNone: + break; + case callbackUcode: + if (pcb->ucode.func) + ucv_put(pcb->ucode.func); + pcb->ucode.func = 0; + if (pcb->ucode.this) + ucv_put(pcb->ucode.this); + pcb->ucode.this = 0; + break; + case callbackC_int_user_flags: + if (pcb->c_int_user_flags.func) + { + (*pcb->c_int_user_flags.func)( vm, pcb->c_int_user_flags.user, UC_ASYNC_CALLBACK_FLAG_CLEANUP); + pcb->c_int_user_flags.func = 0; + pcb->c_int_user_flags.user = 0; + } + break; + case callbackC_int_user_args_flags: + if (pcb->c_int_user_args_flags.func) + { + (*pcb->c_int_user_args_flags.func)( vm, pcb->c_int_user_args_flags.user, 0, 0, UC_ASYNC_CALLBACK_FLAG_CLEANUP); + pcb->c_int_user_args_flags.func = 0; + pcb->c_int_user_args_flags.user = 0; + } + break; + } + for (size_t n = 0; n < pcb->nargs; n++) + { + ucv_put(pcb->args[n]); + pcb->args[n] = 0; + } + pcb->nargs = 0; +} + +uc_value_t * +async_callback_get_ucode_func( async_manager_t *manager, struct async_callback *cb ) +{ + switch ((async_callback_type_t)cb->callback_type) + { + case callbackNone: + return 0; + case callbackUcode: + return ucv_get(cb->ucode.func); + case callbackC_int_user_flags: + return 0; + case callbackC_int_user_args_flags: + return 0; + } + return 0; +} + +int +async_callback_call( async_manager_t *manager, struct async_callback *cb, uc_value_t **args, size_t nargs, uc_value_t **ret, bool cleanup) +{ + int flags = UC_ASYNC_CALLBACK_FLAG_EXECUTE | (cleanup ? UC_ASYNC_CALLBACK_FLAG_CLEANUP : 0); + switch ((async_callback_type_t)cb->callback_type) + { + case callbackNone: + return EXCEPTION_NONE; + case callbackUcode: + { + uc_vm_t *vm = manager->vm; + uc_vm_stack_push(vm, ucv_get(cb->ucode.func)); + for (size_t n = 0; n < nargs; n++) + uc_vm_stack_push(vm, ucv_get(args[n])); + for (size_t n = 0; n < cb->nargs; n++) + uc_vm_stack_push(vm, ucv_get(cb->args[n])); + int ex = uc_vm_call(vm, false, cb->nargs + nargs); + if (cleanup) + { + ucv_put(cb->ucode.func); + cb->ucode.func = 0; + ucv_put(cb->ucode.this); + cb->ucode.this = 0; + for (size_t n = 0; n < cb->nargs; n++) + { + ucv_put(cb->args[n]); + cb->args[n] = 0; + } + cb->nargs = 0; + } + if (ex != EXCEPTION_NONE) + return ex; + uc_value_t *pret = uc_vm_stack_pop(vm); + if (ret) + *ret = pret; + else + ucv_put(pret); + return ex; + } + case callbackC_int_user_flags: + { + if (!cb->c_int_user_flags.func) + return EXCEPTION_NONE; + int ex = (*cb->c_int_user_flags.func)(manager->vm, cb->c_int_user_flags.user, flags); + if (cleanup) + { + cb->c_int_user_flags.func = 0; + cb->c_int_user_flags.user = 0; + } + return ex; + } + case callbackC_int_user_args_flags: + { + if (!cb->c_int_user_flags.func) + return EXCEPTION_NONE; + uc_value_t *args2[nargs + cb->nargs]; + size_t m = 0; + for (size_t n = 0; n < nargs; n++) + args2[m++] = args[n]; + for (size_t n = 0; n < cb->nargs; n++) + args2[m++] = cb->args[n]; + int ex = (*cb->c_int_user_args_flags.func)(manager->vm, cb->c_int_user_args_flags.user, args2, m, flags); + if (cleanup) + { + cb->c_int_user_args_flags.func = 0; + cb->c_int_user_args_flags.user = 0; + } + return ex; + } + } + return EXCEPTION_NONE; +} diff --git a/lib/async/callback.h b/lib/async/callback.h new file mode 100644 index 00000000..75b18ed5 --- /dev/null +++ b/lib/async/callback.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +This file is part of the async plugin for ucode +*/ + +#ifndef UC_ASYNC_CALLBACK_H +#define UC_ASYNC_CALLBACK_H + +typedef enum +{ + callbackNone = 0, + callbackUcode, + callbackC_int_user_flags, + callbackC_int_user_args_flags +} async_callback_type_t; + +struct async_callback +{ + uint32_t callback_type : 4; + uint32_t type : 4; + uint32_t nargs : 8; + uint32_t still_available_bits : 16; + + union + { + struct + { + uc_value_t *func; + uc_value_t *this; // not used, so far + } ucode; + struct + { + int (*func)(uc_vm_t *, void *user, int flags); + void *user; + } c_int_user_flags; + struct + { + int (*func)(uc_vm_t *, void *user, uc_value_t **args, size_t nargs, int flags); + void *user; + } c_int_user_args_flags; + }; + + uc_value_t *args[]; +}; + +extern __hidden uc_value_t * +async_callback_get_ucode_func( async_manager_t *manager, struct async_callback *cb ); + +extern __hidden void +async_callback_destroy( uc_vm_t *vm, struct async_callback *pcb); + +extern __hidden int +async_callback_call( async_manager_t *manager, struct async_callback *cb, uc_value_t **args, size_t nargs, uc_value_t **ret, bool cleanup); + +#endif //ndef UC_ASYNC_CALLBACK_H diff --git a/lib/async/manager.c b/lib/async/manager.c new file mode 100644 index 00000000..63660db4 --- /dev/null +++ b/lib/async/manager.c @@ -0,0 +1,625 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +This file is part of the async plugin for ucode +*/ + +/** + * # Async functions + * + * The `async` module provides asynchronous functionality. + * + * Functions can be individually imported and directly accessed using the + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#named_import named import} + * syntax: + * + * ``` + * import { Promise, setTimeout } from 'async'; + * + * Promise( (resolve,reject)=> + * { + * setTimeout( ()=> + * { + * resolve( 'done' ); + * }, 1000 ) + * }).then( ( a )=> + * { + * print( a ); + * }); + * ``` + * + * Alternatively, the module namespace can be imported + * using a wildcard import statement: + * + * ``` + * import * as async from 'async'; + * + * async.Promise( (resolve,reject)=> + * { + * async.setTimeout( ()=> + * { + * resolve( 'done' ); + * }, 1000 ) + * }).then( ( a )=> + * { + * print( a ); + * }); + * ``` + * + * @module async + */ + +#include +#include +#include +#include +#include +#include + +#include "ucode/module.h" +#include "ucode/platform.h" +#include "ucode/async.h" + +#include "manager.h" +#include "promise.h" +#include "callback.h" +#include "timer.h" +#include "queuer.h" +#include "alien.h" + +#ifdef ASYNC_HAS_UPTIME +double +async_manager_uptime( async_manager_t *manager ) +{ + if (!manager) + return NAN; + int64_t now = async_timer_current_time() - manager->start_time; + return (double)now / 1000.0; +} +#endif + +/* Start an interruptable sleep */ +/*void async_sleep( async_manager_t *manager, int64_t msec) +{ + if (msec < 1) + return; + + struct timespec wait; + wait.tv_sec = msec / 1000; + wait.tv_nsec = (msec % 1000) * 1000000; + nanosleep(&wait, 0); +}*/ + +int +async_todo_unlink( async_manager_t *manager, async_todo_t *todo) +{ + if (!todo) + return EXCEPTION_NONE; + + if (0 != --todo->refcount) + return EXCEPTION_NONE; + + DEBUG_ASSERT( 0 == todo->in_todo_list ); + + int ret = EXCEPTION_NONE; + switch ((enum async_todo_type)todo->todo_type) + { + case todoClearedTimer: + case todoTimer: + async_timer_destroy( manager, todo ); + break; + case todoPromise: + ret = async_promise_destroy( manager, todo); + break; + } + + free(todo); + return ret; +} + +// When is the todo object due? +static inline int64_t +async_todo_due(async_todo_t *todo) +{ + switch ((enum async_todo_type)todo->todo_type) + { + case todoClearedTimer: + case todoPromise: + return 0; + case todoTimer: + return async_timer_cast(todo)->due; + } + return 0; +} + +void +async_todo_put_in_list( async_manager_t *manager, async_todo_t *todo) +{ + DEBUG_ASSERT( 0 == todo->in_todo_list ); + todo->in_todo_list = 1; + + ASYNC_ALIEN_TODO_INCREMENT( manager ); + + int64_t due = async_todo_due(todo); + if( due ) + { + DEBUG_PRINTF( "%-1.3lf Todo %p scheduled at %-1.3lf\n", async_manager_uptime(manager), todo, + (due - manager->start_time) / 1000.0 ); + + } + async_todo_t *previous = 0; + for (async_todo_t *it = manager->todo_list; it; it = it->next) + { + if (async_todo_due(it) > due) + { + todo->next = it; + if (previous) + previous->next = todo; + else + manager->todo_list = todo; + todo->refcount++; + return; + } + previous = it; + } + todo->next = 0; + if (previous) + previous->next = todo; + else + manager->todo_list = todo; + todo->refcount++; +} + +static int +async_handle_todo( async_manager_t *manager ) +{ + if (!manager->todo_list) + return EXCEPTION_NONE; + int64_t now = async_timer_current_time(); + async_todo_t *to_be_handled = 0; + + while (manager->todo_list) + { + bool end_of_todo_for_now = false; + switch ((enum async_todo_type)manager->todo_list->todo_type) + { + case todoClearedTimer: + break; + case todoTimer: + end_of_todo_for_now = async_timer_cast(manager->todo_list)->due > now; + break; + case todoPromise: + break; + } + if (end_of_todo_for_now) + break; + + async_todo_t *pop = manager->todo_list; + manager->todo_list = pop->next; + pop->in_todo_list = 0; + pop->next = 0; + + if (todoClearedTimer == pop->todo_type) + { + async_todo_unlink(manager, pop); + } + else + { + pop->next = to_be_handled; + to_be_handled = pop; + } + } + + if (!to_be_handled) + return EXCEPTION_NONE; + + reverse_stack(async_todo_t, to_be_handled); + + while (to_be_handled) + { + async_todo_t *pop = to_be_handled; + to_be_handled = pop->next; + pop->next = 0; + int ex = EXCEPTION_NONE; + + switch ((enum async_todo_type)pop->todo_type) + { + case todoClearedTimer: + { + // The timer can be cleared in one of the previous to_be_handled functions + break; + } + case todoTimer: + { + ex = async_timer_do(manager,pop); + break; + } + case todoPromise: + { + ex = async_promise_do(manager, pop); + break; + } + } + + { + int ex2 = async_todo_unlink(manager, pop); + if (EXCEPTION_NONE == ex) + ex = ex2; + } + + if (EXCEPTION_NONE != ex) + { + // put back all remaining todo's + reverse_stack(async_todo_t, to_be_handled); + while (to_be_handled) + { + async_todo_t *pop = to_be_handled; + to_be_handled = pop->next; + pop->next = manager->todo_list; + manager->todo_list = pop; + pop->in_todo_list = 1; + } + return ex; + } + } + return EXCEPTION_NONE; +} + +static int64_t +async_how_long_to_next_todo( async_manager_t *manager ) +{ + while (manager->todo_list) + { + switch ((enum async_todo_type)manager->todo_list->todo_type) + { + case todoClearedTimer: + { + // remove from list + async_todo_t *pop = manager->todo_list; + manager->todo_list = pop->next; + pop->next = 0; + pop->in_todo_list = 0; + async_todo_unlink(manager, pop); + continue; + } + case todoPromise: + return 0; + case todoTimer: + { + int64_t now = async_timer_current_time(); + int64_t due = async_timer_cast(manager->todo_list)->due; + if (due > now) + return due - now; + return 0; + } + } + } + + // Nothing in todo list + return -1; +} + +static void +async_manager_cleanup(uc_vm_t *vm, async_manager_t *manager) +{ + while (manager->todo_list) + { + async_todo_t *pop = manager->todo_list; + manager->todo_list = pop->next; + pop->next = 0; + async_todo_unlink( manager, pop); + } + + { + struct async_callback_queuer *queuer = manager->callback_queuer; + manager->callback_queuer = 0; + async_callback_queuer_free( manager, queuer ); + } +#ifdef ASYNC_HAS_ALIENS + { + async_alien_t *alien = manager->alien; + manager->alien = 0; + async_alien_free( manager, alien ); + } +#endif +} + +static int +async_event_pump( struct uc_async_manager *_man, unsigned max_wait, int flags) +{ + async_manager_t *manager = async_manager_cast( _man ); + + if (flags & UC_ASYNC_PUMP_PUMP) + { + int64_t until = 0; + if (UINT_MAX == max_wait) + { + until = INT64_MAX; + } + else if (max_wait) + { + until = async_timer_current_time() + max_wait; + } + + do + { + DEBUG_PRINTF("%-1.3lf Pump!\n", async_manager_uptime( manager )); + int ex = async_handle_todo( manager ); + if (EXCEPTION_NONE != ex) + { + if (EXCEPTION_EXIT == ex) + { + manager->silent = 1; + return STATUS_EXIT; + } + return ERROR_RUNTIME; + } + + ex = async_handle_queued_callbacks( manager ); + if (EXCEPTION_NONE != ex) + { + if (EXCEPTION_EXIT == ex) + { + manager->silent = 1; + return STATUS_EXIT; + } + return ERROR_RUNTIME; + } + + int64_t tosleep = async_how_long_to_next_todo( manager ); + + if (-1 == tosleep) // no todo list + { + if( 0 == manager->pending_promises_cnt ) // no pending promises + { + + IF_NO_MORE_ALIENS(manager) + { + // Nothing to do anymore + DEBUG_PRINTF("%-1.3lf Last!\n", async_manager_uptime( manager )); + break; // do {} while( ) + } + } + tosleep = INT64_MAX; + } + + if (max_wait && !async_any_queued_callbacks_waiting( manager )) + { + ASYNC_ALIEN_LEAVE(manager); + + if ((unsigned)tosleep > max_wait) + tosleep = max_wait; + if (tosleep > 0) + { +#ifdef DEBUG_PRINT + DEBUG_PRINTF("%-1.3lf Start wait\n", async_manager_uptime( manager )); + /* The printf could have eaten a signal. + So look if something was added to the async stack */ + if( !async_any_queued_callbacks_waiting( manager ) ) +#endif + async_sleep( manager, tosleep); + DEBUG_PRINTF("%-1.3lf End wait\n", async_manager_uptime( manager )); + } + + ASYNC_ALIEN_ENTER(manager); + } + else + { + ASYNC_ALIEN_LEAVE(manager); + ASYNC_ALIEN_ENTER(manager); + } + + } while ((flags & UC_ASYNC_PUMP_CYCLIC) && + (until > async_timer_current_time())); + } // if( flags & UC_ASYNC_PUMP_PUMP ) + + if (flags & UC_ASYNC_PUMP_CLEANUP) + { + uc_vm_t *vm = manager->vm; + manager->vm = 0; + async_manager_cleanup( vm, manager ); + uc_vm_registry_delete( vm, "async.manager" ); + } + + return STATUS_OK; +} + + +/** + * Event pump, in which the asynchronous functions are actually executed. + * + * You can call this inside your program regularly, or at the end. + * When omitted the async functions will be called after the script + * has 'ended', by the vm. + * + * @function module:async#PumpEvents + * + * @param {Number} [timespan=null] + * Timespan in msec. The function will keep pumping events until *timespan* + * msec has elapsed. When no timespan is provided, PumpEvents() will keep + * pumping until no timers are left and no active promises are around, + * or an exception occurs. + * + * @param {Boolean} [single=false] + * Flag to only pump once, and then 'sleep' *timespan* msec, or + * return at the moment the next event is due to be executed, + * whatever comes first. + * This is usable if you want to do something between each stroke of the + * event pump: + * ``` + * let promise = async.Promise( (resolve,reject)=>{ resolve( 1 ) } ); + * for( let i=0; i<5; i++ ) + * { + * promise = promise.then( (num)=>{ print(num); return ++num } ); + * } + * + * while( async.PumpEvents( 1000, true ) ) + * { + * print( ` *${async.async_manager_uptime()}* ` ); + * } + * // will output something like '*0.002* 1 *0.003* 2 *0.003* 3 *0.004* 4 *0.005* 5 *0.005*' + * // and then exit. + * ``` + * But also + * ``` + * let timer; + * timer = async.setInterval( ( cnt )=> + * { + * if( ++cnt.cnt == 5 ) + * async.clearTimeout( timer ); + * print( cnt.cnt ); + * }, 100, { cnt: 0 } ); + * + * while( async.PumpEvents( 1000, true ) ) + * { + * print( ` *${async.async_manager_uptime()}* ` ); + * } + * // will output something like '*0.101* 1 *0.201* 2 *0.301* 3 *0.401* 4 *0.501* 5' + * // and then exit. + * ``` + * + * @return {Boolean} + * True if more events are (or will be) available, False if no more events are to be expected. + * + * @example + * async.setTimeout( ()=>{}, 10000 ); + * + * let count = 0; + * while( async.PumpEvents( 1000 ) ) + * { + * print( `${++count} ` ); + * } + * // Will output '1 2 3 4 5 6 7 9' and then exit. + * // Maybe '10' is also printed, depending on the exact timing. + */ + +static uc_value_t * +PumpEvents(uc_vm_t *vm, size_t nargs) +{ + unsigned msec = UINT_MAX; + int flags = UC_ASYNC_PUMP_CYCLIC | UC_ASYNC_PUMP_PUMP; + uc_value_t *pmsec = uc_fn_arg(0); + if (pmsec) + { + int64_t v = ucv_int64_get(pmsec); + if (v > 0) + { + if (v < UINT_MAX) + msec = (unsigned)v; + else + msec = UINT_MAX - 1; + } + } + uc_value_t *psingle = uc_fn_arg(1); + if (psingle) + { + bool v = ucv_boolean_get(psingle); + if (v) + flags &= ~UC_ASYNC_PUMP_CYCLIC; + } + + async_manager_t *manager = async_manager_get( vm ); + if( !manager ) + return ucv_boolean_new(false); + + async_event_pump( &manager->header, msec, flags); + + if( manager->pending_promises_cnt || + manager->todo_list) + return ucv_boolean_new(true); + + return ucv_boolean_new(false); +} + + +#ifdef ASYNC_HAS_UPTIME +/** + * Returns the uptime of the script (since importing the async plugin), in seconds, + * with a milli seconds resolution. + * (Actually a debug helper, but I decided to leave it) + * + * @function module:async#uptime + * + * @returns {Number} + * Uptime in seconds. + * + * @example + * let timer + * timer = async.setInterval( (a)=> + * { + * if( async.async_manager_uptime() > 5 ) + * async.clearTimeout( timer ); + * print( `${async.async_manager_uptime()} ` ); + * }, 1000 ); + * + * while( async.PumpEvents() ); + * // Will output something like '0.003 1.003 2.003 3.003 4.003 5.003' and then exit. + */ + +static uc_value_t * +Uptime(uc_vm_t *vm, size_t args) +{ + async_manager_t *manager = async_manager_get( vm ); + return ucv_double_new(async_manager_uptime(manager)); +} +#endif + +static const uc_function_list_t local_async_fns[] = { + {"PumpEvents", PumpEvents}, +#ifdef ASYNC_HAS_UPTIME + {"uptime", Uptime}, +#endif +}; + +static void close_manager( void *ud ) +{ + async_manager_t *manager = ud; + DEBUG_PRINTF( "%-1.3lf close_manager( vm=%p )\n", async_manager_uptime( manager ), manager->vm ); + + // Don't expect the vm to be useable anymore. + manager->vm = 0; + async_manager_cleanup( 0, manager ); + async_manager_unlink( manager ); +} + +void uc_module_init(uc_vm_t *vm, uc_value_t *scope) +{ + if( async_manager_get( vm ) ) + // Initializing twice? + return; + + async_manager_t *manager = xalloc(sizeof(async_manager_t)); + const uc_function_list_t manager_type_fns[0]; + uc_resource_type_t *managertype = uc_type_declare(vm, "async.manager", manager_type_fns, close_manager); + uc_value_t *uv_manager = ucv_resource_new(managertype, async_manager_link( manager ) ); + uc_vm_registry_set(vm, "async.manager", uv_manager); + manager->vm = vm; + + manager->header.event_pump = async_event_pump; + + async_promise_init( manager, scope ); + async_timer_init( manager, scope ); + async_callback_queuer_init( manager, scope ); + async_alien_init( manager, scope ); + + uc_function_list_register(scope, local_async_fns); + +#ifdef ASYNC_HAS_UPTIME + manager->start_time = async_timer_current_time(); +#endif + DEBUG_PRINTF( "%-1.3lf uc_module_init( vm=%p )\n", async_manager_uptime( manager ), vm ); +} diff --git a/lib/async/manager.h b/lib/async/manager.h new file mode 100644 index 00000000..40c8021e --- /dev/null +++ b/lib/async/manager.h @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +This file is part of the async plugin for ucode +*/ + +#ifndef UC_ASYNC_MANAGER_H +#define UC_ASYNC_MANAGER_H + +#include "ucode/platform.h" +#include "ucode/async.h" + +// #define DEBUG_PRINT +#define ASYNC_HAS_UPTIME +#define ASYNC_HAS_ALIENS + +#ifdef DEBUG_PRINT +# define DEBUG_PRINTF(...) printf(__VA_ARGS__) +# define ASYNC_HAS_UPTIME +#else +# define DEBUG_PRINTF(...) +#endif + +#ifdef NDEBUG +# define DEBUG_ASSERT(...) +#else +# define DEBUG_ASSERT(...) assert(__VA_ARGS__) +#endif + +struct async_todo; +struct async_promise; +struct async_callback_queuer; +struct async_alien; + +struct async_manager +{ + struct uc_async_manager header; + + uc_vm_t *vm; + + // Linked list of pending todo's + struct async_todo *todo_list; + // Points to the active promise which excecuting a then, catch or finally handler + // to be able to catch the arguments of 'throw()'. + struct async_promise *active_promise; + + // Number of pending promises + int pending_promises_cnt:31; + int silent:1; // exit is called, no more output + + // Pointer to linked list of async callback's + struct async_callback_queuer *callback_queuer; + +#ifdef ASYNC_HAS_UPTIME + // For uptime + int64_t start_time; +#endif +#ifdef ASYNC_HAS_ALIENS + struct async_alien *alien; +#endif + + uc_resource_type_t *promise_type; + uc_resource_type_t *timer_type; + + uint32_t refcount; +}; + +typedef struct async_manager async_manager_t; + +#ifdef ASYNC_HAS_UPTIME +extern __hidden double +async_manager_uptime( async_manager_t * ); +#endif + +static inline async_manager_t * +async_manager_cast( struct uc_async_manager *m ) +{ + return (async_manager_t *)m; +} + +static inline async_manager_t * +async_manager_get( uc_vm_t *vm ) +{ + if( !vm ) return 0; + struct uc_async_manager *manager = uc_async_manager_get( vm ); + if( !manager ) + return 0; + return async_manager_cast( manager ); +} + +static inline async_manager_t * +async_manager_link( async_manager_t *manager ) +{ + manager->refcount++; + return manager; +} + +static inline bool +async_manager_unlink( async_manager_t *manager ) +{ + if( 0 == --manager->refcount ) + { + free( manager ); + return true; + } + return false; +} + +enum async_todo_type +{ + todoPromise = 1, + todoTimer = 2, + todoClearedTimer = todoTimer | 1, +}; + +typedef struct async_todo +{ + struct uc_async_timer header; // empty struct + + uint32_t todo_type : 2; + + /* refcount can be max 4. + For promises: 1 for the ucode promise object, + 2 for the associated resolve,reject objects, + and 1 for being in the todo list. + For timers: 1 for the ucode timer object + and 1 for being in the todo list. + So 3 bits is plenty */ + uint32_t refcount : 3; + /* One bit to know if this object is in the todo list */ + uint32_t in_todo_list : 1; + + /* which leaves 26 bits for general purpose: */ + uint32_t promise_pending : 1; // is added to 'global' vm->pending_promises_cnt + uint32_t promise_state : 2; // pending, resolved, rejected + uint32_t promise_result_is_exception : 1; + /* still 22 bits left */ + + struct async_todo *next; +} async_todo_t; + +extern __hidden void +async_todo_put_in_list( async_manager_t *manager, async_todo_t *todo); + +extern __hidden int +async_todo_unlink( async_manager_t *manager, async_todo_t *todo); + +#define reverse_stack(type, stack) \ + do \ + { \ + type *walk = stack, *reversed = 0; \ + while (walk) \ + { \ + type *pop = walk; \ + walk = pop->next; \ + pop->next = reversed; \ + reversed = pop; \ + } \ + stack = reversed; \ + } while (0) + + +#endif // ndef UC_ASYNC_MANAGER_H diff --git a/lib/async/promise.c b/lib/async/promise.c new file mode 100644 index 00000000..5347e92d --- /dev/null +++ b/lib/async/promise.c @@ -0,0 +1,1459 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +This file is part of the async plugin for ucode +*/ + +#include "ucode/lib.h" +#include "ucode/vm.h" +#include "ucode/types.h" + +#include "promise.h" +#include "callback.h" +#include "timer.h" + +enum +{ + promisePending = 0, + + promiseThen, + promiseCatch, + promiseFinally, +}; + +struct async_promise_method +{ + struct async_promise_method *next; + struct async_callback callback; +}; + +typedef struct async_promise_method async_promise_method_t; + +static void +uc_promise_func_free( async_manager_t *manager, async_promise_method_t *func) +{ + if (!func) + return; + async_callback_destroy( manager->vm, &func->callback); + free(func); +} + +static void +async_exception_clear( async_manager_t *manager, uc_exception_t *exception) +{ + exception->type = EXCEPTION_NONE; + + ucv_put(exception->stacktrace); + exception->stacktrace = NULL; + + free(exception->message); + exception->message = NULL; +} + +static void +async_exception_free( async_manager_t *manager, uc_exception_t *exception) +{ + if (!exception) + return; + async_exception_clear( manager, exception); + free(exception); +} + +static void +async_exception_move( async_manager_t *manager, uc_exception_t *to, uc_exception_t *from) +{ + if (from && to) + { + to->type = from->type; + from->type = EXCEPTION_NONE; + to->stacktrace = from->stacktrace; + from->stacktrace = 0; + to->message = from->message; + from->message = 0; + } +} + +static uc_exception_t * +async_exception_new( async_manager_t *manager, uc_exception_t *exception) +{ + uc_exception_t *ret = xalloc(sizeof(uc_exception_t)); + if (exception) + { + async_exception_move( manager, ret, exception); + } + return ret; +} + +/** + * Represents a promise object as returned by + * {@link module:async#Promise|Promise()} or {@link module:async#PromiseAll|PromiseAll()}. + * + * @class module:async.promise + * @hideconstructor + * + * @see {@link module:async#Promise|Promise()} + * + * @implements then(), catch() and finally() + * + * @example + * + * const promise = async.Promise(…); + * + * promise.then( ()=>{} ); + * promise.catch( ()=>{} ); + * promise.finally( ()=>{} ); + */ + +async_promise_t * +uc_promise_new( async_manager_t *manager ) +{ + async_promise_t *p = xalloc(sizeof(async_promise_t)); + p->header.refcount = 1; + p->header.todo_type = todoPromise; + p->manager = async_manager_link( manager ); + p->manager->pending_promises_cnt++; + p->header.promise_pending = 1; + + DEBUG_PRINTF("%-1.3lf new promise %p %u\n", async_manager_uptime(manager), p, manager->pending_promises_cnt); + return p; +} + +static inline async_promise_t * +async_promise_cast(async_todo_t *todo) +{ + DEBUG_ASSERT(todoPromise == todo->todo_type); + return (async_promise_t *)todo; +} + +static void +async_promise_clear_result( async_manager_t *manager, async_promise_t *promise ) +{ + if( !promise ) + return; + ucv_put( promise->reject_caused_by ); + promise->reject_caused_by = 0; + + if (promise->header.promise_result_is_exception) + { + async_exception_free( manager, promise->result.exception); + promise->header.promise_result_is_exception = 0; + promise->result.exception = 0; + } + else + { + ucv_put( promise->result.value ); + promise->result.value = 0; + } +} + +static uc_chunk_t * +uc_vm_frame_chunk(uc_callframe_t *frame) +{ + return frame->closure ? &frame->closure->function->chunk : NULL; +} + +static bool +async_vm_raise_exception_caused_by( uc_vm_t *vm, uc_value_t *caused_by, int type, const char *err, intptr_t arg ) +{ + uc_callframe_t *frame = 0; + bool ret = false; + if( caused_by && UC_CLOSURE == ucv_type(caused_by) ) + { + uc_vector_grow(&vm->callframes); + + frame = &vm->callframes.entries[vm->callframes.count++]; + frame->closure = (uc_closure_t *)caused_by; + frame->cfunction = NULL; + frame->stackframe = vm->stack.count; + frame->ip = uc_vm_frame_chunk(frame)->entries; /* that would point to the first instruction of the closure so the error message would point there as well */ + frame->ctx = NULL; + frame->mcall = false; + } + if( vm->callframes.count ) + { + // No exceptions without framestack. It will crash + uc_vm_raise_exception(vm, type, err, arg ); + ret = true; + } + if( frame ) + { + /* "pop" artifical callframe */ + vm->callframes.count--; + } + return ret; +} + +int +async_promise_destroy( async_manager_t *manager, async_todo_t *todo) +{ + async_promise_t *promise = async_promise_cast(todo); + uc_vm_t *vm = manager ? manager->vm : 0; + async_manager_t *vm_is_active = vm ? manager : 0; + + if (vm_is_active && promise->header.promise_pending) + { + vm_is_active->pending_promises_cnt--; + promise->header.promise_pending = 0; + } + + DEBUG_PRINTF("%-1.3lf delete promise %p %d\n", async_manager_uptime(vm_is_active), promise, + vm_is_active ? vm_is_active->pending_promises_cnt : -1 ); + + int ret = EXCEPTION_NONE; + bool uncaught = promiseCatch == promise->header.promise_state; + if (uncaught) + { + if (vm_is_active && promise->header.promise_result_is_exception) + { + // put back the original exception + async_exception_clear( vm_is_active, &vm->exception); + async_exception_move( vm_is_active, &vm->exception, promise->result.exception); + async_exception_free( vm_is_active, promise->result.exception); + promise->result.exception = 0; + promise->header.promise_result_is_exception = 0; + ret = vm->exception.type; + uncaught = false; + } + } + + uc_value_t *caused_by = 0; + if( uncaught ) + { + caused_by = promise->reject_caused_by; + promise->reject_caused_by = 0; + } + async_promise_clear_result( vm_is_active, promise ); + + async_promise_method_t *stack = promise->stack; + for (; stack;) + { + async_promise_method_t *pop = stack; + stack = pop->next; + + if (vm_is_active && !(uncaught) && promiseFinally == pop->callback.type) + async_callback_call( vm_is_active, &pop->callback, 0, 0, 0, false); + async_callback_destroy( vm, &pop->callback); + free(pop); + } + + if (uncaught) + { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" +#pragma GCC diagnostic ignored "-Wuse-after-free" + static const char *err = "Rejected promise %p without catch handler\n"; + if (vm_is_active ) + { + if( !async_vm_raise_exception_caused_by( vm, caused_by, ret = EXCEPTION_RUNTIME, err, (intptr_t)promise ) ) + { + fprintf( stderr, err, promise); + } + } + else + { + if( !manager || !manager->silent ) + fprintf( stderr, err, promise); + } +#pragma GCC diagnostic pop + } + + ucv_put( caused_by ); + return ret; +} + +static void async_promise_method_pushback( async_manager_t *manager, async_promise_t *promise, async_promise_method_t *func) +{ + async_promise_method_t *previous = 0; + for (async_promise_method_t *it = promise->stack; it; it = it->next) + { + previous = it; + } + if (previous) + previous->next = func; + else + promise->stack = func; + + if (0 == promise->header.promise_pending) + { + promise->header.promise_pending = 1; + manager->pending_promises_cnt++; + } + + if ((0 == promise->header.in_todo_list) && + (promisePending != promise->header.promise_state)) + { + async_todo_put_in_list( manager, &promise->header); + } +} + +static void +async_promise_add_ucode_func( async_manager_t *manager, async_promise_t *promise, uc_value_t *func, int type) +{ + if (!ucv_is_callable(func)) + { + uc_vm_raise_exception(manager->vm, EXCEPTION_TYPE, "arg1 needs to be callable"); + return; + } + + async_promise_method_t *pf = xalloc(sizeof(async_promise_method_t)); + pf->callback.type = type; + pf->callback.callback_type = callbackUcode; + pf->callback.ucode.func = ucv_get(func); + pf->next = 0; + + async_promise_method_pushback(manager, promise, pf); +} + +static void +async_promise_add_c_int_user_args_flags_func( async_manager_t *manager, + async_promise_t *promise, + int (*func)(uc_vm_t *, void *, uc_value_t **, size_t, int), void *user, int type) +{ + async_promise_method_t *pf = xalloc(sizeof(async_promise_method_t)); + pf->callback.type = type; + pf->callback.callback_type = callbackC_int_user_args_flags; + pf->callback.c_int_user_args_flags.func = func; + pf->callback.c_int_user_args_flags.user = user; + pf->next = 0; + + async_promise_method_pushback( manager, promise, pf); +} + +struct async_promise_resolver +{ + uc_async_promise_resolver_t header; // empty struct + uint32_t refcount : 16; + uint32_t type : 16; + async_promise_t *promise; + uc_value_t *callback; // The callback provided with the creator: async.Promise( callback ) +}; + +typedef struct async_promise_resolver async_promise_resolver_t; + +static inline async_promise_resolver_t * +async_promise_resolver_cast( struct uc_async_promise_resolver *p ) +{ + return (async_promise_resolver_t *)p; +} + +static async_promise_resolver_t * +async_promise_resolver_new( async_promise_t *promise ) +{ + async_promise_resolver_t *resolver = xalloc(sizeof(async_promise_resolver_t)); + resolver->refcount = 1; + promise->header.refcount++; + resolver->promise = promise; + promise->resolver = &resolver->header; + return resolver; +} + +static const char *_strPromise = "async.promise"; + +static uc_value_t * +async_promise_then(uc_vm_t *vm, size_t nargs) +{ + async_promise_t **ppromise = uc_fn_this(_strPromise); + if (!ppromise || !*ppromise) + return 0; + async_promise_add_ucode_func( (*ppromise)->manager, *ppromise, uc_fn_arg(0), promiseThen); + return ucv_get(_uc_fn_this_res(vm)); +} + +static uc_value_t * +async_promise_catch(uc_vm_t *vm, size_t nargs) +{ + async_promise_t **ppromise = uc_fn_this(_strPromise); + if (!ppromise || !*ppromise) + return 0; + async_promise_add_ucode_func( (*ppromise)->manager, *ppromise, uc_fn_arg(0), promiseCatch); + return ucv_get(_uc_fn_this_res(vm)); +} + +static uc_value_t * +async_promise_finally(uc_vm_t *vm, size_t nargs) +{ + async_promise_t **ppromise = uc_fn_this(_strPromise); + if (!ppromise || !*ppromise) + return 0; + async_promise_add_ucode_func( (*ppromise)->manager, *ppromise, uc_fn_arg(0), promiseFinally); + return ucv_get(_uc_fn_this_res(vm)); +} + +static const uc_function_list_t promise_type_fns[] = { + {"then", async_promise_then}, + {"catch", async_promise_catch}, + {"finally", async_promise_finally}, +}; + +static void +close_promise(void *ud) +{ + async_promise_t *promise = ud; + if (promise) + { + DEBUG_PRINTF("%-1.3lf close promise %p %u\n", async_manager_uptime(promise->manager), promise, promise->header.refcount); + if( async_manager_unlink( promise->manager ) ) + promise->manager = 0; + async_todo_unlink(promise->manager, &promise->header); + } +} + +int +async_promise_do( async_manager_t *manager, async_todo_t *todo) +{ + async_promise_t *promise = async_promise_cast(todo); + + int state = promise->header.promise_state; + + async_promise_method_t *next_to_be_handled = 0; + // walk the stack searching for a handler + for (; promise->stack;) + { + next_to_be_handled = promise->stack; + promise->stack = next_to_be_handled->next; + next_to_be_handled->next = 0; + + if (state == next_to_be_handled->callback.type + || promiseFinally == next_to_be_handled->callback.type) + { + break; + } + + uc_promise_func_free( manager, next_to_be_handled); + next_to_be_handled = 0; + } + + if( !next_to_be_handled ) + { + // mark as 'not pending' + if (promise->header.promise_pending) + { + promise->header.promise_pending = 0; + if( manager ) manager->pending_promises_cnt--; + } + return EXCEPTION_NONE; + } + + uc_value_t *out = 0; + int ex = EXCEPTION_NONE; + { + uc_value_t *in = 0; + if (promiseFinally != next_to_be_handled->callback.type) + { + if (promise->header.promise_result_is_exception) + { + in = ucv_string_new(promise->result.exception->message); + } + else + { + in = ucv_get( promise->result.value ); + } + + async_promise_clear_result( manager, promise ); + } + + // reset the state, so we know when throw() is called + promise->header.promise_state = promisePending; + + { + async_promise_t *push = manager->active_promise; + manager->active_promise = promise; + ex = async_callback_call( manager, &next_to_be_handled->callback, &in, 1, &out, true); + manager->active_promise = push; + } + + ucv_put(in); + } + + uc_value_t *caused_by = async_callback_get_ucode_func( manager, &next_to_be_handled->callback ); + int ittype = next_to_be_handled->callback.type; + uc_promise_func_free( manager, next_to_be_handled); + + if (EXCEPTION_NONE != ex) + { + if (EXCEPTION_EXIT == ex) + { + ucv_put( caused_by ); + return ex; + } + ucv_put(out); + ucv_put(promise->reject_caused_by); + promise->reject_caused_by = caused_by; + if( promiseCatch == promise->header.promise_state ) + { + // Caused by a throw() + async_exception_clear( manager, &manager->vm->exception ); + } + else + { + // Take over the exception + promise->result.exception = async_exception_new( manager, &manager->vm->exception); + promise->header.promise_result_is_exception = 1; + promise->header.promise_state = promiseCatch; + } + // reschedule + async_todo_put_in_list( manager, &promise->header ); + return EXCEPTION_NONE; + } + + ucv_put(caused_by); + caused_by = 0; + + if (promiseFinally == ittype) + { + // Return value of finally is ignored, if it's not an exception + ucv_put(out); + + // put state back + promise->header.promise_state = state; + // reschedule + async_todo_put_in_list( manager, &promise->header ); + return EXCEPTION_NONE; + } + + { // Is the result a promise? + async_promise_t **ppromise = (async_promise_t **)ucv_resource_dataptr(out, _strPromise); + if (ppromise && *ppromise) + { + async_promise_t *new_promise = *ppromise; + // We must push it's handler stack in front of ours, + // and adopt it's state and it's resolver + async_promise_method_t *previous = 0; + for (async_promise_method_t *it2 = new_promise->stack; it2; it2 = it2->next) + { + previous = it2; + } + if (previous) + previous->next = promise->stack; + else + new_promise->stack = promise->stack; + promise->stack = new_promise->stack; + new_promise->stack = 0; + + if (promise->resolver) + { + // Shouldn't be possible, but handle anyway + async_promise_resolver_cast( promise->resolver )->promise = 0; + promise->resolver = 0; + promise->header.refcount--; + } + + if (new_promise->resolver) + { + async_promise_resolver_cast( new_promise->resolver )->promise = promise; + promise->resolver = new_promise->resolver; + new_promise->resolver = 0; + new_promise->header.refcount--; + promise->header.refcount++; + } + + promise->result.value = new_promise->result.value; + new_promise->result.value = 0; + promise->header.promise_result_is_exception = new_promise->header.promise_result_is_exception; + new_promise->header.promise_result_is_exception = 0; + promise->header.promise_state = new_promise->header.promise_state; + + // destroys also new_promise + ucv_put(out); + + if( promisePending == promise->header.promise_state ) + { + // not reschedule. We must wait for the resolver to act + } + else + { + // reschedule + async_todo_put_in_list( manager, &promise->header ); + } + + return EXCEPTION_NONE; + } + } + + promise->header.promise_state = promiseThen; + promise->result.value = out; + promise->header.promise_result_is_exception = 0; + // reschedule + async_todo_put_in_list( manager, &promise->header ); + return EXCEPTION_NONE; +} + +static void +async_resolve_or_reject( async_manager_t *manager, async_promise_resolver_t *resolver, uc_value_t *res, int type) +{ + if (!resolver || !resolver->promise) + return; + + async_promise_t *promise = resolver->promise; + resolver->promise = 0; + promise->resolver = 0; + + if (promisePending != promise->header.promise_state) + { + async_todo_unlink( manager, &promise->header); + return; + } + + if (promiseThen == type) + promise->header.promise_state = promiseThen; + else if (promiseCatch == type) + promise->header.promise_state = promiseCatch; + + promise->result.value = ucv_get(res); + promise->header.promise_result_is_exception = 0; + + if( !promise->header.promise_pending ) + { + promise->header.promise_pending = 1; + manager->pending_promises_cnt++; + } + + if (!promise->header.in_todo_list) + { + async_todo_put_in_list( manager, &promise->header); + } + + async_todo_unlink( manager, &promise->header); +} + +static int +async_promise_resolver_unlink( async_manager_t *manager, async_promise_resolver_t *resolver ) +{ + if( 0 == resolver || 0 != --resolver->refcount ) + return EXCEPTION_NONE; + + if (resolver->promise) + { + async_promise_t *promise = resolver->promise; + resolver->promise = 0; + promise->resolver = 0; + + DEBUG_PRINTF("%-1.3lf promise abandoned %p\n", async_manager_uptime(promise->manager), promise); + promise->result.value = ucv_string_new("Promise abandoned"); + promise->header.promise_result_is_exception = 0; + promise->header.promise_state = promiseCatch; + promise->reject_caused_by = resolver->callback; + resolver->callback = 0; + if (promise->manager) + async_todo_put_in_list(promise->manager, &promise->header); + async_todo_unlink( promise->manager, &promise->header); + } + + if( resolver->callback ) + ucv_put( resolver->callback ); + free(resolver); + return EXCEPTION_NONE; +} + +static uc_value_t * +async_resolver_resolve_or_reject( uc_vm_t *vm, size_t nargs, int type ) +{ + uc_cfunction_t *callee = uc_vector_last(&vm->callframes)->cfunction; + async_promise_resolver_t **presolver = (async_promise_resolver_t**)ucv_cfunction_ex_get_user( (uc_value_t *)callee ); + if( presolver ) + { + async_promise_resolver_t *resolver = *presolver; + if( resolver ) + { + async_manager_t *manager = async_manager_get( vm ); + async_resolve_or_reject( manager, resolver, uc_fn_arg(0), type); + } + } + return 0; +} +static uc_value_t * +async_resolver_resolve(uc_vm_t *vm, size_t nargs) +{ + return async_resolver_resolve_or_reject( vm, nargs, promiseThen ); +} + +static uc_value_t * +async_resolver_reject(uc_vm_t *vm, size_t nargs) +{ + return async_resolver_resolve_or_reject( vm, nargs, promiseCatch ); +} + +static void +async_resolver_destroy( uc_value_t *uv ) +{ + async_promise_resolver_t **presolver = (async_promise_resolver_t**)ucv_cfunction_ex_get_user( uv ); + if( presolver ) + { + async_promise_resolver_t *resolver = *presolver; + *presolver = 0; + if( resolver ) + { + async_promise_resolver_unlink( 0, resolver ); + } + } +} + +static int +uc_resolver_immediate(uc_vm_t *vm, void *user, int flags) +{ + async_promise_resolver_t *resolver = user; + async_manager_t *manager = async_manager_get( vm ); + int ex = EXCEPTION_NONE; + if (flags & UC_ASYNC_CALLBACK_FLAG_EXECUTE) + { + uc_vm_stack_push(vm, ucv_get(resolver->callback)); + uc_value_t *resolve = ucv_cfunction_ex_new( "resolve", async_resolver_resolve, async_resolver_destroy, sizeof(resolver) ); + uc_value_t *reject = ucv_cfunction_ex_new( "reject", async_resolver_reject, async_resolver_destroy, sizeof(resolver) ); + + async_promise_resolver_t **presolver = (async_promise_resolver_t**)ucv_cfunction_ex_get_user( resolve ); + resolver->refcount++; + *presolver = resolver; + + presolver = (async_promise_resolver_t**)ucv_cfunction_ex_get_user( reject ); + resolver->refcount++; + *presolver = resolver; + + uc_vm_stack_push(vm, resolve ); + uc_vm_stack_push(vm, reject ); + + ex = uc_vm_call(vm, false, 2); + + if( EXCEPTION_NONE == ex ) + ucv_put(uc_vm_stack_pop(vm)); + } + if (flags & UC_ASYNC_CALLBACK_FLAG_CLEANUP) + { + int ex2 = async_promise_resolver_unlink( manager, resolver ); + if( EXCEPTION_NONE == ex ) + ex = ex2; + } + return ex; +} + +struct async_promise_array_result +{ + struct async_promise_array *promise_array; + uc_value_t *value; + int state; // only in use by ASYNC_PROMISE_ALLSETTLED +}; + +typedef enum { + ASYNC_PROMISE_ALL = 1, + ASYNC_PROMISE_ANY, + ASYNC_PROMISE_RACE, + ASYNC_PROMISE_ALLSETTLED, +} promise_array_type_t; + +struct async_promise_array +{ + uint32_t refcount:9; + uint32_t exec_refcount:9; + uint32_t numresults: 8; + uint32_t type : 4; // all, any, race, allsettled + async_promise_resolver_t *resolver; + + struct async_promise_array_result results[]; +}; + +static void async_promise_array_unlink(struct async_promise_array *promise_array) +{ + if (0 != --promise_array->refcount) + return; + for( uint32_t n=0; nnumresults; n++ ) + ucv_put( promise_array->results[ n ].value ); + free(promise_array); +} + +static int async_promise_array_resolve( async_manager_t *manager, struct async_promise_array *promise_array, uc_value_t **args, size_t nargs, int type) +{ + if( !promise_array->resolver ) + return EXCEPTION_NONE; + + if( promisePending == type ) + { + DEBUG_PRINTF("%-1.3lf %p will be pending forever\n", async_manager_uptime(manager), promise_array->resolver); + // to prevent it from keeping the script running forever, we'll remove it's 'promise pending' status + async_promise_t *promise = promise_array->resolver->promise; + if( promise ) + { + if( promise->header.promise_pending ) + { + manager->pending_promises_cnt--; + promise->header.promise_pending = 0; + } + // and cleanup + promise->resolver = 0; + promise_array->resolver->promise = 0; + async_todo_unlink( manager, &promise->header ); + } + } + else + { + DEBUG_PRINTF("%-1.3lf %p resolved\n", async_manager_uptime(manager), promise_array->resolver); + uc_value_t *value = 0; + int value_type = 0; + + switch( (promise_array_type_t)promise_array->type ) + { + case ASYNC_PROMISE_ALL: + if( promiseCatch == type ) + value_type = 1; + else if( promiseThen == type ) + value_type = 2; + break; + case ASYNC_PROMISE_ANY: + if( promiseCatch == type ) + value_type = 2; + else if( promiseThen == type ) + value_type = 1; + break; + case ASYNC_PROMISE_RACE: + value_type = 1; + break; + case ASYNC_PROMISE_ALLSETTLED: + value_type = 3; + + } + switch( value_type ) + { + case 1: // the provided argument in the current call + { + if( nargs > 0) + value = args[0]; + break; + } + case 2: // the array of stored values + { + value = ucv_array_new_length( manager->vm, promise_array->numresults ); + for( uint32_t n=0; nnumresults; n++ ) + { + uc_value_t *elem = promise_array->results[ n ].value; + promise_array->results[ n ].value = 0; + if( elem ) + ucv_array_set( value, n, elem ); + } + break; + } + case 3: // the array of stored values, as struct (for 'allsettled) + { + value = ucv_array_new_length( manager->vm, promise_array->numresults ); + uc_value_t *fullfilled = 0, *rejected = 0; + for( uint32_t n=0; nnumresults; n++ ) + { + struct async_promise_array_result *result = + &promise_array->results[ n ]; + uc_value_t *obj = ucv_object_new( manager->vm ); + ucv_get( obj ); + if( result->state == promiseCatch ) + { + if( 0 == rejected ) rejected = ucv_string_new( "rejected" ); + ucv_object_add( obj, "status", ucv_get( rejected ) ); + ucv_object_add( obj, "reason", result->value ); + result->value = 0; + } + if( result->state == promiseThen ) + { + if( 0 == fullfilled ) fullfilled = ucv_string_new( "fullfilled" ); + ucv_object_add( obj, "status", ucv_get( fullfilled ) ); + ucv_object_add( obj, "value", result->value ); + result->value = 0; + } + ucv_array_set( value, n, obj ); + } + ucv_put( fullfilled ); + ucv_put( rejected ); + break; + } + } + + async_resolve_or_reject(manager, promise_array->resolver, value, type); + } + async_promise_resolver_unlink( manager, promise_array->resolver); + promise_array->resolver = 0; + return EXCEPTION_NONE; +} + +static int async_promise_array_immediate(uc_vm_t *vm, void *user, int flags) +{ + /* When we come in this function, the array of promises didn't contain + any usable value. So what to do? */ + struct async_promise_array *promise_array = user; + int ex = EXCEPTION_NONE; + if (flags & UC_ASYNC_CALLBACK_FLAG_EXECUTE) + { + int type = 0; + switch( (promise_array_type_t)promise_array->type ) + { + case ASYNC_PROMISE_ALL: + type = promiseThen; + break; + case ASYNC_PROMISE_ANY: + type = promiseCatch; + break; + case ASYNC_PROMISE_RACE: + /* According to the spec the promise should stay pending forever */ + type = promisePending; + break; + case ASYNC_PROMISE_ALLSETTLED: + type = promiseThen; + break; + } + ex = async_promise_array_resolve( async_manager_get( vm ), promise_array, 0, 0, type); + } + if (flags & UC_ASYNC_CALLBACK_FLAG_CLEANUP) + async_promise_array_unlink(promise_array); + return ex; +} + +static int async_promise_array_then(uc_vm_t *vm, void *user, uc_value_t **args, size_t nargs, int flags) +{ + struct async_promise_array_result *result = user; + struct async_promise_array *promise_array = result->promise_array; + async_manager_t *manager = async_manager_get( vm ); + + int ex = EXCEPTION_NONE; + if (flags & UC_ASYNC_CALLBACK_FLAG_EXECUTE) + { + DEBUG_PRINTF("%-1.3lf promise_array_then()\n", async_manager_uptime(manager)); + result->state = promiseThen; + int cnt = --promise_array->exec_refcount; + switch( (promise_array_type_t)promise_array->type ) + { + case ASYNC_PROMISE_ALL: + if( nargs ) result->value = ucv_get( args[ 0 ] ); + break; + case ASYNC_PROMISE_ANY: + cnt = 0; + break; + case ASYNC_PROMISE_RACE: + cnt = 0; + break; + case ASYNC_PROMISE_ALLSETTLED: + if( nargs ) result->value = ucv_get( args[ 0 ] ); + break; + } + if( 0 == cnt ) + ex = async_promise_array_resolve( manager, promise_array, args, nargs, promiseThen); + } + if (flags & UC_ASYNC_CALLBACK_FLAG_CLEANUP) + { + async_promise_array_unlink(promise_array); + } + return ex; +} + +static int async_promise_array_catch(uc_vm_t *vm, void *user, uc_value_t **args, size_t nargs, int flags) +{ + struct async_promise_array_result *result = user; + struct async_promise_array *promise_array = result->promise_array; + async_manager_t *manager = async_manager_get( vm ); + int ex = EXCEPTION_NONE; + if (flags & UC_ASYNC_CALLBACK_FLAG_EXECUTE) + { + DEBUG_PRINTF("%-1.3lf promise_array_catch()\n", async_manager_uptime(manager)); + result->state = promiseCatch; + int cnt = --promise_array->exec_refcount; + switch( (promise_array_type_t)promise_array->type ) + { + case ASYNC_PROMISE_ALL: + cnt = 0; + break; + case ASYNC_PROMISE_ANY: + if( nargs ) result->value = ucv_get( args[ 0 ] ); + break; + case ASYNC_PROMISE_RACE: + cnt = 0; + break; + case ASYNC_PROMISE_ALLSETTLED: + if( nargs ) result->value = ucv_get( args[ 0 ] ); + break; + } + if( 0 == cnt ) + ex = async_promise_array_resolve(manager, promise_array, args, nargs, promiseCatch); + } + if (flags & UC_ASYNC_CALLBACK_FLAG_CLEANUP) + { + async_promise_array_unlink(promise_array); + } + return ex; +} + +static uc_value_t * +async_promise_array_new( uc_vm_t *vm, size_t nargs, int type ) +{ + uc_value_t *arr = uc_fn_arg(0); + if (arr && arr->type != UC_ARRAY) + { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "arg1 needs to be an array"); + return 0; + } + + size_t length = ucv_array_length(arr); + if( length > 255 ) + { + // promise_array->numresults has only 8 bits + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "arg1p[] may not exceed 255 elements"); + return 0; + } + + async_manager_t *manager = async_manager_get( vm ); + if( !manager ) + return 0; + + struct async_promise_array *promise_array = + xalloc(sizeof(struct async_promise_array) + length * sizeof(struct async_promise_array_result)); + + promise_array->type = type; + async_promise_t *promise = uc_promise_new( manager ); + promise_array->resolver = async_promise_resolver_new(promise); + + for (size_t n = 0; n < length; n++) + { + uc_value_t *elem = ucv_array_get(arr, n); + async_promise_t **ppromise = (async_promise_t **)ucv_resource_dataptr(elem, _strPromise); + if (ppromise && *ppromise) + { + if( ASYNC_PROMISE_RACE == type && + ((*ppromise)->header.promise_state != promisePending) && + (promisePending == promise->header.promise_state) ) + { + // this one should fullfill the promise + if( (*ppromise)->header.promise_result_is_exception ) + { + promise->result.exception = (*ppromise)->result.exception; + (*ppromise)->result.exception = 0; + (*ppromise)->header.promise_result_is_exception = 0; + } + else + { + promise->result.value = (*ppromise)->result.value; + (*ppromise)->result.value = 0; + } + promise->header.promise_state = (*ppromise)->header.promise_state; + promise_array->resolver->promise = 0; + promise->resolver = 0; + async_todo_unlink( manager, &promise->header ); + // Lets continue normally to keep the code simple + } + struct async_promise_array_result *slot = + &promise_array->results[ promise_array->numresults++ ]; + slot->promise_array = promise_array; + promise_array->exec_refcount++; + promise_array->refcount++; + async_promise_add_c_int_user_args_flags_func( manager, *ppromise, async_promise_array_then, slot, promiseThen); + promise_array->refcount++; + async_promise_add_c_int_user_args_flags_func( manager, *ppromise, async_promise_array_catch, slot, promiseCatch); + } + else if( ASYNC_PROMISE_RACE == type ) + { + if( promisePending == promise->header.promise_state ) + { + // Promise should be resolved by this value + promise->result.value = ucv_get( elem ); + promise->header.promise_state = promiseThen; + promise_array->resolver->promise = 0; + promise->resolver = 0; + async_todo_unlink( manager, &promise->header ); + } + } + else if( ASYNC_PROMISE_ALLSETTLED == type ) + { + // This value should simply show up as fullfilled: + struct async_promise_array_result *slot = + &promise_array->results[ promise_array->numresults++ ]; + slot->promise_array = promise_array; + slot->state = promiseThen; + slot->value = ucv_get( elem ); + } + } + + if (0 == promise_array->exec_refcount && 0 != promise_array->resolver->promise ) + { + // Array didn't contain any promises. We will resolve in a 'setImmediate'. + promise_array->refcount++; + async_timer_t *timer = async_timer_c_int_user_flags_new( manager, async_promise_array_immediate, promise_array); + async_todo_put_in_list( manager, &timer->header); + timer->header.refcount--; + } + + return uc_resource_new( manager->promise_type, promise); +} + +static struct uc_value * +_uc_async_new_promise( struct uc_async_manager *_man, uc_async_promise_resolver_t **resolver) +{ + if( !resolver ) + return 0; + async_manager_t *manager = async_manager_cast( _man ); + async_promise_t *promise = uc_promise_new( manager ); + *resolver = &async_promise_resolver_new(promise)->header; + return uc_resource_new( manager->promise_type, promise); +} + +static void +_uc_async_resolve_reject( struct uc_async_manager *_man, uc_async_promise_resolver_t **resolver, uc_value_t *res, bool resolve) +{ + if (!resolver || !*resolver) + return; + async_manager_t *manager = async_manager_cast( _man ); + async_promise_resolver_t *res2 = async_promise_resolver_cast(*resolver); + *resolver = 0; + async_resolve_or_reject( manager, res2, res, resolve ? promiseThen : promiseCatch); + async_promise_resolver_unlink( manager, res2); +} + +static char *uc_cast_string(uc_vm_t *vm, uc_value_t **v, bool *freeable) { + if (ucv_type(*v) == UC_STRING) { + *freeable = false; + + return _ucv_string_get(v); + } + + *freeable = true; + + return ucv_to_string(vm, *v); +} + +/** + * Creates and returns a promise. The provided resolver function will be called + * asynchronously. + * + * @function module:async#Promise + * + * @param {Function} callback + * The callback used to deliver the {?module:async.resolver} object. + * + * @returns {?module:async.promise} + * + * @example + * // Create a promise + * async Promise( (resolve,reject)=> + * { + * resolve( 'world' ); + * print( 'hello ' ); + * }).then( (a)=> + * { + * print( a ); + * }); + * // will output 'hello world' + */ + +static uc_value_t * +Promise(uc_vm_t *vm, size_t nargs) +{ + uc_value_t *func = uc_fn_arg(0); + if (!ucv_is_callable(func)) + { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "arg1 needs to be callable"); + return 0; + } + + async_manager_t *manager = async_manager_get( vm ); + + async_promise_t *promise = uc_promise_new( manager ); + async_promise_resolver_t *resolver = async_promise_resolver_new(promise); + resolver->callback = ucv_get(func); + + async_timer_t *timer = async_timer_c_int_user_flags_new( manager, uc_resolver_immediate, resolver); + async_todo_put_in_list( manager, &timer->header); + timer->header.refcount--; + + return uc_resource_new( manager->promise_type, promise ); +} + +/** + * Takes an array of {@link module:async.promise|promises}, and returns a single promise. + * + * When one of the promises is rejected, the new promise is rejected immediately + * with the reject value. + * When all of the promises are resolved, the new promise is resolved with an array + * of the resolve values. + * + * @function module:async#PromiseAll + * + * @param {Array} promises + * Array of {@link module:async.promise|promises}. Elements in the array which are no promise are ignored. + * An empty array will cause the returned promise to be resolved immediately. + * + * @returns {?module:async.promise} + * + * @example + * function NewPromise( interval, value, do_reject ) + * { + * return async.Promise( (resolve,reject)=> + * { + * async.setTimeout( ()=> + * { + * if( do_reject ) reject( value ); + * resolve( value ); + * }, interval ); + * } ); + * } + * + * // Create an array of promises: + * let promises = [ NewPromise(300,'A',false), + * NewPromise(200,'B',false), + * 'hello', + * NewPromise(400,'C',true) ]; + * + * async.PromiseAll( promises ).then( (a)=> + * { + * print( 'fullfilled ', a, '\n' ); + * }).catch( (a)=> + * { + * print( 'rejected ', a, '\n' ); + * }); + * // will output 'rejected C', however, if you change the boolean for 'C' in false, + * // it will output 'fullfilled [ "A", "B", "C" ]' + */ + +static uc_value_t * +PromiseAll(uc_vm_t *vm, size_t nargs) +{ + return async_promise_array_new( vm, nargs, ASYNC_PROMISE_ALL ); +} + +/** + * Takes an array of {@link module:async.promise|promises}, and returns a single promise. + * + * The first promise which resolves, resolves the new promise. + * When none of the promises is resolved, the new promise will be rejected with + * an array of reject objects. + * + * @function module:async#PromiseAny + * + * @param {Array} promises + * Array of {@link module:async.promise|promises}. Elements in the array which are no promise are ignored. + * An empty array will cause the returned promise to be rejected immediately. + * + * @returns {?module:async.promise} + * + * @example + * function NewPromise( interval, value, do_reject ) + * { + * return async.Promise( (resolve,reject)=> + * { + * async.setTimeout( ()=> + * { + * if( do_reject ) reject( value ); + * resolve( value ); + * }, interval ); + * } ); + * } + * + * // Create an array of promises: + * let promises = [ NewPromise(300,'A',false), + * NewPromise(200,'B',false), + * 'hello', + * NewPromise(400,'C',true) ]; + * + * async.PromiseAny( promises ).then( (a)=> + * { + * print( 'fullfilled ', a, '\n' ); + * }).catch( (a)=> + * { + * print( 'rejected ', a, '\n' ); + * }); + * // will output 'fullfilled B', however, if you change the booleans for 'A' and 'B' + * // in true, it will output 'rejected [ "A", "B", "C" ]' + */ + +static uc_value_t * +PromiseAny(uc_vm_t *vm, size_t nargs) +{ + return async_promise_array_new( vm, nargs, ASYNC_PROMISE_ANY ); +} + +/** + * Takes an array of {@link module:async.promise|promises}, and returns a single promise. + * + * The first promise which settles, settles the new promise. + * When the array of promises contains promises which are already settled, + * or non-promise values, the new promise will settle with the first one of those + * in the array. + * + * @function module:async#PromiseRace + * + * @param {Array} promises + * Array of {@link module:async.promise|promises}. An empty array will cause the returned + * promise to be pending forever. + * + * @returns {?module:async.promise} + * + * @example + * function NewPromise( interval, value, do_reject ) + * { + * return async.Promise( (resolve,reject)=> + * { + * async.setTimeout( ()=> + * { + * if( do_reject ) reject( value ); + * resolve( value ); + * }, interval ); + * } ); + * } + * + * // Create an array of promises: + * let promises = [ NewPromise(300,'A',false), + * NewPromise(200,'B',false), + * 'hello', + * NewPromise(50,'C',true) ]; + * + * async.PromiseRace( promises ).then( (a)=> + * { + * print( 'fullfilled ', a, '\n' ); + * }).catch( (a)=> + * { + * print( 'rejected ', a, '\n' ); + * }); + * // will output 'fullfilled hello', however, if you remove the 'hello' value, + * // it will output 'rejected C' + */ + +static uc_value_t * +PromiseRace(uc_vm_t *vm, size_t nargs) +{ + return async_promise_array_new( vm, nargs, ASYNC_PROMISE_RACE ); +} + +/** + * Takes an array of {@link module:async.promise|promises}, and returns a single promise. + * + * When all promises are settled, the new promise is fullfilled with an array of objects. + * ``` + * { + * status: 'fullfilled', // or 'rejected' + * reason: 'whatever', // only set in case of 'rejected' + * value: 'whatever' // only set in case of 'fullfilled' + * } + * ``` + * A non-promise value in the promise array will be 'fullfilled' in the result array. + * + * @function module:async#PromiseAllSettled + * + * @param {Array} promises + * Array of {@link module:async.promise|promises}. + * + * @returns {?module:async.promise} + * + * @example + * function NewPromise( interval, value, do_reject ) + * { + * return async.Promise( (resolve,reject)=> + * { + * async.setTimeout( ()=> + * { + * if( do_reject ) reject( value ); + * resolve( value ); + * }, interval ); + * } ); + * } + * + * // Create an array of promises: + * let promises = [ NewPromise(300,'A',false), + * NewPromise(200,'B',false), + * 'hello', + * NewPromise(50,'C',true) ]; + * + * async.PromiseAllSettled( promises ).then( (a)=> + * { + * print( 'fullfilled ', a, '\n' ); + * }).catch( (a)=> + * { + * print( 'rejected ', a, '\n' ); + * }); + * // will output 'fullfilled [ { "status": "fullfilled", "value": "A" }, { "status": "fullfilled", "value": "B" }, { "status": "fullfilled", "value": "hello" }, { "status": "rejected", "reason": "C" } ]' + */ + +static uc_value_t * +PromiseAllSettled(uc_vm_t *vm, size_t nargs) +{ + return async_promise_array_new( vm, nargs, ASYNC_PROMISE_ALLSETTLED ); +} + +/** + * Helper function to make it possible to throw an object from a promise method. + * + * When calling this function from within a promise method, the promise is rejected, and + * the provided argument is delivered in the next catch handler. + * + * When called outside a promise method, async.throw( a ) is more or less a synonym of + * die( \`${a}\` ); + * + * @function module:async#throw + * + * @param {Any} error + * Object or value to be delivered in the next catch handler + * + * @throws {Error} + * + * @example + * async.Promise( (resolve,reject)=>{resolve('hello ')}).then((a)=> + * print( a ); + * async.throw( 'world' ); + * print( 'this will never be printed' ) + * }).then( (a)=> + * { + * print( 'this will also never be printed' ); + * }).catch( (a)=> + * print( a ) ); + * }); + * // Will output 'hello world'. + */ + +static uc_value_t * +Throw( uc_vm_t *vm, size_t nargs ) +{ + async_manager_t *manager = async_manager_get( vm ); + async_promise_t *promise = (0 == manager) ? 0 : manager->active_promise; + + if( promise ) + { + // Throw being called from inside a promise + promise->header.promise_state = promiseCatch; + promise->result.value = ucv_get( uc_fn_arg(0) ); + promise->header.promise_result_is_exception = 0; + + // create a 'lightweight' exception, to prevent further code execution + vm->exception.type = EXCEPTION_USER; + return ucv_boolean_new( true ); + } + + // Create a 'fullblown' exception + uc_value_t *v = uc_fn_arg(0); + bool freeable = false; + char *casted = uc_cast_string( vm, &v, &freeable ); + uc_vm_raise_exception( vm, EXCEPTION_USER, "%s", casted ); + if( freeable ) + free( casted ); + return ucv_boolean_new( false ); +} + +static const uc_function_list_t local_async_fns[] = { + {"Promise", Promise}, + {"PromiseAll", PromiseAll}, + {"PromiseAny", PromiseAny}, + {"PromiseRace", PromiseRace}, + {"PromiseAllSettled",PromiseAllSettled}, + {"throw", Throw} +}; + +void async_promise_init( async_manager_t *manager, uc_value_t *scope ) +{ + manager->header.new_promise = _uc_async_new_promise; + manager->header.resolve_reject = _uc_async_resolve_reject; + + /* promise initializing */ + manager->promise_type = uc_type_declare(manager->vm, _strPromise, promise_type_fns, close_promise); + + uc_function_list_register(scope, local_async_fns); +} diff --git a/lib/async/promise.h b/lib/async/promise.h new file mode 100644 index 00000000..5d1d9705 --- /dev/null +++ b/lib/async/promise.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +This file is part of the async plugin for ucode +*/ + +#ifndef UC_ASYNC_PROMISE_H +#define UC_ASYNC_PROMISE_H + +#include "manager.h" + +extern __hidden int +async_promise_destroy( async_manager_t *, async_todo_t *); + +extern __hidden int +async_promise_do( async_manager_t *, async_todo_t *); + +typedef struct async_promise +{ + async_todo_t header; + async_manager_t *manager; + struct uc_async_promise_resolver *resolver; + union + { + uc_value_t *value; + uc_exception_t *exception; + } result; + /* Contains the ucode function which caused the reject. + To be used for the user feedback when no catch handler is found */ + uc_value_t *reject_caused_by; + + struct async_promise_method *stack; + +} async_promise_t; + +extern __hidden async_promise_t * +uc_promise_new( async_manager_t *manager ); + +extern __hidden void +async_promise_init( async_manager_t *manager, uc_value_t *scope ); + +#endif //ndef UC_ASYNC_PROMISE_H diff --git a/lib/async/queuer.c b/lib/async/queuer.c new file mode 100644 index 00000000..aeafc1a8 --- /dev/null +++ b/lib/async/queuer.c @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +This file is part of the async plugin for ucode +*/ +#include +#include +#include + +#include "ucode/lib.h" +#include "ucode/vm.h" +#include "ucode/platform.h" +#include "ucode/async.h" + +#include "manager.h" +#include "timer.h" +#include "queuer.h" + + +/******* + * The part of the code which is responsible for multithreaded asynchronity + **/ +#define SIGNEWCALLBACK SIGUSR1 // Signal used for async callbacks +/* +static uc_value_t * +async_callback_signal_handler( uc_vm_t *vm, size_t nargs ) +{ + // Do nothing. We only want to interrupt the async_sleep function +#ifdef DEBUG_PRINT + async_manager_t *manager = async_manager_get( vm ); + DEBUG_PRINTF( "%-1.3lf Signal handler vm=%p\n", manager ? async_manager_uptime(manager) : NAN, vm ); +#endif + return 0; +} +*/ +static async_manager_t * +async_callback_is_synchron( const async_callback_queuer_t *queuer ) +{ + if (queuer->thread == pthread_self()) + return queuer->manager; + return 0; +} + +/* Wakeup the sleeping script engine */ +static void +async_callback_queuer_wakeup( const async_callback_queuer_t *queuer ) +{ + if (async_callback_is_synchron( queuer ) ) + // running in the script thread + return; + + DEBUG_PRINTF( "%-1.3lf Wakeup script vm=%p\n", async_manager_uptime( queuer->manager ), queuer->manager->vm ); + // send a signal to the script thread; + union sigval info = {0}; + info.sival_ptr = (void *)queuer->thread; + + pthread_sigqueue(queuer->thread, SIGNEWCALLBACK, info); +} + +void async_sleep( async_manager_t *manager, int64_t msec ) +{ + sigset_t waitset; + struct timespec timeout; + + sigemptyset( &waitset ); + sigaddset( &waitset, SIGNEWCALLBACK ); + + timeout.tv_sec = msec / 1000; + timeout.tv_nsec = (msec % 1000) * 1000000; + + sigtimedwait( &waitset, 0, &timeout ); +} + + +static int +async_callback_queuer_addref( const async_callback_queuer_t *cqueuer, bool add ) +{ + async_callback_queuer_t *queuer = (async_callback_queuer_t *)cqueuer; + if( add ) + return __atomic_add_fetch(&queuer->refcount, 1, __ATOMIC_RELAXED); + else + return __atomic_add_fetch(&queuer->refcount, -1, __ATOMIC_RELAXED); +} + +static struct async_callback_queuer_chain * +uc_async_queuer_lock_stack( const async_callback_queuer_t *cqueuer) +{ + async_callback_queuer_t *queuer = (async_callback_queuer_t *)cqueuer; + struct async_callback_queuer_chain **pstack = &queuer->stack; + /* + The stack is locked as the least significant bit is 1. + So we try to set it, which only succeeds if it is not set now. + */ + while (true) + { + struct async_callback_queuer_chain *oldstack = *pstack; + oldstack = (void *)(((intptr_t)oldstack) & ~(intptr_t)1); + struct async_callback_queuer_chain *newstack = oldstack; + newstack = (void *)(((intptr_t)newstack) | (intptr_t)1); + if (__atomic_compare_exchange_n( pstack, &oldstack, newstack, false, __ATOMIC_RELAXED, __ATOMIC_RELAXED)) + { + return oldstack; + } + } +} + +static void +uc_async_queuer_unlock_stack( const async_callback_queuer_t *cqueuer, struct async_callback_queuer_chain *func) +{ + async_callback_queuer_t *queuer = (async_callback_queuer_t *)cqueuer; + struct async_callback_queuer_chain **pstack = &queuer->stack; + /* + Unlock the stack by writing a 'clean' pointer, without bit 0 set. + */ + __atomic_store_n( pstack, func, __ATOMIC_RELAXED); +} + +/******* + * End of multithreaded functions + */ + +struct async_callback_queuer_chain +{ + struct async_callback_queuer_chain *next; + struct async_callback callback; +}; + +static inline async_callback_queuer_t * +async_callback_queuer_cast(struct uc_async_callback_queuer *handler) +{ + return (async_callback_queuer_t *)handler; +} + +static inline async_callback_queuer_t const * +async_callback_queuer_cast_const(struct uc_async_callback_queuer const *handler) +{ + return (async_callback_queuer_t const *)handler; +} + +int +async_handle_queued_callbacks( async_manager_t *manager ) +{ + if( 0 == manager || 0 == manager->callback_queuer ) + return EXCEPTION_NONE; + async_callback_queuer_t *queuer = manager->callback_queuer; + struct async_callback_queuer_chain *stack = uc_async_queuer_lock_stack(queuer); + uc_async_queuer_unlock_stack(queuer, 0); + + if (0 == stack) + return EXCEPTION_NONE; + + reverse_stack(struct async_callback_queuer_chain, stack); + + while (stack) + { + struct async_callback_queuer_chain *pop = stack; + stack = pop->next; + int ex = async_callback_call( manager, &pop->callback, 0, 0, 0, true); + free( pop ); + if (EXCEPTION_NONE == ex) + continue; + if (stack) + { + // put remaining stack back + struct async_callback_queuer_chain *last = stack; + reverse_stack(struct async_callback_queuer_chain, stack); + last->next = uc_async_queuer_lock_stack(queuer); + uc_async_queuer_unlock_stack(queuer, stack); + } + return ex; + } + return EXCEPTION_NONE; +} + +bool +async_any_queued_callbacks_waiting( async_manager_t *manager ) +{ + if (0 == manager || 0 == manager->callback_queuer) + return false; + async_callback_queuer_t *queuer = manager->callback_queuer; + if ((intptr_t)queuer->stack & ~(intptr_t)3) + return true; + return false; +} + +static bool +_uc_async_request_callback(struct uc_async_callback_queuer const *_queuer, + int (*func)(struct uc_vm *, void *, int), void *user) +{ + struct async_callback_queuer_chain *pfunc = xalloc(sizeof(struct async_callback_queuer_chain)); + pfunc->callback.callback_type = callbackC_int_user_flags; + pfunc->callback.c_int_user_flags.func = func; + pfunc->callback.c_int_user_flags.user = user; + + const async_callback_queuer_t *queuer = async_callback_queuer_cast_const(_queuer); + + struct async_callback_queuer_chain *stack = uc_async_queuer_lock_stack(queuer); + + if (stack == (struct async_callback_queuer_chain *)queuer ) + { + // vm doesn't exist anymore + uc_async_queuer_unlock_stack( queuer, stack); + free(pfunc); + return false; + } + + pfunc->next = stack; + uc_async_queuer_unlock_stack( queuer, pfunc); + + async_callback_queuer_wakeup( queuer ); + return true; +} + +void +async_wakeup( const uc_async_callback_queuer_t *_queuer ) +{ + if( !_queuer ) + return; + const async_callback_queuer_t *queuer = async_callback_queuer_cast_const( _queuer ); + async_callback_queuer_wakeup( queuer ); +} + + +static void +_uc_async_callback_queuer_free(struct uc_async_callback_queuer const **pqueuer) +{ + if (0 == pqueuer || 0 == *pqueuer) + return; + async_callback_queuer_t const *queuer = async_callback_queuer_cast_const(*pqueuer); + *pqueuer = 0; + + if( 0 == async_callback_queuer_addref( queuer, false ) ) + { + if( 0 == queuer->manager ) + free( (void *)queuer ); + } +} + +static int +_async_put_todo_in_list( uc_vm_t *vm, void *user, int flags) +{ + async_todo_t *todo = user; + async_manager_t *manager = async_manager_get( vm ); + + if (flags & UC_ASYNC_CALLBACK_FLAG_EXECUTE) + { + async_todo_put_in_list( manager, todo ); + } + if (flags & UC_ASYNC_CALLBACK_FLAG_CLEANUP) + { + async_todo_unlink( manager, todo ); + } + return EXCEPTION_NONE; +} + +static uc_async_timer_t * +_uc_async_create_timer(struct uc_async_callback_queuer const *_queuer, + int (*cb)(uc_vm_t *, void *, int), void *user, uint32_t msec, bool periodic) +{ + async_timer_t *timer = async_timer_c_int_user_flags_new(0, cb, user); + timer->due = async_timer_current_time() + msec; + if (periodic) + timer->periodic = msec; + timer->header.refcount++; + + async_callback_queuer_t const *queuer = async_callback_queuer_cast_const(_queuer); + + // are we synchron? + async_manager_t *manager = async_callback_is_synchron( queuer ); + if (manager) + { + _async_put_todo_in_list( manager->vm, &timer->header, UC_ASYNC_CALLBACK_FLAG_EXECUTE | UC_ASYNC_CALLBACK_FLAG_CLEANUP); + } + else + { + _uc_async_request_callback(_queuer, _async_put_todo_in_list, &timer->header ); + } + + return &timer->header.header; +} + +static int +_async_free_timer(uc_vm_t *vm, void *user, int flags) +{ + uintptr_t v = (uintptr_t)user; + bool clear = v & 1; + v = v & ~((uintptr_t)1); + async_timer_t *timer = (async_timer_t *)v; + async_manager_t *manager = async_manager_get(vm); + if (flags & UC_ASYNC_CALLBACK_FLAG_EXECUTE) + { + if (clear) + async_timer_destroy( manager, &timer->header ); + } + if (flags & UC_ASYNC_CALLBACK_FLAG_CLEANUP) + { + async_todo_unlink( async_manager_get(vm), &timer->header); + } + return EXCEPTION_NONE; +} + + +static void +_uc_async_free_timer(struct uc_async_callback_queuer const *_queuer, + uc_async_timer_t **_pptimer, bool clear) +{ + async_callback_queuer_t const *queuer = async_callback_queuer_cast_const(_queuer); + async_timer_t **pptimer = (async_timer_t **)_pptimer; + if (!pptimer || !*pptimer) + return; + async_timer_t *timer = *pptimer; + *_pptimer = 0; + + // use bit 0 to store the clear flag + if (clear) + { + timer = (async_timer_t *)(((uintptr_t)timer) | 1); + } + + // are we synchron? + async_manager_t *manager = async_callback_is_synchron( queuer ); + if (manager) + { + _async_free_timer( manager->vm, timer, UC_ASYNC_CALLBACK_FLAG_EXECUTE | UC_ASYNC_CALLBACK_FLAG_CLEANUP); + } + else + { + _uc_async_request_callback(_queuer, _async_free_timer, timer); + } +} + +static struct uc_async_callback_queuer const * +_uc_async_new_callback_queuer( struct uc_async_manager *_man ) +{ + async_manager_t *manager = async_manager_cast( _man ); + if( 0 == manager ) + return 0; + + async_callback_queuer_t *queuer = manager->callback_queuer; + if( 0 == queuer ) + { + manager->callback_queuer = queuer = xalloc(sizeof(async_callback_queuer_t)); + + sigset_t set; + + sigemptyset(&set); + sigaddset(&set, SIGNEWCALLBACK ); + if( 0 != pthread_sigmask( SIG_BLOCK, &set, &queuer->oldmask ) ) + { + perror( "pthread_sigmask" ); + exit( 1 ); + } + +/* + pthread_sigmask( ) + // Setup signal handler + uc_cfn_ptr_t ucsignal = uc_stdlib_function("signal"); + uc_value_t *func = ucv_cfunction_new("async", async_callback_signal_handler); + + uc_vm_stack_push( manager->vm, ucv_uint64_new( SIGNEWCALLBACK )); + uc_vm_stack_push( manager->vm, func); + + if (ucsignal(manager->vm, 2) != func) + fprintf(stderr, "Unable to install async_callback_signal_handler\n"); + else + { + DEBUG_PRINTF("%-1.3lf vm=%p signal handler installed\n", async_manager_uptime(manager), manager->vm ); + + } + + ucv_put(uc_vm_stack_pop( manager->vm)); + ucv_put(uc_vm_stack_pop( manager->vm)); + ucv_put( func ); +*/ + + // Remember the thread ID + queuer->thread = pthread_self(); + // And the vm + queuer->manager = async_manager_link( manager ); + + queuer->header.free = _uc_async_callback_queuer_free; + queuer->header.request_callback = _uc_async_request_callback; + queuer->header.create_timer = _uc_async_create_timer; + queuer->header.free_timer = _uc_async_free_timer; + + queuer->refcount = 1; + } + + async_callback_queuer_addref( queuer, true ); + return &queuer->header; +} + +void +async_callback_queuer_free( async_manager_t *manager, async_callback_queuer_t *queuer) +{ + if (0 == queuer) + return; + + struct async_callback_queuer_chain *stack = uc_async_queuer_lock_stack( queuer ); + // write sentinel value meaning that callbacks are disabled forever + uc_async_queuer_unlock_stack( queuer, (void *)queuer ); + + async_manager_unlink( queuer->manager ); + queuer->manager = 0; + + struct uc_async_callback_queuer const *pconsth = &queuer->header; + _uc_async_callback_queuer_free(&pconsth); + + // TODO: Shouldn't we release the signal handler? + + // call all function on stack with exec=false to make them able to free up resources + while (stack) + { + struct async_callback_queuer_chain *pop = stack; + stack = pop->next; + async_callback_destroy( manager->vm, &pop->callback); + } +} + +void +async_callback_queuer_init( async_manager_t *manager, uc_value_t *scope ) +{ + manager->header.new_callback_queuer = _uc_async_new_callback_queuer; +} + diff --git a/lib/async/queuer.h b/lib/async/queuer.h new file mode 100644 index 00000000..676ef7d4 --- /dev/null +++ b/lib/async/queuer.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +This file is part of the async plugin for ucode +*/ +#ifndef ASYNC_QUEUER_H +#define ASYNC_QUEUER_H + +#include +#include "ucode/async.h" +#include "manager.h" + +struct async_callback_queuer_chain; + +typedef struct async_callback_queuer +{ + struct uc_async_callback_queuer header; + + // linked list of callbacks to be executed + struct async_callback_queuer_chain *stack; + int refcount; + + // Thread of the script + pthread_t thread; + // VM in which we live. + async_manager_t *manager; + // Stored sigmask + sigset_t oldmask; +} async_callback_queuer_t; + +extern __hidden void +async_callback_queuer_init( async_manager_t *manager, uc_value_t *scope ); + + +extern __hidden void +async_callback_queuer_free( async_manager_t *, async_callback_queuer_t * ); + +extern __hidden int +async_handle_queued_callbacks( async_manager_t *manager ); + +extern __hidden bool +async_any_queued_callbacks_waiting( async_manager_t *manager ); + +extern __hidden void +async_wakeup( const uc_async_callback_queuer_t *queuer ); + +extern __hidden void +async_sleep( async_manager_t *manager, int64_t msec ); + + +#endif // ndef ASYNC_QUEUER_H \ No newline at end of file diff --git a/lib/async/timer.c b/lib/async/timer.c new file mode 100644 index 00000000..1fb15e71 --- /dev/null +++ b/lib/async/timer.c @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +This file is part of the async plugin for ucode +*/ + +#include "ucode/lib.h" +#include "ucode/vm.h" +#include "ucode/types.h" + +#include "manager.h" +#include "callback.h" +#include "timer.h" + +/** + * Represents a timer object as returned by + * {@link module:async#setTimeout|setTimeout()}, {@link module:async#setInterval|setInterval()} or {@link module:async#setImmediate|setImmediate()}. + * + * This class has no methods. The only sane usage is to pass it to {@link module:async#clearTimeout|clearTimeout()} + * + * @class module:async.timer + * @hideconstructor + * + */ +// async_timer_t + +int64_t +async_timer_current_time(); + +static async_timer_t * +uc_timer_ucode_new( async_manager_t *manager, size_t nargs, uc_value_t *cb, size_t startarg) +{ + size_t n_args = nargs - startarg; + async_timer_t *timer = xalloc(sizeof(async_timer_t) + n_args * sizeof(uc_value_t *)); + timer->header.refcount = 1; + timer->header.todo_type = todoTimer; + timer->callback.callback_type = callbackUcode; + timer->callback.nargs = n_args; + timer->callback.ucode.func = ucv_get(cb); + uc_vm_t *vm = manager->vm; + + for (size_t n1 = startarg, n2 = 0; n1 < nargs; n1++, n2++) + timer->callback.args[n2] = ucv_get(uc_fn_arg(n1)); + + return timer; +} + +async_timer_t * +async_timer_c_int_user_flags_new( async_manager_t *manager, int (*func)(uc_vm_t *, void *, int), void *user) +{ + async_timer_t *timer = xalloc(sizeof(async_timer_t)); + timer->header.refcount = 1; + timer->header.todo_type = todoTimer; + timer->callback.callback_type = callbackC_int_user_flags; + timer->callback.c_int_user_flags.func = func; + timer->callback.c_int_user_flags.user = user; + return timer; +} + +void +async_timer_destroy( async_manager_t *manager, async_todo_t *todo ) +{ + if( !todo ) + return; + async_timer_t *timer = async_timer_cast( todo ); + timer->header.todo_type = todoClearedTimer; + timer->due = 0; + timer->periodic = 0; + + async_callback_destroy( manager ? manager->vm : 0, &timer->callback ); +} + +int +async_timer_do( async_manager_t *manager, async_todo_t *todo ) +{ + async_timer_t *timer = async_timer_cast(todo); + int ex = async_callback_call(manager, &timer->callback, 0, 0, 0, false); + if (0 == timer->periodic || + // the timer can be cleared in the callback itself + todoClearedTimer == timer->header.todo_type) + return ex; + timer->due += timer->periodic; + async_todo_put_in_list(manager, &timer->header); + return ex; +} + +int64_t +async_timer_current_time() +{ + struct timespec monotime; + clock_gettime(CLOCK_MONOTONIC, &monotime); + return ((int64_t)monotime.tv_sec) * 1000 + (monotime.tv_nsec / 1000000); +} + + +enum +{ + timerTimeout = 0, + timerPeriodic, + timerImmediate, +}; + +static const char *_strTimer = "async.timer"; + +static void +close_timer(void *p) +{ + async_timer_t *timer = p; + if (timer) + { + async_todo_unlink(0, &timer->header); + } +} + +static uc_value_t * +createTimer(uc_vm_t *vm, size_t nargs, int type) +{ + uc_value_t *cb = uc_fn_arg(0); + if (!ucv_is_callable(cb)) + { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "arg1 needs to be callable"); + return 0; + } + int64_t timeout = 0; + if (nargs > 1 && timerImmediate != type) + { + uc_value_t *arg2 = uc_fn_arg(1); + timeout = ucv_int64_get(arg2); + } + else if (timerPeriodic == type) + { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "arg2 needs to be a number"); + return 0; + } + + if (timerPeriodic == type && timeout < 1) + timeout = 1; + + size_t startarg = 2; + if (timerImmediate == type) + startarg = 1; + + async_manager_t *manager = async_manager_get( vm ); + + async_timer_t *timer = uc_timer_ucode_new( manager, nargs, cb, startarg); + + timer->periodic = (timerPeriodic == type) ? timeout : 0; + + if (timerImmediate == type) + { + timer->due = 0; + } + else + { + timer->due = async_timer_current_time() + timeout; + } + + async_todo_put_in_list(manager, &timer->header); + timer->header.refcount--; + + if (0 == manager->timer_type) + { + static const uc_function_list_t timer_fns[] = {}; + manager->timer_type = uc_type_declare(vm, _strTimer, timer_fns, close_timer); + } + + timer->header.refcount++; + return uc_resource_new(manager->timer_type, timer); +} + +/** + * Start a timer, to execute it's callback after a delay. + * + * The timer can be stopped before the callback is executed using clearTimeout() + * + * @function module:async#setTimeout + * + * @param {Function} callback + * + * @param {Number} [interval = 0] + * Optional time to be waited (in msec) before the callback is called. + * + * @param {Any} ...args + * Optional Argument(s) to be passed to the callback function. + * + * @returns {?module:async.timer} + * + * @example + * async.setTimeout( (a)=> + * { + * print( a ); + * }, 10000, 'hello world' ); + * + * while( async.PumpEvents() ); + * // Will output 'hello world' after 10 seconds, and then exit. + */ + +static uc_value_t * +setTimeout(uc_vm_t *vm, size_t nargs) +{ + return createTimer(vm, nargs, timerTimeout); +} + +/** + * Start a periodic timer, to execute it's callback at each interval. + * + * The timer can be stopped using clearTimeout() + * + * @function module:async#setInterval + * + * @param {Function} callback + * + * @param {Number} interval + * Interval time in millisec. + * + * @param {Any} ...args + * Optional Argument(s) to be passed to the callback function. + * + * @returns {?module:async.timer} + * + * @example + * async.setInterval( (a)=> + * { + * print( `${++a.count}\n` ); + * }, 1000, { count: 0 } ); + * + * while( async.PumpEvents() ); + * // Will output '1\n2\n3\n...' forever. + */ + +static uc_value_t * +setInterval(uc_vm_t *vm, size_t nargs) +{ + return createTimer(vm, nargs, timerPeriodic); +} + +/** + * Let callback be executed in the next event pump stroke. + * + * In theory it can be stopped using clearTimeout() + * + * A *setImmediate()* is executed before *setTimeout( ()=>{}, 0 )*. + * Background: the setTimeout() is scheduled at *now + 0 msec*, + * while 'setImmediate()' is scheduled at *start of time*, + * which has already passed. + * + * @function module:async#setImmediate + * + * @param {Function} callback + * + * @param {Any} ...args + * Optional Argument(s) to be passed to the callback function. + * + * @returns {?module:async.timer} + * + * @example + * async.setTimeout( (a)=> + * { + * print( a ); + * }, 0, 'world' ); + * async.setImmediate( (a)=> + * { + * print( a ); + * }, 'hello ' ); + * + * while( async.PumpEvents() ); + * // Will output 'hello world', and exit. + */ + +static uc_value_t * +setImmediate(uc_vm_t *vm, size_t nargs) +{ + return createTimer(vm, nargs, timerImmediate); +} + +/** + * Clears a timer. It's safe to call it more than once on the same timer object. + * + * @function module:async#clearTimeout + * + * @param {?module:async.timer} timer + * + * @returns {Boolean} + * True if the timer is a valid timer object, and false if it isn't. + * + * @example + * let timer = async.setTimeout( (a)=> + * { + * print( 'hello world' ); + * }, 1000 ); + * async.clearTimeout( timer ); + * while( async.PumpEvents() ); + * // Will output nothing, and exit immediately. + */ + +static uc_value_t * +clearTimeout(uc_vm_t *vm, size_t nargs) +{ + async_timer_t **ptimer = (async_timer_t **)ucv_resource_dataptr(uc_fn_arg(0), _strTimer); + if (ptimer && *ptimer) + { + async_timer_t *timer = *ptimer; + async_manager_t *manager = async_manager_get( vm ); + async_timer_destroy( manager, &timer->header ); + return ucv_boolean_new(true); + } + return ucv_boolean_new(false); +} + +static const uc_function_list_t local_async_fns[] = { + {"setTimeout", setTimeout}, + {"setInterval", setInterval}, + {"setImmediate", setImmediate}, + {"clearTimeout", clearTimeout}, +}; + +void +async_timer_init( async_manager_t *manager, uc_value_t *scope ) +{ + uc_function_list_register(scope, local_async_fns); +} diff --git a/lib/async/timer.h b/lib/async/timer.h new file mode 100644 index 00000000..2977f697 --- /dev/null +++ b/lib/async/timer.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 Isaac de Wolff + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +This file is part of the async plugin for ucode +*/ + +#ifndef UC_ASYNC_TIMER_H +#define UC_ASYNC_TIMER_H + +#include "manager.h" +#include "callback.h" + +extern __hidden int64_t +async_timer_current_time(); + +typedef struct async_timer +{ + async_todo_t header; + int64_t due; + uint32_t periodic; + struct async_callback callback; +} async_timer_t; + +// Safety. Let the compiler error out when the wrong type is casted. +static inline struct async_timer * +async_timer_cast(async_todo_t *p) +{ + DEBUG_ASSERT(p->todo_type & todoTimer); + return (struct async_timer *)p; +} + +extern __hidden void +async_timer_destroy( async_manager_t *, async_todo_t * ); + +extern __hidden int +async_timer_do( async_manager_t *, async_todo_t * ); + +extern __hidden struct async_timer * +async_timer_c_int_user_flags_new( async_manager_t *manager, int (*func)(uc_vm_t *, void *, int), void *user); + +extern __hidden void +async_timer_init( async_manager_t *manager, uc_value_t *scope ); + +#endif //ndef UC_ASYNC_TIMER_H diff --git a/vm.c b/vm.c index a3dc05b6..2abb49a8 100644 --- a/vm.c +++ b/vm.c @@ -29,6 +29,7 @@ #include "ucode/program.h" #include "ucode/lib.h" /* uc_error_context_format() */ #include "ucode/platform.h" +#include "ucode/async.h" #undef __insn #define __insn(_name) #_name, @@ -3087,6 +3088,8 @@ uc_vm_execute(uc_vm_t *vm, uc_program_t *program, uc_value_t **retval) status = uc_vm_execute_chunk(vm); + status = uc_async_finish( vm, status, UINT_MAX ); + switch (status) { case STATUS_OK: val = uc_vm_stack_pop(vm); From d7caa84dc39e579d90841925c612daa30105eb02 Mon Sep 17 00:00:00 2001 From: Isaac de Wolff Date: Fri, 3 Jan 2025 15:03:29 +0100 Subject: [PATCH 04/13] Solve a problem with atomic operations. In clang the value has to be declared as _Atomic, while this isn't needed (or forced) in gcc. --- lib/async/alien.c | 6 +++--- lib/async/alien.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/async/alien.c b/lib/async/alien.c index a3572265..9dc21824 100644 --- a/lib/async/alien.c +++ b/lib/async/alien.c @@ -38,7 +38,7 @@ This file is part of the async plugin for ucode #ifdef ASYNC_HAS_ALIENS static int -async_futex(uint32_t *uaddr, int futex_op, uint32_t val, +async_futex( _Atomic uint32_t *uaddr, int futex_op, uint32_t val, const struct timespec *timeout, uint32_t *uaddr2, uint32_t val3) { return syscall(SYS_futex, uaddr, futex_op, val, @@ -46,7 +46,7 @@ async_futex(uint32_t *uaddr, int futex_op, uint32_t val, } static void -async_alien_enter_futex( uint32_t *futex_addr ) +async_alien_enter_futex( _Atomic uint32_t *futex_addr ) { while (1) { @@ -76,7 +76,7 @@ async_alien_enter( async_manager_t *manager ) } static void -async_alien_release_futex( uint32_t *futex_addr ) +async_alien_release_futex( _Atomic uint32_t *futex_addr ) { const uint32_t zero = 0; atomic_store( futex_addr, zero ); diff --git a/lib/async/alien.h b/lib/async/alien.h index cb0074fe..74e351df 100644 --- a/lib/async/alien.h +++ b/lib/async/alien.h @@ -37,7 +37,7 @@ typedef struct async_alien // to know if an alien call should interrupt the sleep int todo_seq; - uint32_t the_futex; + _Atomic uint32_t the_futex; uint32_t refcount; // zero if vm ended From c77eee0626494a303f5b8fa49b1082b12e74d053 Mon Sep 17 00:00:00 2001 From: Isaac de Wolff Date: Fri, 3 Jan 2025 15:11:55 +0100 Subject: [PATCH 05/13] Remove an (by clang) unrecognized #pragma --- lib/async/promise.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/async/promise.c b/lib/async/promise.c index 5347e92d..ed4c287d 100644 --- a/lib/async/promise.c +++ b/lib/async/promise.c @@ -254,7 +254,7 @@ async_promise_destroy( async_manager_t *manager, async_todo_t *todo) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" -#pragma GCC diagnostic ignored "-Wuse-after-free" +//#pragma GCC diagnostic ignored "-Wuse-after-free" static const char *err = "Rejected promise %p without catch handler\n"; if (vm_is_active ) { From 8233650a7083b9cc52290e0d6b57d7b3ef164707 Mon Sep 17 00:00:00 2001 From: Isaac de Wolff Date: Sat, 4 Jan 2025 09:14:55 +0100 Subject: [PATCH 06/13] Link pthread in the examples binaries. --- examples/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 12d32ba2..85989431 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -3,5 +3,5 @@ FOREACH(example ${examples}) GET_FILENAME_COMPONENT(example ${example} NAME_WE) SET(CLI_SOURCES main.c) ADD_EXECUTABLE(${example} ${example}.c) - TARGET_LINK_LIBRARIES(${example} libucode ${json}) + TARGET_LINK_LIBRARIES(${example} libucode ${json} pthread) ENDFOREACH(example) From 48c607714869fef37b4077752dfb1e2a234f1dac Mon Sep 17 00:00:00 2001 From: Isaac de Wolff Date: Sat, 4 Jan 2025 09:41:11 +0100 Subject: [PATCH 07/13] Remove null pointer warning in 'ynezz/gh-actions-openwrt-ci-native' --- include/ucode/types.h | 8 +++++--- lib/async/promise.c | 5 ++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/include/ucode/types.h b/include/ucode/types.h index 09aa248c..6d2a1617 100644 --- a/include/ucode/types.h +++ b/include/ucode/types.h @@ -477,11 +477,13 @@ ucv_cfunction_ex_get_magic() static inline bool ucv_is_cfunction_ex( uc_value_t *uv ) { + if( NULL == uv ) + return false; if( ucv_type(uv) != UC_CFUNCTION ) return false; - const char *magic; - if( !ucv_cfunction_ex_helper( 2, &magic ) ) - return false; + const char *magic = 0; + if( !ucv_cfunction_ex_helper( 2, &magic ) || NULL == magic ) + return false; return 0 == strcmp( ((uc_cfunction_t *)uv)->name, magic ); } diff --git a/lib/async/promise.c b/lib/async/promise.c index ed4c287d..218008a5 100644 --- a/lib/async/promise.c +++ b/lib/async/promise.c @@ -662,7 +662,10 @@ async_promise_resolver_unlink( async_manager_t *manager, async_promise_resolver_ static uc_value_t * async_resolver_resolve_or_reject( uc_vm_t *vm, size_t nargs, int type ) { - uc_cfunction_t *callee = uc_vector_last(&vm->callframes)->cfunction; + uc_callframe_t *lastframe = uc_vector_last(&vm->callframes); + if( !lastframe ) + return 0; + uc_cfunction_t *callee = lastframe->cfunction; async_promise_resolver_t **presolver = (async_promise_resolver_t**)ucv_cfunction_ex_get_user( (uc_value_t *)callee ); if( presolver ) { From b17bf116fdb00dda598344420d2d2d9658580b57 Mon Sep 17 00:00:00 2001 From: Isaac de Wolff Date: Sat, 4 Jan 2025 10:00:53 +0100 Subject: [PATCH 08/13] Remove an apparently gnu-only extension in uc_cfunction_ex_t. --- include/ucode/types.h | 3 ++- types.c | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/include/ucode/types.h b/include/ucode/types.h index 6d2a1617..74c1e2c4 100644 --- a/include/ucode/types.h +++ b/include/ucode/types.h @@ -196,7 +196,8 @@ typedef struct { typedef void (*uc_cfn_feedback_t)( uc_value_t * ); typedef struct { - uc_cfunction_t header; + uc_value_t header; + uc_cfn_ptr_t cfn; intptr_t magic; uc_cfn_feedback_t feedback; char name[]; diff --git a/types.c b/types.c index f323d983..dd69fcbc 100644 --- a/types.c +++ b/types.c @@ -1162,6 +1162,9 @@ ucv_cfunction_new(const char *name, uc_cfn_ptr_t fptr) bool ucv_cfunction_ex_helper( int cmd, void *args ) { + _Static_assert( offsetof(uc_cfunction_t,name) == offsetof(uc_cfunction_ex_t,magic), + "Problem with alignment of uc_cfunction_ex_t" ); + static const char *strmagic = #if INTPTR_MAX == INT64_MAX "\xFF" "FEEDBA"; // 8 bytes, including closing zero @@ -1181,9 +1184,9 @@ ucv_cfunction_ex_helper( int cmd, void *args ) namelen = strlen(name); cfn = xalloc(sizeof(*cfn) + ALIGN( namelen + 1 ) + (size_t)pargs[3] ); - cfn->header.header.type = UC_CFUNCTION; - cfn->header.header.refcount = 1; - cfn->header.cfn = pargs[ 1 ]; + cfn->header.type = UC_CFUNCTION; + cfn->header.refcount = 1; + cfn->cfn = pargs[ 1 ]; cfn->magic = *(intptr_t *)strmagic; cfn->feedback = pargs[ 2 ]; From 19b014b15476637b6c6113f089a9811377de64d3 Mon Sep 17 00:00:00 2001 From: Isaac de Wolff Date: Sat, 4 Jan 2025 10:21:12 +0100 Subject: [PATCH 09/13] Do not set a one bit integer bitfield to 1. Should be -1. --- lib/async/alien.c | 2 +- lib/async/manager.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/async/alien.c b/lib/async/alien.c index 9dc21824..b2163f12 100644 --- a/lib/async/alien.c +++ b/lib/async/alien.c @@ -50,7 +50,7 @@ async_alien_enter_futex( _Atomic uint32_t *futex_addr ) { while (1) { - const uint32_t zero = 0; + uint32_t zero = 0; if( atomic_compare_exchange_strong( futex_addr, &zero, 1 ) ) return; diff --git a/lib/async/manager.c b/lib/async/manager.c index 63660db4..45a38e9b 100644 --- a/lib/async/manager.c +++ b/lib/async/manager.c @@ -363,7 +363,7 @@ async_event_pump( struct uc_async_manager *_man, unsigned max_wait, int flags) { if (EXCEPTION_EXIT == ex) { - manager->silent = 1; + manager->silent = -1; return STATUS_EXIT; } return ERROR_RUNTIME; @@ -374,7 +374,7 @@ async_event_pump( struct uc_async_manager *_man, unsigned max_wait, int flags) { if (EXCEPTION_EXIT == ex) { - manager->silent = 1; + manager->silent = -1; return STATUS_EXIT; } return ERROR_RUNTIME; From 61a3ad10fdad7cff4e41fe221ea3e67f3689aa01 Mon Sep 17 00:00:00 2001 From: Isaac de Wolff Date: Sat, 4 Jan 2025 10:58:32 +0100 Subject: [PATCH 10/13] Disable 'alien calls' for MacOS, as it needs Linux futex. Add a warning suppressing pragma. --- lib/async/manager.h | 5 +++++ lib/async/promise.c | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/async/manager.h b/lib/async/manager.h index 40c8021e..ac0d12ee 100644 --- a/lib/async/manager.h +++ b/lib/async/manager.h @@ -28,6 +28,11 @@ This file is part of the async plugin for ucode #define ASYNC_HAS_UPTIME #define ASYNC_HAS_ALIENS +#ifdef __APPLE__ +// For now. Aliens needs Linux futex, which isn't available on MacOS. +# undef ASYNC_HAS_ALIENS +#endif + #ifdef DEBUG_PRINT # define DEBUG_PRINTF(...) printf(__VA_ARGS__) # define ASYNC_HAS_UPTIME diff --git a/lib/async/promise.c b/lib/async/promise.c index 218008a5..8741e85c 100644 --- a/lib/async/promise.c +++ b/lib/async/promise.c @@ -186,7 +186,10 @@ async_vm_raise_exception_caused_by( uc_vm_t *vm, uc_value_t *caused_by, int type if( vm->callframes.count ) { // No exceptions without framestack. It will crash +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" uc_vm_raise_exception(vm, type, err, arg ); +#pragma GCC diagnostic pop ret = true; } if( frame ) @@ -254,7 +257,7 @@ async_promise_destroy( async_manager_t *manager, async_todo_t *todo) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" -//#pragma GCC diagnostic ignored "-Wuse-after-free" + static const char *err = "Rejected promise %p without catch handler\n"; if (vm_is_active ) { From d2f02c6705dda4754ff81688867af80a29e9732a Mon Sep 17 00:00:00 2001 From: Isaac de Wolff Date: Sat, 4 Jan 2025 11:07:01 +0100 Subject: [PATCH 11/13] Do not include if Aliens as disabled. Remove unused inline function 'async_callback_queuer_cast()'. --- lib/async/alien.c | 6 +++--- lib/async/queuer.c | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/async/alien.c b/lib/async/alien.c index b2163f12..d67293a4 100644 --- a/lib/async/alien.c +++ b/lib/async/alien.c @@ -17,9 +17,6 @@ /* This file is part of the async plugin for ucode */ -#include -#include -#include #include #include #include @@ -36,6 +33,9 @@ This file is part of the async plugin for ucode #include "queuer.h" #ifdef ASYNC_HAS_ALIENS +#include +#include +#include static int async_futex( _Atomic uint32_t *uaddr, int futex_op, uint32_t val, diff --git a/lib/async/queuer.c b/lib/async/queuer.c index aeafc1a8..55eb79fe 100644 --- a/lib/async/queuer.c +++ b/lib/async/queuer.c @@ -139,11 +139,13 @@ struct async_callback_queuer_chain struct async_callback callback; }; +/* static inline async_callback_queuer_t * async_callback_queuer_cast(struct uc_async_callback_queuer *handler) { return (async_callback_queuer_t *)handler; } +*/ static inline async_callback_queuer_t const * async_callback_queuer_cast_const(struct uc_async_callback_queuer const *handler) From efa981785a6007e204d65062b4752b4a08220536 Mon Sep 17 00:00:00 2001 From: Isaac de Wolff Date: Sat, 4 Jan 2025 11:21:20 +0100 Subject: [PATCH 12/13] Do not omit names of unused function arguments --- lib/async/alien.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/async/alien.c b/lib/async/alien.c index d67293a4..96a42e3e 100644 --- a/lib/async/alien.c +++ b/lib/async/alien.c @@ -210,7 +210,7 @@ async_alien_free( async_manager_t *manager, struct async_alien *alien ) #else // defined ASYNC_HAS_ALIENS static const uc_async_alient_t * -_uc_async_new_alien( struct uc_async_manager * ) +_uc_async_new_alien( struct uc_async_manager *manager ) { return 0; } From a8398e87a4248c7713fa9c778d1f47b73365baed Mon Sep 17 00:00:00 2001 From: Isaac de Wolff Date: Sat, 4 Jan 2025 11:45:51 +0100 Subject: [PATCH 13/13] Exchange pthread_sigqueue() by pthread_kill(), as the former is not supported by MacOS. --- lib/async/queuer.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/async/queuer.c b/lib/async/queuer.c index 55eb79fe..7b40872e 100644 --- a/lib/async/queuer.c +++ b/lib/async/queuer.c @@ -65,10 +65,7 @@ async_callback_queuer_wakeup( const async_callback_queuer_t *queuer ) DEBUG_PRINTF( "%-1.3lf Wakeup script vm=%p\n", async_manager_uptime( queuer->manager ), queuer->manager->vm ); // send a signal to the script thread; - union sigval info = {0}; - info.sival_ptr = (void *)queuer->thread; - - pthread_sigqueue(queuer->thread, SIGNEWCALLBACK, info); + pthread_kill( queuer->thread, SIGNEWCALLBACK ); } void async_sleep( async_manager_t *manager, int64_t msec )