Skip to content

Commit 689879a

Browse files
committed
node-api: generalize finalizer second pass callback
Generalize the finalizer's second pass callback to make it cancellable and simplify the code around the second pass callback. With this change, it is determined that Reference::Finalize or RefBase::Finalize are called once, either from the env's shutdown, or from the env's second pass callback. All existing node-api js tests should pass without a touch. The js_native_api cctest is no longer applicable with this change, just removing it.
1 parent 1124558 commit 689879a

6 files changed

+186
-394
lines changed

node.gyp

-1
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,6 @@
998998
'test/cctest/test_base_object_ptr.cc',
999999
'test/cctest/test_node_postmortem_metadata.cc',
10001000
'test/cctest/test_environment.cc',
1001-
'test/cctest/test_js_native_api_v8.cc',
10021001
'test/cctest/test_linked_binding.cc',
10031002
'test/cctest/test_node_api.cc',
10041003
'test/cctest/test_per_process.cc',

src/js_native_api_v8.cc

+75-168
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ inline napi_status Unwrap(napi_env env,
218218
if (action == RemoveWrap) {
219219
CHECK(obj->DeletePrivate(context, NAPI_PRIVATE_KEY(context, wrapper))
220220
.FromJust());
221-
Reference::Delete(reference);
221+
delete reference;
222222
}
223223

224224
return GET_RETURN_STATUS(env);
@@ -464,11 +464,20 @@ RefBase::RefBase(napi_env env,
464464
void* finalize_data,
465465
void* finalize_hint)
466466
: Finalizer(env, finalize_callback, finalize_data, finalize_hint),
467-
_refcount(initial_refcount),
468-
_delete_self(delete_self) {
467+
refcount_(initial_refcount),
468+
delete_self_(delete_self) {
469469
Link(finalize_callback == nullptr ? &env->reflist : &env->finalizing_reflist);
470470
}
471471

472+
// When a RefBase is being deleted, it may have been queued to call its
473+
// finalizer.
474+
RefBase::~RefBase() {
475+
// Remove from the env's tracked list.
476+
Unlink();
477+
// Try to remove the finalizer from the scheduled second pass callback.
478+
env_->DequeueFinalizer(this);
479+
}
480+
472481
RefBase* RefBase::New(napi_env env,
473482
uint32_t initial_refcount,
474483
bool delete_self,
@@ -483,105 +492,65 @@ RefBase* RefBase::New(napi_env env,
483492
finalize_hint);
484493
}
485494

486-
RefBase::~RefBase() {
487-
Unlink();
488-
}
489-
490495
void* RefBase::Data() {
491-
return _finalize_data;
492-
}
493-
494-
// Delete is called in 2 ways. Either from the finalizer or
495-
// from one of Unwrap or napi_delete_reference.
496-
//
497-
// When it is called from Unwrap or napi_delete_reference we only
498-
// want to do the delete if the finalizer has already run or
499-
// cannot have been queued to run (ie the reference count is > 0),
500-
// otherwise we may crash when the finalizer does run.
501-
// If the finalizer may have been queued and has not already run
502-
// delay the delete until the finalizer runs by not doing the delete
503-
// and setting _delete_self to true so that the finalizer will
504-
// delete it when it runs.
505-
//
506-
// The second way this is called is from
507-
// the finalizer and _delete_self is set. In this case we
508-
// know we need to do the deletion so just do it.
509-
void RefBase::Delete(RefBase* reference) {
510-
if ((reference->RefCount() != 0) || (reference->_delete_self) ||
511-
(reference->_finalize_ran)) {
512-
delete reference;
513-
} else {
514-
// defer until finalizer runs as
515-
// it may already be queued
516-
reference->_delete_self = true;
517-
}
496+
return finalize_data_;
518497
}
519498

520499
uint32_t RefBase::Ref() {
521-
return ++_refcount;
500+
return ++refcount_;
522501
}
523502

524503
uint32_t RefBase::Unref() {
525-
if (_refcount == 0) {
504+
if (refcount_ == 0) {
526505
return 0;
527506
}
528-
return --_refcount;
507+
return --refcount_;
529508
}
530509

531510
uint32_t RefBase::RefCount() {
532-
return _refcount;
533-
}
534-
535-
void RefBase::Finalize(bool is_env_teardown) {
536-
// In addition to being called during environment teardown, this method is
537-
// also the entry point for the garbage collector. During environment
538-
// teardown we have to remove the garbage collector's reference to this
539-
// method so that, if, as part of the user's callback, JS gets executed,
540-
// resulting in a garbage collection pass, this method is not re-entered as
541-
// part of that pass, because that'll cause a double free (as seen in
542-
// https://github.com/nodejs/node/issues/37236).
543-
//
544-
// Since this class does not have access to the V8 persistent reference,
545-
// this method is overridden in the `Reference` class below. Therein the
546-
// weak callback is removed, ensuring that the garbage collector does not
547-
// re-enter this method, and the method chains up to continue the process of
548-
// environment-teardown-induced finalization.
549-
550-
// During environment teardown we have to convert a strong reference to
551-
// a weak reference to force the deferring behavior if the user's finalizer
552-
// happens to delete this reference so that the code in this function that
553-
// follows the call to the user's finalizer may safely access variables from
554-
// this instance.
555-
if (is_env_teardown && RefCount() > 0) _refcount = 0;
556-
557-
if (_finalize_callback != nullptr) {
558-
// This ensures that we never call the finalizer twice.
559-
napi_finalize fini = _finalize_callback;
560-
_finalize_callback = nullptr;
561-
_env->CallFinalizer(fini, _finalize_data, _finalize_hint);
562-
}
563-
564-
// this is safe because if a request to delete the reference
565-
// is made in the finalize_callback it will defer deletion
566-
// to this block and set _delete_self to true
567-
if (_delete_self || is_env_teardown) {
568-
Delete(this);
569-
} else {
570-
_finalize_ran = true;
511+
return refcount_;
512+
}
513+
514+
void RefBase::Finalize() {
515+
bool delete_self = delete_self_;
516+
// Swap out the field finalize_callback so that it can not be accidentally
517+
// called more than once.
518+
napi_finalize finalize_callback = finalize_callback_;
519+
finalize_callback_ = nullptr;
520+
521+
// Either the RefBase is going to be deleted in the finalize_callback or not,
522+
// it should be removed from the tracked list.
523+
Unlink();
524+
// 1. If the finalize_callback is present, it should either delete the
525+
// RefBase, or set the flag `delete_self`.
526+
// 2. If the finalizer is not present, the RefBase can be deleted after the
527+
// call.
528+
if (finalize_callback != nullptr) {
529+
env_->CallFinalizer(finalize_callback, finalize_data_, finalize_hint_);
530+
// No access to `this` after finalize_callback is called.
531+
}
532+
533+
// If the RefBase is not self-destructive, userland code should delete it.
534+
// Now delete it if it is self-destructive.
535+
if (delete_self) {
536+
delete this;
571537
}
572538
}
573539

574540
template <typename... Args>
575541
Reference::Reference(napi_env env, v8::Local<v8::Value> value, Args&&... args)
576542
: RefBase(env, std::forward<Args>(args)...),
577-
_persistent(env->isolate, value),
578-
_secondPassParameter(new SecondPassCallParameterRef(this)),
579-
_secondPassScheduled(false) {
543+
persistent_(env->isolate, value) {
580544
if (RefCount() == 0) {
581545
SetWeak();
582546
}
583547
}
584548

549+
Reference::~Reference() {
550+
// Reset the handle. And no weak callback will be invoked.
551+
persistent_.Reset();
552+
}
553+
585554
Reference* Reference::New(napi_env env,
586555
v8::Local<v8::Value> value,
587556
uint32_t initial_refcount,
@@ -598,24 +567,25 @@ Reference* Reference::New(napi_env env,
598567
finalize_hint);
599568
}
600569

601-
Reference::~Reference() {
602-
// If the second pass callback is scheduled, it will delete the
603-
// parameter passed to it, otherwise it will never be scheduled
604-
// and we need to delete it here.
605-
if (!_secondPassScheduled) {
606-
delete _secondPassParameter;
607-
}
608-
}
609-
610570
uint32_t Reference::Ref() {
571+
// When the persistent_ is cleared in the WeakCallback, and a second pass
572+
// callback is pending, return 0 unconditionally.
573+
if (persistent_.IsEmpty()) {
574+
return 0;
575+
}
611576
uint32_t refcount = RefBase::Ref();
612577
if (refcount == 1) {
613-
ClearWeak();
578+
persistent_.ClearWeak();
614579
}
615580
return refcount;
616581
}
617582

618583
uint32_t Reference::Unref() {
584+
// When the persistent_ is cleared in the WeakCallback, and a second pass
585+
// callback is pending, return 0 unconditionally.
586+
if (persistent_.IsEmpty()) {
587+
return 0;
588+
}
619589
uint32_t old_refcount = RefCount();
620590
uint32_t refcount = RefBase::Unref();
621591
if (old_refcount == 1 && refcount == 0) {
@@ -625,99 +595,36 @@ uint32_t Reference::Unref() {
625595
}
626596

627597
v8::Local<v8::Value> Reference::Get() {
628-
if (_persistent.IsEmpty()) {
598+
if (persistent_.IsEmpty()) {
629599
return v8::Local<v8::Value>();
630600
} else {
631-
return v8::Local<v8::Value>::New(_env->isolate, _persistent);
601+
return v8::Local<v8::Value>::New(env_->isolate, persistent_);
632602
}
633603
}
634604

635-
void Reference::Finalize(bool is_env_teardown) {
636-
// During env teardown, `~napi_env()` alone is responsible for finalizing.
637-
// Thus, we don't want any stray gc passes to trigger a second call to
638-
// `RefBase::Finalize()`. ClearWeak will ensure that even if the
639-
// gc is in progress no Finalization will be run for this Reference
640-
// by the gc.
641-
if (is_env_teardown) {
642-
ClearWeak();
643-
}
605+
void Reference::Finalize() {
606+
// Unconditionally reset the persistent handle so that no weak callback will
607+
// be invoked again.
608+
persistent_.Reset();
644609

645610
// Chain up to perform the rest of the finalization.
646-
RefBase::Finalize(is_env_teardown);
647-
}
648-
649-
// ClearWeak is marking the Reference so that the gc should not
650-
// collect it, but it is possible that a second pass callback
651-
// may have been scheduled already if we are in shutdown. We clear
652-
// the secondPassParameter so that even if it has been
653-
// scheduled no Finalization will be run.
654-
void Reference::ClearWeak() {
655-
if (!_persistent.IsEmpty()) {
656-
_persistent.ClearWeak();
657-
}
658-
if (_secondPassParameter != nullptr) {
659-
*_secondPassParameter = nullptr;
660-
}
611+
RefBase::Finalize();
661612
}
662613

663614
// Mark the reference as weak and eligible for collection
664615
// by the gc.
665616
void Reference::SetWeak() {
666-
if (_secondPassParameter == nullptr) {
667-
// This means that the Reference has already been processed
668-
// by the second pass callback, so its already been Finalized, do
669-
// nothing
670-
return;
671-
}
672-
_persistent.SetWeak(
673-
_secondPassParameter, FinalizeCallback, v8::WeakCallbackType::kParameter);
674-
*_secondPassParameter = this;
617+
persistent_.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter);
675618
}
676619

677620
// The N-API finalizer callback may make calls into the engine. V8's heap is
678621
// not in a consistent state during the weak callback, and therefore it does
679-
// not support calls back into it. However, it provides a mechanism for adding
680-
// a finalizer which may make calls back into the engine by allowing us to
681-
// attach such a second-pass finalizer from the first pass finalizer. Thus,
682-
// we do that here to ensure that the N-API finalizer callback is free to call
683-
// into the engine.
684-
void Reference::FinalizeCallback(
685-
const v8::WeakCallbackInfo<SecondPassCallParameterRef>& data) {
686-
SecondPassCallParameterRef* parameter = data.GetParameter();
687-
Reference* reference = *parameter;
688-
if (reference == nullptr) {
689-
return;
690-
}
691-
692-
// The reference must be reset during the first pass.
693-
reference->_persistent.Reset();
694-
// Mark the parameter not delete-able until the second pass callback is
695-
// invoked.
696-
reference->_secondPassScheduled = true;
697-
698-
data.SetSecondPassCallback(SecondPassCallback);
699-
}
700-
701-
// Second pass callbacks are scheduled with platform tasks. At env teardown,
702-
// the tasks may have already be scheduled and we are unable to cancel the
703-
// second pass callback task. We have to make sure that parameter is kept
704-
// alive until the second pass callback is been invoked. In order to do
705-
// this and still allow our code to Finalize/delete the Reference during
706-
// shutdown we have to use a separately allocated parameter instead
707-
// of a parameter within the Reference object itself. This is what
708-
// is stored in _secondPassParameter and it is allocated in the
709-
// constructor for the Reference.
710-
void Reference::SecondPassCallback(
711-
const v8::WeakCallbackInfo<SecondPassCallParameterRef>& data) {
712-
SecondPassCallParameterRef* parameter = data.GetParameter();
713-
Reference* reference = *parameter;
714-
delete parameter;
715-
if (reference == nullptr) {
716-
// the reference itself has already been deleted so nothing to do
717-
return;
718-
}
719-
reference->_secondPassParameter = nullptr;
720-
reference->Finalize();
622+
// not support calls back into it. Enqueue the invocation of the finalizer.
623+
void Reference::WeakCallback(const v8::WeakCallbackInfo<Reference>& data) {
624+
Reference* reference = data.GetParameter();
625+
// The reference must be reset during the weak callback as the API protocol.
626+
reference->persistent_.Reset();
627+
reference->env_->EnqueueFinalizer(reference);
721628
}
722629

723630
} // end of namespace v8impl
@@ -2515,7 +2422,7 @@ napi_status NAPI_CDECL napi_delete_reference(napi_env env, napi_ref ref) {
25152422
CHECK_ENV(env);
25162423
CHECK_ARG(env, ref);
25172424

2518-
v8impl::Reference::Delete(reinterpret_cast<v8impl::Reference*>(ref));
2425+
delete reinterpret_cast<v8impl::Reference*>(ref);
25192426

25202427
return napi_clear_last_error(env);
25212428
}
@@ -3205,7 +3112,7 @@ napi_status NAPI_CDECL napi_set_instance_data(napi_env env,
32053112
if (old_data != nullptr) {
32063113
// Our contract so far has been to not finalize any old data there may be.
32073114
// So we simply delete it.
3208-
v8impl::RefBase::Delete(old_data);
3115+
delete old_data;
32093116
}
32103117

32113118
env->instance_data =

0 commit comments

Comments
 (0)