445 lines
17 KiB
C
445 lines
17 KiB
C
/*
|
|
* Finalizer handling.
|
|
*/
|
|
|
|
#include "duk_internal.h"
|
|
|
|
#if defined(DUK_USE_FINALIZER_SUPPORT)
|
|
|
|
/*
|
|
* Fake torture finalizer.
|
|
*/
|
|
|
|
#if defined(DUK_USE_FINALIZER_TORTURE)
|
|
DUK_LOCAL duk_ret_t duk__fake_global_finalizer(duk_hthread *thr) {
|
|
DUK_DD(DUK_DDPRINT("fake global torture finalizer executed"));
|
|
|
|
/* Require a lot of stack to force a value stack grow/shrink. */
|
|
duk_require_stack(thr, 100000);
|
|
|
|
/* Force a reallocation with pointer change for value stack
|
|
* to maximize side effects.
|
|
*/
|
|
duk_hthread_valstack_torture_realloc(thr);
|
|
|
|
/* Inner function call, error throw. */
|
|
duk_eval_string_noresult(thr,
|
|
"(function dummy() {\n"
|
|
" dummy.prototype = null; /* break reference loop */\n"
|
|
" try {\n"
|
|
" throw 'fake-finalizer-dummy-error';\n"
|
|
" } catch (e) {\n"
|
|
" void e;\n"
|
|
" }\n"
|
|
"})()");
|
|
|
|
/* The above creates garbage (e.g. a function instance). Because
|
|
* the function/prototype reference loop is broken, it gets collected
|
|
* immediately by DECREF. If Function.prototype has a _Finalizer
|
|
* property (happens in some test cases), the garbage gets queued to
|
|
* finalize_list. This still won't cause an infinite loop because
|
|
* the torture finalizer is called once per finalize_list run and
|
|
* the garbage gets handled in the same run. (If the garbage needs
|
|
* mark-and-sweep collection, an infinite loop might ensue.)
|
|
*/
|
|
return 0;
|
|
}
|
|
|
|
DUK_LOCAL void duk__run_global_torture_finalizer(duk_hthread *thr) {
|
|
DUK_ASSERT(thr != NULL);
|
|
|
|
/* Avoid fake finalization when callstack limit is near. Otherwise
|
|
* a callstack limit error will be created, then refzero'ed. The
|
|
* +5 headroom is conservative.
|
|
*/
|
|
if (thr->heap->call_recursion_depth + 5 >= thr->heap->call_recursion_limit ||
|
|
thr->callstack_top + 5 >= DUK_USE_CALLSTACK_LIMIT) {
|
|
DUK_D(DUK_DPRINT("skip global torture finalizer, too little headroom for call recursion or call stack size"));
|
|
return;
|
|
}
|
|
|
|
/* Run fake finalizer. Avoid creating unnecessary garbage. */
|
|
duk_push_c_function(thr, duk__fake_global_finalizer, 0 /*nargs*/);
|
|
(void) duk_pcall(thr, 0 /*nargs*/);
|
|
duk_pop(thr);
|
|
}
|
|
#endif /* DUK_USE_FINALIZER_TORTURE */
|
|
|
|
/*
|
|
* Process the finalize_list to completion.
|
|
*
|
|
* An object may be placed on finalize_list by either refcounting or
|
|
* mark-and-sweep. The refcount of objects placed by refcounting will be
|
|
* zero; the refcount of objects placed by mark-and-sweep is > 0. In both
|
|
* cases the refcount is bumped by 1 artificially so that a REFZERO event
|
|
* can never happen while an object is waiting for finalization. Without
|
|
* this bump a REFZERO could now happen because user code may call
|
|
* duk_push_heapptr() and then pop a value even when it's on finalize_list.
|
|
*
|
|
* List processing assumes refcounts are kept up-to-date at all times, so
|
|
* that once the finalizer returns, a zero refcount is a reliable reason to
|
|
* free the object immediately rather than place it back to the heap. This
|
|
* is the case because we run outside of refzero_list processing so that
|
|
* DECREF cascades are handled fully inline.
|
|
*
|
|
* For mark-and-sweep queued objects (had_zero_refcount false) the object
|
|
* may be freed immediately if its refcount is zero after the finalizer call
|
|
* (i.e. finalizer removed the reference loop for the object). If not, the
|
|
* next mark-and-sweep will collect the object unless it has become reachable
|
|
* (i.e. rescued) by that time and its refcount hasn't fallen to zero before
|
|
* that. Mark-and-sweep detects these objects because their FINALIZED flag
|
|
* is set.
|
|
*
|
|
* There's an inherent limitation for mark-and-sweep finalizer rescuing: an
|
|
* object won't get refinalized if (1) it's rescued, but (2) becomes
|
|
* unreachable before mark-and-sweep has had time to notice it. The next
|
|
* mark-and-sweep round simply doesn't have any information of whether the
|
|
* object has been unreachable the whole time or not (the only way to get
|
|
* that information would be a mark-and-sweep pass for *every finalized
|
|
* object*). This is awkward for the application because the mark-and-sweep
|
|
* round is not generally visible or under full application control.
|
|
*
|
|
* For refcount queued objects (had_zero_refcount true) the object is either
|
|
* immediately freed or rescued, and waiting for a mark-and-sweep round is not
|
|
* necessary (or desirable); FINALIZED is cleared when a rescued object is
|
|
* queued back to heap_allocated. The object is eligible for finalization
|
|
* again (either via refcounting or mark-and-sweep) immediately after being
|
|
* rescued. If a refcount finalized object is placed into an unreachable
|
|
* reference loop by its finalizer, it will get collected by mark-and-sweep
|
|
* and currently the finalizer will execute again.
|
|
*
|
|
* There's a special case where:
|
|
*
|
|
* - Mark-and-sweep queues an object to finalize_list for finalization.
|
|
* - The finalizer is executed, FINALIZED is set, and object is queued
|
|
* back to heap_allocated, waiting for a new mark-and-sweep round.
|
|
* - The object's refcount drops to zero before mark-and-sweep has a
|
|
* chance to run another round and make a rescue/free decision.
|
|
*
|
|
* This is now handled by refzero code: if an object has a finalizer but
|
|
* FINALIZED is already set, the object is freed without finalizer processing.
|
|
* The outcome is the same as if mark-and-sweep was executed at that point;
|
|
* mark-and-sweep would also free the object without another finalizer run.
|
|
* This could also be changed so that the refzero-triggered finalizer *IS*
|
|
* executed: being refzero collected implies someone has operated on the
|
|
* object so it hasn't been totally unreachable the whole time. This would
|
|
* risk a finalizer loop however.
|
|
*/
|
|
|
|
DUK_INTERNAL void duk_heap_process_finalize_list(duk_heap *heap) {
|
|
duk_heaphdr *curr;
|
|
#if defined(DUK_USE_DEBUG)
|
|
duk_size_t count = 0;
|
|
#endif
|
|
|
|
DUK_DDD(DUK_DDDPRINT("duk_heap_process_finalize_list: %p", (void *) heap));
|
|
|
|
if (heap->pf_prevent_count != 0) {
|
|
DUK_DDD(DUK_DDDPRINT("skip finalize_list processing: pf_prevent_count != 0"));
|
|
return;
|
|
}
|
|
|
|
/* Heap alloc prevents mark-and-sweep before heap_thread is ready. */
|
|
DUK_ASSERT(heap != NULL);
|
|
DUK_ASSERT(heap->heap_thread != NULL);
|
|
DUK_ASSERT(heap->heap_thread->valstack != NULL);
|
|
#if defined(DUK_USE_REFERENCE_COUNTING)
|
|
DUK_ASSERT(heap->refzero_list == NULL);
|
|
#endif
|
|
|
|
DUK_ASSERT(heap->pf_prevent_count == 0);
|
|
heap->pf_prevent_count = 1;
|
|
|
|
/* Mark-and-sweep no longer needs to be prevented when running
|
|
* finalizers: mark-and-sweep skips any rescue decisions if there
|
|
* are any objects in finalize_list when mark-and-sweep is entered.
|
|
* This protects finalized objects from incorrect rescue decisions
|
|
* caused by finalize_list being a reachability root and only
|
|
* partially processed. Freeing decisions are not postponed.
|
|
*/
|
|
|
|
/* When finalizer torture is enabled, make a fake finalizer call with
|
|
* maximum side effects regardless of whether finalize_list is empty.
|
|
*/
|
|
#if defined(DUK_USE_FINALIZER_TORTURE)
|
|
duk__run_global_torture_finalizer(heap->heap_thread);
|
|
#endif
|
|
|
|
/* Process finalize_list until it becomes empty. There's currently no
|
|
* protection against a finalizer always creating more garbage.
|
|
*/
|
|
while ((curr = heap->finalize_list) != NULL) {
|
|
#if defined(DUK_USE_REFERENCE_COUNTING)
|
|
duk_bool_t queue_back;
|
|
#endif
|
|
|
|
DUK_DD(DUK_DDPRINT("processing finalize_list entry: %p -> %!iO", (void *) curr, curr));
|
|
|
|
DUK_ASSERT(DUK_HEAPHDR_GET_TYPE(curr) == DUK_HTYPE_OBJECT); /* Only objects have finalizers. */
|
|
DUK_ASSERT(!DUK_HEAPHDR_HAS_REACHABLE(curr));
|
|
DUK_ASSERT(!DUK_HEAPHDR_HAS_TEMPROOT(curr));
|
|
DUK_ASSERT(DUK_HEAPHDR_HAS_FINALIZABLE(curr)); /* All objects on finalize_list will have this flag (except object being finalized right now). */
|
|
DUK_ASSERT(!DUK_HEAPHDR_HAS_FINALIZED(curr)); /* Queueing code ensures. */
|
|
DUK_ASSERT(!DUK_HEAPHDR_HAS_READONLY(curr)); /* ROM objects never get freed (or finalized). */
|
|
|
|
#if defined(DUK_USE_ASSERTIONS)
|
|
DUK_ASSERT(heap->currently_finalizing == NULL);
|
|
heap->currently_finalizing = curr;
|
|
#endif
|
|
|
|
/* Clear FINALIZABLE for object being finalized, so that
|
|
* duk_push_heapptr() can properly ignore the object.
|
|
*/
|
|
DUK_HEAPHDR_CLEAR_FINALIZABLE(curr);
|
|
|
|
if (DUK_LIKELY(!heap->pf_skip_finalizers)) {
|
|
/* Run the finalizer, duk_heap_run_finalizer() sets
|
|
* and checks for FINALIZED to prevent the finalizer
|
|
* from executing multiple times per finalization cycle.
|
|
* (This safeguard shouldn't be actually needed anymore).
|
|
*/
|
|
|
|
#if defined(DUK_USE_REFERENCE_COUNTING)
|
|
duk_bool_t had_zero_refcount;
|
|
#endif
|
|
|
|
/* The object's refcount is >0 throughout so it won't be
|
|
* refzero processed prematurely.
|
|
*/
|
|
#if defined(DUK_USE_REFERENCE_COUNTING)
|
|
DUK_ASSERT(DUK_HEAPHDR_GET_REFCOUNT(curr) >= 1);
|
|
had_zero_refcount = (DUK_HEAPHDR_GET_REFCOUNT(curr) == 1); /* Preincremented on finalize_list insert. */
|
|
#endif
|
|
|
|
DUK_ASSERT(!DUK_HEAPHDR_HAS_FINALIZED(curr));
|
|
duk_heap_run_finalizer(heap, (duk_hobject *) curr); /* must never longjmp */
|
|
DUK_ASSERT(DUK_HEAPHDR_HAS_FINALIZED(curr));
|
|
/* XXX: assert that object is still in finalize_list
|
|
* when duk_push_heapptr() allows automatic rescue.
|
|
*/
|
|
|
|
#if defined(DUK_USE_REFERENCE_COUNTING)
|
|
DUK_DD(DUK_DDPRINT("refcount after finalizer (includes bump): %ld", (long) DUK_HEAPHDR_GET_REFCOUNT(curr)));
|
|
if (DUK_HEAPHDR_GET_REFCOUNT(curr) == 1) { /* Only artificial bump in refcount? */
|
|
#if defined(DUK_USE_DEBUG)
|
|
if (had_zero_refcount) {
|
|
DUK_DD(DUK_DDPRINT("finalized object's refcount is zero -> free immediately (refcount queued)"));
|
|
} else {
|
|
DUK_DD(DUK_DDPRINT("finalized object's refcount is zero -> free immediately (mark-and-sweep queued)"));
|
|
}
|
|
#endif
|
|
queue_back = 0;
|
|
} else
|
|
#endif
|
|
{
|
|
#if defined(DUK_USE_REFERENCE_COUNTING)
|
|
queue_back = 1;
|
|
if (had_zero_refcount) {
|
|
/* When finalization is triggered
|
|
* by refzero and we queue the object
|
|
* back, clear FINALIZED right away
|
|
* so that the object can be refinalized
|
|
* immediately if necessary.
|
|
*/
|
|
DUK_HEAPHDR_CLEAR_FINALIZED(curr);
|
|
}
|
|
#endif
|
|
}
|
|
} else {
|
|
/* Used during heap destruction: don't actually run finalizers
|
|
* because we're heading into forced finalization. Instead,
|
|
* queue finalizable objects back to the heap_allocated list.
|
|
*/
|
|
DUK_D(DUK_DPRINT("skip finalizers flag set, queue object to heap_allocated without finalizing"));
|
|
DUK_ASSERT(!DUK_HEAPHDR_HAS_FINALIZED(curr));
|
|
#if defined(DUK_USE_REFERENCE_COUNTING)
|
|
queue_back = 1;
|
|
#endif
|
|
}
|
|
|
|
/* Dequeue object from finalize_list. Note that 'curr' may no
|
|
* longer be finalize_list head because new objects may have
|
|
* been queued to the list. As a result we can't optimize for
|
|
* the single-linked heap case and must scan the list for
|
|
* removal, typically the scan is very short however.
|
|
*/
|
|
DUK_HEAP_REMOVE_FROM_FINALIZE_LIST(heap, curr);
|
|
|
|
/* Queue back to heap_allocated or free immediately. */
|
|
#if defined(DUK_USE_REFERENCE_COUNTING)
|
|
if (queue_back) {
|
|
/* FINALIZED is only cleared if object originally
|
|
* queued for finalization by refcounting. For
|
|
* mark-and-sweep FINALIZED is left set, so that
|
|
* next mark-and-sweep round can make a rescue/free
|
|
* decision.
|
|
*/
|
|
DUK_ASSERT(DUK_HEAPHDR_GET_REFCOUNT(curr) >= 1);
|
|
DUK_HEAPHDR_PREDEC_REFCOUNT(curr); /* Remove artificial refcount bump. */
|
|
DUK_HEAPHDR_CLEAR_FINALIZABLE(curr);
|
|
DUK_HEAP_INSERT_INTO_HEAP_ALLOCATED(heap, curr);
|
|
} else {
|
|
/* No need to remove the refcount bump here. */
|
|
DUK_ASSERT(DUK_HEAPHDR_GET_TYPE(curr) == DUK_HTYPE_OBJECT); /* currently, always the case */
|
|
DUK_DD(DUK_DDPRINT("refcount finalize after finalizer call: %!O", curr));
|
|
duk_hobject_refcount_finalize_norz(heap, (duk_hobject *) curr);
|
|
duk_free_hobject(heap, (duk_hobject *) curr);
|
|
DUK_DD(DUK_DDPRINT("freed hobject after finalization: %p", (void *) curr));
|
|
}
|
|
#else /* DUK_USE_REFERENCE_COUNTING */
|
|
DUK_HEAPHDR_CLEAR_FINALIZABLE(curr);
|
|
DUK_HEAP_INSERT_INTO_HEAP_ALLOCATED(heap, curr);
|
|
#endif /* DUK_USE_REFERENCE_COUNTING */
|
|
|
|
#if defined(DUK_USE_DEBUG)
|
|
count++;
|
|
#endif
|
|
|
|
#if defined(DUK_USE_ASSERTIONS)
|
|
DUK_ASSERT(heap->currently_finalizing != NULL);
|
|
heap->currently_finalizing = NULL;
|
|
#endif
|
|
}
|
|
|
|
/* finalize_list will always be processed completely. */
|
|
DUK_ASSERT(heap->finalize_list == NULL);
|
|
|
|
#if 0
|
|
/* While NORZ macros are used above, this is unnecessary because the
|
|
* only pending side effects are now finalizers, and finalize_list is
|
|
* empty.
|
|
*/
|
|
DUK_REFZERO_CHECK_SLOW(heap->heap_thread);
|
|
#endif
|
|
|
|
/* Prevent count may be bumped while finalizers run, but should always
|
|
* be reliably unbumped by the time we get here.
|
|
*/
|
|
DUK_ASSERT(heap->pf_prevent_count == 1);
|
|
heap->pf_prevent_count = 0;
|
|
|
|
#if defined(DUK_USE_DEBUG)
|
|
DUK_DD(DUK_DDPRINT("duk_heap_process_finalize_list: %ld finalizers called", (long) count));
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
* Run an duk_hobject finalizer. Must never throw an uncaught error
|
|
* (but may throw caught errors).
|
|
*
|
|
* There is no return value. Any return value or error thrown by
|
|
* the finalizer is ignored (although errors are debug logged).
|
|
*
|
|
* Notes:
|
|
*
|
|
* - The finalizer thread 'top' assertions are there because it is
|
|
* critical that strict stack policy is observed (i.e. no cruft
|
|
* left on the finalizer stack).
|
|
*/
|
|
|
|
DUK_LOCAL duk_ret_t duk__finalize_helper(duk_hthread *thr, void *udata) {
|
|
DUK_ASSERT(thr != NULL);
|
|
DUK_UNREF(udata);
|
|
|
|
DUK_DDD(DUK_DDDPRINT("protected finalization helper running"));
|
|
|
|
/* [... obj] */
|
|
|
|
/* _Finalizer property is read without checking if the value is
|
|
* callable or even exists. This is intentional, and handled
|
|
* by throwing an error which is caught by the safe call wrapper.
|
|
*
|
|
* XXX: Finalizer lookup should traverse the prototype chain (to allow
|
|
* inherited finalizers) but should not invoke accessors or proxy object
|
|
* behavior. At the moment this lookup will invoke proxy behavior, so
|
|
* caller must ensure that this function is not called if the target is
|
|
* a Proxy.
|
|
*/
|
|
duk_get_prop_stridx_short(thr, -1, DUK_STRIDX_INT_FINALIZER); /* -> [... obj finalizer] */
|
|
duk_dup_m2(thr);
|
|
duk_push_boolean(thr, DUK_HEAP_HAS_FINALIZER_NORESCUE(thr->heap));
|
|
DUK_DDD(DUK_DDDPRINT("calling finalizer"));
|
|
duk_call(thr, 2); /* [ ... obj finalizer obj heapDestruct ] -> [ ... obj retval ] */
|
|
DUK_DDD(DUK_DDDPRINT("finalizer returned successfully"));
|
|
return 0;
|
|
|
|
/* Note: we rely on duk_safe_call() to fix up the stack for the caller,
|
|
* so we don't need to pop stuff here. There is no return value;
|
|
* caller determines rescued status based on object refcount.
|
|
*/
|
|
}
|
|
|
|
DUK_INTERNAL void duk_heap_run_finalizer(duk_heap *heap, duk_hobject *obj) {
|
|
duk_hthread *thr;
|
|
duk_ret_t rc;
|
|
#if defined(DUK_USE_ASSERTIONS)
|
|
duk_idx_t entry_top;
|
|
#endif
|
|
|
|
DUK_DD(DUK_DDPRINT("running duk_hobject finalizer for object: %p", (void *) obj));
|
|
|
|
DUK_ASSERT(heap != NULL);
|
|
DUK_ASSERT(heap->heap_thread != NULL);
|
|
thr = heap->heap_thread;
|
|
DUK_ASSERT(obj != NULL);
|
|
DUK_ASSERT_VALSTACK_SPACE(heap->heap_thread, 1);
|
|
|
|
#if defined(DUK_USE_ASSERTIONS)
|
|
entry_top = duk_get_top(thr);
|
|
#endif
|
|
/*
|
|
* Get and call the finalizer. All of this must be wrapped
|
|
* in a protected call, because even getting the finalizer
|
|
* may trigger an error (getter may throw one, for instance).
|
|
*/
|
|
|
|
/* ROM objects could inherit a finalizer, but they are never deemed
|
|
* unreachable by mark-and-sweep, and their refcount never falls to 0.
|
|
*/
|
|
DUK_ASSERT(!DUK_HEAPHDR_HAS_READONLY((duk_heaphdr *) obj));
|
|
|
|
/* Duktape 2.1: finalize_list never contains objects with FINALIZED
|
|
* set, so no need to check here.
|
|
*/
|
|
DUK_ASSERT(!DUK_HEAPHDR_HAS_FINALIZED((duk_heaphdr *) obj));
|
|
#if 0
|
|
if (DUK_HEAPHDR_HAS_FINALIZED((duk_heaphdr *) obj)) {
|
|
DUK_D(DUK_DPRINT("object already finalized, avoid running finalizer twice: %!O", obj));
|
|
return;
|
|
}
|
|
#endif
|
|
DUK_HEAPHDR_SET_FINALIZED((duk_heaphdr *) obj); /* ensure never re-entered until rescue cycle complete */
|
|
|
|
#if defined(DUK_USE_ES6_PROXY)
|
|
if (DUK_HOBJECT_IS_PROXY(obj)) {
|
|
/* This may happen if duk_set_finalizer() or Duktape.fin() is
|
|
* called for a Proxy object. In such cases the fast finalizer
|
|
* flag will be set on the Proxy, not the target, and neither
|
|
* will be finalized.
|
|
*/
|
|
DUK_D(DUK_DPRINT("object is a Proxy, skip finalizer call"));
|
|
return;
|
|
}
|
|
#endif /* DUK_USE_ES6_PROXY */
|
|
|
|
duk_push_hobject(thr, obj); /* this also increases refcount by one */
|
|
rc = duk_safe_call(thr, duk__finalize_helper, NULL /*udata*/, 0 /*nargs*/, 1 /*nrets*/); /* -> [... obj retval/error] */
|
|
DUK_ASSERT_TOP(thr, entry_top + 2); /* duk_safe_call discipline */
|
|
|
|
if (rc != DUK_EXEC_SUCCESS) {
|
|
/* Note: we ask for one return value from duk_safe_call to get this
|
|
* error debugging here.
|
|
*/
|
|
DUK_D(DUK_DPRINT("wrapped finalizer call failed for object %p (ignored); error: %!T",
|
|
(void *) obj, (duk_tval *) duk_get_tval(thr, -1)));
|
|
}
|
|
duk_pop_2(thr); /* -> [...] */
|
|
|
|
DUK_ASSERT_TOP(thr, entry_top);
|
|
}
|
|
|
|
#else /* DUK_USE_FINALIZER_SUPPORT */
|
|
|
|
/* nothing */
|
|
|
|
#endif /* DUK_USE_FINALIZER_SUPPORT */
|