From 330b985fc94223afe7f7ffe0ccb9bbea5e9b9a07 Mon Sep 17 00:00:00 2001 From: undy <200824315+undy-aeon@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:08:25 -0400 Subject: [PATCH 1/3] Add an API to discard pending jobs --- api-test.c | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ quickjs.c | 17 +++++++++++++++ quickjs.h | 8 +++++++ 3 files changed, 86 insertions(+) diff --git a/api-test.c b/api-test.c index 8a3ce9eec..e52bf41ee 100644 --- a/api-test.c +++ b/api-test.c @@ -2195,8 +2195,69 @@ void private_symbols(void) JS_FreeRuntime(rt); } +static int discarded_job_calls; +static int discarded_job_finalizers; + +static JSValue discard_job_callback(JSContext *ctx, int argc, JSValueConst *argv) +{ + discarded_job_calls++; + return JS_UNDEFINED; +} + +static void discard_job_finalizer(JSRuntime *rt, JSValueConst val) +{ + discarded_job_finalizers++; +} + +static void discard_pending_jobs(void) +{ + JSRuntime *rt = new_runtime(); + JSContext *ctx = JS_NewContext(rt); + JSContext *job_ctx = NULL; + JSClassID class_id = 0; + JSClassDef def = { "DiscardJobArgument", .finalizer = discard_job_finalizer }; + JSValueConst args[2]; + JSMemoryUsage before, after; + int i; + + assert(rt && ctx); + assert(JS_DiscardPendingJobs(rt) == 0); + JS_NewClassID(rt, &class_id); + assert(JS_NewClass(rt, class_id, &def) == 0); + JS_ComputeMemoryUsage(rt, &before); + for (i = 0; i < 2; i++) { + args[0] = JS_NewObjectClass(ctx, class_id); + args[1] = JS_NewString(ctx, "retained job argument"); + assert(!JS_IsException(args[0])); + assert(!JS_IsException(args[1])); + assert(JS_EnqueueJob(ctx, discard_job_callback, 2, args) == 0); + JS_FreeValue(ctx, (JSValue)args[0]); + JS_FreeValue(ctx, (JSValue)args[1]); + } + assert(discarded_job_finalizers == 0); + assert(JS_IsJobPending(rt)); + assert(JS_DiscardPendingJobs(rt) == 2); + assert(!JS_IsJobPending(rt)); + assert(discarded_job_calls == 0); + assert(discarded_job_finalizers == 2); + assert(JS_DiscardPendingJobs(rt) == 0); + JS_ComputeMemoryUsage(rt, &after); + assert(after.malloc_count == before.malloc_count); + assert(after.memory_used_size == before.memory_used_size); + + assert(JS_EnqueueJob(ctx, discard_job_callback, 0, NULL) == 0); + assert(JS_ExecutePendingJob(rt, &job_ctx) == 1); + assert(job_ctx == ctx); + assert(discarded_job_calls == 1); + assert(JS_ExecutePendingJob(rt, &job_ctx) == 0); + assert(JS_DiscardPendingJobs(rt) == 0); + JS_FreeContext(ctx); + JS_FreeRuntime(rt); +} + int main(void) { + discard_pending_jobs(); cfunctions(); sync_call(); async_call(); diff --git a/quickjs.c b/quickjs.c index 6ca030b9f..663c084be 100644 --- a/quickjs.c +++ b/quickjs.c @@ -2508,6 +2508,23 @@ int JS_EnqueueJob(JSContext *ctx, JSJobFunc *job_func, return 0; } +size_t JS_DiscardPendingJobs(JSRuntime *rt) +{ + size_t count = 0; + JSJobEntry *e; + int i; + + while (!list_empty(&rt->job_list)) { + e = list_entry(rt->job_list.next, JSJobEntry, link); + list_del(&e->link); + for (i = 0; i < e->argc; i++) + JS_FreeValueRT(rt, e->argv[i]); + js_free_rt(rt, e); + count++; + } + return count; +} + bool JS_IsJobPending(JSRuntime *rt) { return !list_empty(&rt->job_list); diff --git a/quickjs.h b/quickjs.h index b5e5576ff..105b5eb75 100644 --- a/quickjs.h +++ b/quickjs.h @@ -1247,6 +1247,14 @@ typedef JSValue JSJobFunc(JSContext *ctx, int argc, JSValueConst *argv); JS_EXTERN int JS_EnqueueJob(JSContext *ctx, JSJobFunc *job_func, int argc, JSValueConst *argv); +/* Discard all queued jobs without executing their callbacks. Releases the + queue's argument references and entries, but does not free contexts or the + runtime. Returns the number of discarded jobs; the runtime remains usable. + Call only while no JavaScript or job is executing and with exclusive access + to the runtime. Releasing arguments may invoke native finalizers; these must + not execute JavaScript, enqueue jobs, or reenter this function. This does not + settle promises whose reactions are discarded. */ +JS_EXTERN size_t JS_DiscardPendingJobs(JSRuntime *rt); JS_EXTERN bool JS_IsJobPending(JSRuntime *rt); JS_EXTERN JSContext *JS_GetPendingJobContext(JSRuntime *rt); JS_EXTERN int JS_ExecutePendingJob(JSRuntime *rt, JSContext **pctx); From 84a8a02c2377225157dd62b2a7b1ca011ef75dfc Mon Sep 17 00:00:00 2001 From: undy <200824315+undy-aeon@users.noreply.github.com> Date: Thu, 24 Sep 2026 09:15:50 -0400 Subject: [PATCH 2/3] Detach pending jobs before releasing arguments --- api-test.c | 31 +++++++++++++++++++++++++++++++ list.h | 22 ++++++++++++++++++++++ quickjs.c | 7 +++++-- quickjs.h | 7 ++++--- 4 files changed, 62 insertions(+), 5 deletions(-) diff --git a/api-test.c b/api-test.c index e52bf41ee..a10275847 100644 --- a/api-test.c +++ b/api-test.c @@ -2206,7 +2206,11 @@ static JSValue discard_job_callback(JSContext *ctx, int argc, JSValueConst *argv static void discard_job_finalizer(JSRuntime *rt, JSValueConst val) { + JSContext *ctx = JS_GetRuntimeOpaque(rt); + discarded_job_finalizers++; + if (ctx) + assert(JS_EnqueueJob(ctx, discard_job_callback, 0, NULL) == 0); } static void discard_pending_jobs(void) @@ -2251,6 +2255,33 @@ static void discard_pending_jobs(void) assert(discarded_job_calls == 1); assert(JS_ExecutePendingJob(rt, &job_ctx) == 0); assert(JS_DiscardPendingJobs(rt) == 0); + + /* Finalizers may enqueue new jobs, but discarding must only consume the + jobs present at entry. Exercise both singleton and multi-entry queues. */ + JS_SetRuntimeOpaque(rt, ctx); + for (i = 1; i <= 2; i++) { + int j, calls = discarded_job_calls; + int finalizers = discarded_job_finalizers; + + for (j = 0; j < i; j++) { + args[0] = JS_NewObjectClass(ctx, class_id); + assert(!JS_IsException(args[0])); + assert(JS_EnqueueJob(ctx, discard_job_callback, 1, args) == 0); + JS_FreeValue(ctx, (JSValue)args[0]); + } + assert(JS_DiscardPendingJobs(rt) == (size_t)i); + assert(discarded_job_finalizers == finalizers + i); + assert(discarded_job_calls == calls); + assert(JS_IsJobPending(rt)); + for (j = 0; j < i; j++) { + assert(JS_ExecutePendingJob(rt, &job_ctx) == 1); + assert(job_ctx == ctx); + } + assert(discarded_job_calls == calls + i); + assert(JS_ExecutePendingJob(rt, &job_ctx) == 0); + assert(JS_DiscardPendingJobs(rt) == 0); + } + JS_SetRuntimeOpaque(rt, NULL); JS_FreeContext(ctx); JS_FreeRuntime(rt); } diff --git a/list.h b/list.h index b8dd71681..dc4f7813d 100644 --- a/list.h +++ b/list.h @@ -86,6 +86,28 @@ static inline int list_empty(struct list_head *el) return el->next == el; } +/* move elements from 'el' through the tail of 'src' into 'dst' */ +static inline void list_split(struct list_head *src, struct list_head *el, + struct list_head *dst) +{ + dst->prev = src->prev; + dst->prev->next = dst; + dst->next = el; + src->prev = el->prev; + src->prev->next = src; + el->prev = dst; +} + +/* move all elements from 'src' into 'dst', leaving 'src' empty */ +static inline void list_move(struct list_head *src, struct list_head *dst) +{ + if (list_empty(src)) { + init_list_head(dst); + } else { + list_split(src, src->next, dst); + } +} + #define list_for_each(el, head) \ for(el = (head)->next; el != (head); el = el->next) diff --git a/quickjs.c b/quickjs.c index 663c084be..90ed6f2a8 100644 --- a/quickjs.c +++ b/quickjs.c @@ -2511,11 +2511,14 @@ int JS_EnqueueJob(JSContext *ctx, JSJobFunc *job_func, size_t JS_DiscardPendingJobs(JSRuntime *rt) { size_t count = 0; + struct list_head jobs; JSJobEntry *e; int i; - while (!list_empty(&rt->job_list)) { - e = list_entry(rt->job_list.next, JSJobEntry, link); + /* Finalizers may enqueue jobs while arguments are released. */ + list_move(&rt->job_list, &jobs); + while (!list_empty(&jobs)) { + e = list_entry(jobs.next, JSJobEntry, link); list_del(&e->link); for (i = 0; i < e->argc; i++) JS_FreeValueRT(rt, e->argv[i]); diff --git a/quickjs.h b/quickjs.h index 105b5eb75..5a30ca3fb 100644 --- a/quickjs.h +++ b/quickjs.h @@ -1247,12 +1247,13 @@ typedef JSValue JSJobFunc(JSContext *ctx, int argc, JSValueConst *argv); JS_EXTERN int JS_EnqueueJob(JSContext *ctx, JSJobFunc *job_func, int argc, JSValueConst *argv); -/* Discard all queued jobs without executing their callbacks. Releases the - queue's argument references and entries, but does not free contexts or the +/* Discard the jobs queued at entry without executing their callbacks. Releases + their argument references and entries, but does not free contexts or the runtime. Returns the number of discarded jobs; the runtime remains usable. Call only while no JavaScript or job is executing and with exclusive access to the runtime. Releasing arguments may invoke native finalizers; these must - not execute JavaScript, enqueue jobs, or reenter this function. This does not + not execute JavaScript or reenter this function. Jobs enqueued by finalizers + remain pending and are not included in the returned count. This does not settle promises whose reactions are discarded. */ JS_EXTERN size_t JS_DiscardPendingJobs(JSRuntime *rt); JS_EXTERN bool JS_IsJobPending(JSRuntime *rt); From 24f0d4f85a0e07bdd308e4bd4c3d379a9316364b Mon Sep 17 00:00:00 2001 From: undy <200824315+undy-aeon@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:38:04 -0400 Subject: [PATCH 3/3] Fix MSVC compilation of pending-job tests Keep owned job arguments as JSValue and borrow them when enqueuing. This avoids struct-to-struct casts rejected by MSVC while preserving JS_CHECK_JSVALUE compatibility. --- api-test.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api-test.c b/api-test.c index a10275847..4bcd0db5c 100644 --- a/api-test.c +++ b/api-test.c @@ -2220,7 +2220,7 @@ static void discard_pending_jobs(void) JSContext *job_ctx = NULL; JSClassID class_id = 0; JSClassDef def = { "DiscardJobArgument", .finalizer = discard_job_finalizer }; - JSValueConst args[2]; + JSValue args[2]; JSMemoryUsage before, after; int i; @@ -2234,9 +2234,9 @@ static void discard_pending_jobs(void) args[1] = JS_NewString(ctx, "retained job argument"); assert(!JS_IsException(args[0])); assert(!JS_IsException(args[1])); - assert(JS_EnqueueJob(ctx, discard_job_callback, 2, args) == 0); - JS_FreeValue(ctx, (JSValue)args[0]); - JS_FreeValue(ctx, (JSValue)args[1]); + assert(JS_EnqueueJob(ctx, discard_job_callback, 2, (JSValueConst *)args) == 0); + JS_FreeValue(ctx, args[0]); + JS_FreeValue(ctx, args[1]); } assert(discarded_job_finalizers == 0); assert(JS_IsJobPending(rt)); @@ -2266,8 +2266,8 @@ static void discard_pending_jobs(void) for (j = 0; j < i; j++) { args[0] = JS_NewObjectClass(ctx, class_id); assert(!JS_IsException(args[0])); - assert(JS_EnqueueJob(ctx, discard_job_callback, 1, args) == 0); - JS_FreeValue(ctx, (JSValue)args[0]); + assert(JS_EnqueueJob(ctx, discard_job_callback, 1, (JSValueConst *)args) == 0); + JS_FreeValue(ctx, args[0]); } assert(JS_DiscardPendingJobs(rt) == (size_t)i); assert(discarded_job_finalizers == finalizers + i);