diff --git a/api-test.c b/api-test.c index 85aa27e4c..d3788ab1a 100644 --- a/api-test.c +++ b/api-test.c @@ -473,6 +473,26 @@ static void promise_mark_as_handled(void) JS_FreeRuntime(rt); } +static void clear_kept_objects(void) +{ + JSRuntime *rt = new_runtime(); + JSContext *ctx = JS_NewContext(rt); + + JSValue w = eval(ctx, "let o = {x:1}; const w = new WeakRef(o); o = null; w"); + assert(JS_IsObject(w)); + JSValue got = eval(ctx, "w.deref()"); + assert(JS_IsObject(got)); + JS_FreeValue(ctx, got); + + JS_ClearKeptObjects(rt); + got = eval(ctx, "w.deref()"); + assert(JS_IsUndefined(got)); + + JS_FreeValue(ctx, w); + JS_FreeContext(ctx); + JS_FreeRuntime(rt); +} + static void runtime_cstring_free(void) { JSRuntime *rt = new_runtime(); @@ -1294,6 +1314,7 @@ int main(void) module_serde(); module_unhandled_rejection(); promise_mark_as_handled(); + clear_kept_objects(); runtime_cstring_free(); utf16_string(); weak_map_gc_check(); diff --git a/quickjs.c b/quickjs.c index d51b96867..64657feee 100644 --- a/quickjs.c +++ b/quickjs.c @@ -378,6 +378,9 @@ struct JSRuntime { // to js_promise_constructor JSValueLink *parent_promise; + // AddToKeptObjects list, dropped at job boundaries + JSValueLink *kept_objects; + JSHostPromiseRejectionTracker *host_promise_rejection_tracker; void *host_promise_rejection_tracker_opaque; @@ -2527,6 +2530,10 @@ int JS_ExecutePendingJob(JSRuntime *rt, JSContext **pctx) JSValue res; int i, ret; + /* job boundary: the host calling us means the previous script or job + finished */ + JS_ClearKeptObjects(rt); + if (list_empty(&rt->job_list)) { *pctx = NULL; return 0; @@ -2641,6 +2648,7 @@ void JS_FreeRuntime(JSRuntime *rt) rt->in_free = true; JS_FreeValueRT(rt, rt->current_exception); + JS_ClearKeptObjects(rt); list_for_each_safe(el, el1, &rt->job_list) { JSJobEntry *e = list_entry(el, JSJobEntry, link); @@ -62499,6 +62507,29 @@ typedef struct JSWeakRefData { static JSWeakRefData js_weakref_sentinel; +static int js_add_to_kept_objects(JSContext *ctx, JSValueConst value) +{ + JSValueLink *link = js_malloc(ctx, sizeof(*link)); + if (!link) + return -1; + link->value = js_dup(value); + link->next = ctx->rt->kept_objects; + ctx->rt->kept_objects = link; + return 0; +} + +void JS_ClearKeptObjects(JSRuntime *rt) +{ + JSValueLink *link = rt->kept_objects; + rt->kept_objects = NULL; + while (link) { + JSValueLink *next = link->next; + JS_FreeValueRT(rt, unsafe_unconst(link->value)); + js_free_rt(rt, link); + link = next; + } +} + static void js_weakref_finalizer(JSRuntime *rt, JSValueConst val) { JSWeakRefData *wrd = JS_GetOpaque(val, JS_CLASS_WEAK_REF); @@ -62551,6 +62582,10 @@ static JSValue js_weakref_constructor(JSContext *ctx, JSValueConst new_target, insert_weakref_record(arg, wr); JS_SetOpaqueInternal(obj, wrd); + if (js_add_to_kept_objects(ctx, arg)) { + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } return obj; } @@ -62561,6 +62596,8 @@ static JSValue js_weakref_deref(JSContext *ctx, JSValueConst this_val, int argc, return JS_EXCEPTION; if (wrd == &js_weakref_sentinel) return JS_UNDEFINED; + if (js_add_to_kept_objects(ctx, wrd->target)) + return JS_EXCEPTION; return js_dup(wrd->target); } diff --git a/quickjs.h b/quickjs.h index e950144c6..2c712b00f 100644 --- a/quickjs.h +++ b/quickjs.h @@ -557,6 +557,7 @@ JS_EXTERN int JS_AddIntrinsicTypedArrays(JSContext *ctx); JS_EXTERN int JS_AddIntrinsicPromise(JSContext *ctx); JS_EXTERN int JS_AddIntrinsicBigInt(JSContext *ctx); JS_EXTERN int JS_AddIntrinsicWeakRef(JSContext *ctx); +JS_EXTERN void JS_ClearKeptObjects(JSRuntime *rt); JS_EXTERN int JS_AddPerformance(JSContext *ctx); JS_EXTERN int JS_AddIntrinsicDOMException(JSContext *ctx); JS_EXTERN int JS_AddIntrinsicAToB(JSContext *ctx); diff --git a/tests/bug652.js b/tests/bug652.js index 27dea3bdd..00efb63bd 100644 --- a/tests/bug652.js +++ b/tests/bug652.js @@ -1,4 +1,4 @@ import { assert } from "./assert.js" const ref = new WeakRef({}) const val = ref.deref() // should not throw -assert(val, undefined) +assert(typeof val, "object") // kept alive until the end of the job diff --git a/tests/weakref-kept-objects.js b/tests/weakref-kept-objects.js new file mode 100644 index 000000000..bb83a4d75 --- /dev/null +++ b/tests/weakref-kept-objects.js @@ -0,0 +1,20 @@ +import { assert } from "./assert.js"; + +/* the constructor keeps the target alive until the end of the job */ +{ + let o = { x: 1 }; + const w = new WeakRef(o); + o = null; + assert(typeof w.deref(), "object"); +} + +/* deref() keeps it too, and the kept set is cleared at job boundaries */ +{ + let o = { x: 2 }; + const w = new WeakRef(o); + assert(w.deref().x, 2); + o = null; + assert(typeof w.deref(), "object"); + await Promise.resolve(); + assert(w.deref(), undefined); +}