Skip to content

Commit

Permalink
node-api: run finalizers directly from GC
Browse files Browse the repository at this point in the history
  • Loading branch information
vmoroz committed Jun 23, 2023
1 parent 640a791 commit 1391400
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 42 deletions.
10 changes: 10 additions & 0 deletions src/js_native_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,16 @@ NAPI_EXTERN napi_status NAPI_CDECL napi_add_finalizer(napi_env env,

#endif // NAPI_VERSION >= 5

#ifdef NAPI_EXPERIMENTAL

NAPI_EXTERN napi_status NAPI_CDECL
node_api_post_finalizer(napi_env env,
napi_finalize finalize_cb,
void* finalize_data,
void* finalize_hint);

#endif // NAPI_EXPERIMENTAL

#if NAPI_VERSION >= 6

// BigInt
Expand Down
130 changes: 93 additions & 37 deletions src/js_native_api_v8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,28 @@
(out) = v8::type::New((buffer), (byte_offset), (length)); \
} while (0)

namespace v8impl {
void napi_env__::InvokeFinalizerFromGC(v8impl::RefTracker* finalizer) {
if (module_api_version != NAPI_VERSION_EXPERIMENTAL) {
EnqueueFinalizer(finalizer);
} else {
// The experimental code calls finalizers immediately to release native
// objects as soon as possible, but it suspends use of JS from finalizer.
// If JS calls are needed, then the finalizer code must call
// node_api_post_finalizer.
if (last_error.error_code == napi_ok && last_exception.IsEmpty()) {
bool saved_suspend_call_into_js = suspend_call_into_js;
finalizer->Finalize();
suspend_call_into_js = saved_suspend_call_into_js;
} else {
// The finalizers can be run in the middle of JS or C++ code.
// That code may be in an error state. In that case use the asynchronous
// finalizer.
EnqueueFinalizer(finalizer);
}
}
}

namespace v8impl {
namespace {

template <typename CCharType, typename StringMaker>
Expand Down Expand Up @@ -604,28 +624,72 @@ void Finalizer::ResetFinalizer() {
finalize_hint_ = nullptr;
}

// Wrapper around v8impl::Persistent that implements reference counting.
RefBase::RefBase(napi_env env,
uint32_t initial_refcount,
Ownership ownership,
napi_finalize finalize_callback,
void* finalize_data,
void* finalize_hint)
TrackedFinalizer::TrackedFinalizer(napi_env env,
napi_finalize finalize_callback,
void* finalize_data,
void* finalize_hint)
: Finalizer(env, finalize_callback, finalize_data, finalize_hint),
refcount_(initial_refcount),
ownership_(ownership) {
RefTracker() {
Link(finalize_callback == nullptr ? &env->reflist : &env->finalizing_reflist);
}

// When a RefBase is being deleted, it may have been queued to call its
TrackedFinalizer* TrackedFinalizer::New(napi_env env,
napi_finalize finalize_callback,
void* finalize_data,
void* finalize_hint) {
return new TrackedFinalizer(
env, finalize_callback, finalize_data, finalize_hint);
}

// When a TrackedFinalizer is being deleted, it may have been queued to call its
// finalizer.
RefBase::~RefBase() {
TrackedFinalizer::~TrackedFinalizer() {
// Remove from the env's tracked list.
Unlink();
// Try to remove the finalizer from the scheduled second pass callback.
env_->DequeueFinalizer(this);
}

void TrackedFinalizer::Finalize() {
FinalizeCore(/*deleteMe:*/ true);
}

void TrackedFinalizer::FinalizeCore(bool deleteMe) {
// Swap out the field finalize_callback so that it can not be accidentally
// called more than once.
napi_finalize finalize_callback = finalize_callback_;
void* finalize_data = finalize_data_;
void* finalize_hint = finalize_hint_;
ResetFinalizer();

// Either the RefBase is going to be deleted in the finalize_callback or not,
// it should be removed from the tracked list.
Unlink();
// 1. If the finalize_callback is present, it should either delete the
// derived RefBase, or set ownership with Ownership::kRuntime.
// 2. If the finalizer is not present, the derived RefBase can be deleted
// after the call.
if (finalize_callback != nullptr) {
env_->CallFinalizer(finalize_callback, finalize_data, finalize_hint);
// No access to `this` after finalize_callback is called.
}

if (deleteMe) {
delete this;
}
}

// Wrapper around v8impl::Persistent that implements reference counting.
RefBase::RefBase(napi_env env,
uint32_t initial_refcount,
Ownership ownership,
napi_finalize finalize_callback,
void* finalize_data,
void* finalize_hint)
: TrackedFinalizer(env, finalize_callback, finalize_data, finalize_hint),
refcount_(initial_refcount),
ownership_(ownership) {}

RefBase* RefBase::New(napi_env env,
uint32_t initial_refcount,
Ownership ownership,
Expand Down Expand Up @@ -660,31 +724,9 @@ uint32_t RefBase::RefCount() {
}

void RefBase::Finalize() {
Ownership ownership = ownership_;
// Swap out the field finalize_callback so that it can not be accidentally
// called more than once.
napi_finalize finalize_callback = finalize_callback_;
void* finalize_data = finalize_data_;
void* finalize_hint = finalize_hint_;
ResetFinalizer();

// Either the RefBase is going to be deleted in the finalize_callback or not,
// it should be removed from the tracked list.
Unlink();
// 1. If the finalize_callback is present, it should either delete the
// RefBase, or set ownership with Ownership::kRuntime.
// 2. If the finalizer is not present, the RefBase can be deleted after the
// call.
if (finalize_callback != nullptr) {
env_->CallFinalizer(finalize_callback, finalize_data, finalize_hint);
// No access to `this` after finalize_callback is called.
}

// If the RefBase is not Ownership::kRuntime, userland code should delete it.
// Now delete it if it is Ownership::kRuntime.
if (ownership == Ownership::kRuntime) {
delete this;
}
// Delete it if it is Ownership::kRuntime.
FinalizeCore(/*deleteMe:*/ ownership_ == Ownership::kRuntime);
}

template <typename... Args>
Expand Down Expand Up @@ -779,7 +821,7 @@ void Reference::WeakCallback(const v8::WeakCallbackInfo<Reference>& data) {
Reference* reference = data.GetParameter();
// The reference must be reset during the weak callback as the API protocol.
reference->persistent_.Reset();
reference->env_->EnqueueFinalizer(reference);
reference->env_->InvokeFinalizerFromGC(reference);
}

} // end of namespace v8impl
Expand Down Expand Up @@ -3310,6 +3352,20 @@ napi_status NAPI_CDECL napi_add_finalizer(napi_env env,
return napi_clear_last_error(env);
}

#ifdef NAPI_EXPERIMENTAL

napi_status NAPI_CDECL node_api_post_finalizer(napi_env env,
napi_finalize finalize_cb,
void* finalize_data,
void* finalize_hint) {
CHECK_ENV(env);
env->EnqueueFinalizer(v8impl::TrackedFinalizer::New(
env, finalize_cb, finalize_data, finalize_hint));
return napi_clear_last_error(env);
}

#endif

napi_status NAPI_CDECL napi_adjust_external_memory(napi_env env,
int64_t change_in_bytes,
int64_t* adjusted_value) {
Expand Down
32 changes: 28 additions & 4 deletions src/js_native_api_v8.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ struct napi_env__ {
if (--refs == 0) DeleteMe();
}

virtual bool can_call_into_js() const { return true; }
virtual bool can_call_into_js() const { return !suspend_call_into_js; }

static inline void HandleThrow(napi_env env, v8::Local<v8::Value> value) {
if (env->terminatedOrTerminating()) {
Expand Down Expand Up @@ -102,9 +102,13 @@ struct napi_env__ {
// Call finalizer immediately.
virtual void CallFinalizer(napi_finalize cb, void* data, void* hint) {
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context());
CallIntoModule([&](napi_env env) { cb(env, data, hint); });
}

// Invoke finalizer from V8 garbage collector.
void InvokeFinalizerFromGC(v8impl::RefTracker* finalizer);

// Enqueue the finalizer to the napi_env's own queue of the second pass
// weak callback.
// Implementation should drain the queue at the time it is safe to call
Expand Down Expand Up @@ -148,6 +152,7 @@ struct napi_env__ {
int refs = 1;
void* instance_data = nullptr;
int32_t module_api_version = NODE_API_DEFAULT_MODULE_API_VERSION;
bool suspend_call_into_js = false;

protected:
// Should not be deleted directly. Delete with `napi_env__::DeleteMe()`
Expand Down Expand Up @@ -363,8 +368,28 @@ enum class Ownership {
kUserland,
};

// Wrapper around Finalizer that implements reference counting.
class RefBase : public Finalizer, public RefTracker {
// Wrapper around Finalizer that can be tracked.
class TrackedFinalizer : public Finalizer, public RefTracker {
protected:
TrackedFinalizer(napi_env env,
napi_finalize finalize_callback,
void* finalize_data,
void* finalize_hint);

public:
static TrackedFinalizer* New(napi_env env,
napi_finalize finalize_callback,
void* finalize_data,
void* finalize_hint);
~TrackedFinalizer() override;

protected:
void Finalize() override;
void FinalizeCore(bool deleteMe);
};

// Wrapper around TrackedFinalizer that implements reference counting.
class RefBase : public TrackedFinalizer {
protected:
RefBase(napi_env env,
uint32_t initial_refcount,
Expand All @@ -380,7 +405,6 @@ class RefBase : public Finalizer, public RefTracker {
napi_finalize finalize_callback,
void* finalize_data,
void* finalize_hint);
virtual ~RefBase();

void* Data();
uint32_t Ref();
Expand Down
2 changes: 1 addition & 1 deletion src/node_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ void node_napi_env__::DeleteMe() {
}

bool node_napi_env__::can_call_into_js() const {
return node_env()->can_call_into_js();
return Super::can_call_into_js() && node_env()->can_call_into_js();
}

void node_napi_env__::CallFinalizer(napi_finalize cb, void* data, void* hint) {
Expand Down
2 changes: 2 additions & 0 deletions src/node_api_internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#include "util-inl.h"

struct node_napi_env__ : public napi_env__ {
using Super = napi_env__;

node_napi_env__(v8::Local<v8::Context> context,
const std::string& module_filename,
int32_t module_api_version);
Expand Down

0 comments on commit 1391400

Please sign in to comment.