Skip to content

Commit

Permalink
Make use of the ref returned by napi_wrap
Browse files Browse the repository at this point in the history
  • Loading branch information
zcbenz committed Jul 12, 2024
1 parent f3bec2b commit dc8b93b
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 100 deletions.
83 changes: 26 additions & 57 deletions src/instance_data.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,81 +77,50 @@ class InstanceData {
// address of the class itself, so 1 pointer can actually represent 2
// different instances. To avoid duplicate key for different instances, we
// also include typename as part of the key.
using WeakRefKey = std::pair<const char*, void*>;

// The garbage collection in V8 has 2 phases:
// 1. Garbage collect the unused object, which makes the object stored in
// weak ref become undefined;
// 2. Call the finalizer of the object, which usually calls DeleteWeakRef
// in it to remove the weak ref.
//
// So when we are between 1 and 2, the weak ref is alive because the finalizer
// has not been called, but the object in it has become undefined. If we call
// AddWeakRef with the same |key|, which usually happens when converting a ptr
// to JS immediately after its previous JS wrapper gets GCed, we will have 2
// different weak refs with the same |key|, with a pending finalizer to remove
// the previous weak ref.
//
// In order to make all finalizers happily call DeleteWeakRef without removing
// the current weak ref which points to the living object, we are doing a ref
// counted policy here:
// 1. In AddWeakRef, if there is a weak ref with |key|, add ref count, and
// replace the weak ref with the ref to current value.
// 2. In DeleteWeakRef, only remove the whole |key| when ref count drops to 0.
template<typename T>
void AddWeakRef(void* ptr, napi_value value) {
WeakRefKey key{internal::TopClass<T>::name, ptr};
auto it = weak_refs_.find(key);
if (it != weak_refs_.end()) {
it->second.first++;
it->second.second = Persistent(env_, value, 0);
} else {
weak_refs_.emplace(key, std::make_pair(1, Persistent(env_, value, 0)));
}
}
using WrapperKey = std::pair<const char*, void*>;

// Used to store the results of napi_wrap, it is caller's responsibility to
// destroy the result.
template<typename T>
bool GetWeakRef(void* ptr, napi_value* out) const {
WeakRefKey key{internal::TopClass<T>::name, ptr};
auto it = weak_refs_.find(key);
if (it == weak_refs_.end())
return false;
napi_value result = it->second.second.Value();
if (!result) {
// Betwen GC phase 1 and 2, the weak ref exists but object becomes
// undefined, we want to create a new object in this case so return false.
return false;
}
*out = result;
return true;
void AddWrapper(void* ptr, napi_ref ref) {
WrapperKey key{internal::TopClass<T>::name, ptr};
wrappers_.emplace(key, Persistent(env_, ref));
}

// DeleteWeakRef returns false if there is no weak ref existing for ptr, which
// would only happen when user has removed the weak ref manually.
template<typename T>
bool DeleteWeakRef(void* ptr) {
WeakRefKey key{internal::TopClass<T>::name, ptr};
auto it = weak_refs_.find(key);
if (it == weak_refs_.end())
bool GetWrapper(void* ptr, napi_value* result) const {
WrapperKey key{internal::TopClass<T>::name, ptr};
auto it = wrappers_.find(key);
if (it == wrappers_.end())
return false;
if (--it->second.first == 0)
weak_refs_.erase(it);
return true;
*result = it->second.Value();
return *result != nullptr;
}

size_t WeakRefsSize() const {
return weak_refs_.size();
template<typename T>
void DeleteWrapper(void* ptr) {
WrapperKey key{internal::TopClass<T>::name, ptr};
assert(wrappers_.find(key) != wrappers_.end());
wrappers_.erase(key);
}

private:
explicit InstanceData(napi_env env)
: env_(env),
attached_tables_(env, WeakMap(env)) {}

~InstanceData() {
// Node frees all references on exit whether they belong to user or runtime,
// so we have to leak them to avoid double free.
for (auto& [key, handle] : wrappers_) {
handle.Release();
}
}

napi_env env_;
Persistent attached_tables_;
std::map<void*, Persistent> strong_refs_;
std::map<WeakRefKey, std::pair<uint32_t, Persistent>> weak_refs_;
std::map<WrapperKey, Persistent> wrappers_;

const int tag_ = 0x8964;
};
Expand Down
10 changes: 10 additions & 0 deletions src/persistent.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class Persistent {
assert(s == napi_ok);
}

// This is only used for storing the result of napi_wrap.
Persistent(napi_env env, napi_ref ref)
: env_(env), ref_(ref), is_weak_(true) {}

Persistent() {}

Persistent(const Persistent& other) {
Expand Down Expand Up @@ -89,6 +93,12 @@ class Persistent {
return result;
}

napi_ref Release() {
napi_ref ref = ref_;
ref_ = nullptr;
return ref;
}

bool IsEmpty() const {
return !ref_;
}
Expand Down
28 changes: 8 additions & 20 deletions src/prototype.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,34 +30,28 @@ struct Type<Class<T>> {
template<typename T>
napi_status ManagePointerInJSWrapper(napi_env env, T* ptr, napi_value* result) {
InstanceData* instance_data = InstanceData::Get(env);
if (internal::CanCachePointer<T>::value) {
// Check if there is already a JS object created.
if (instance_data->GetWeakRef<T>(ptr, result))
return napi_ok;
}
// Check if there is already a JS object created.
if (instance_data->GetWrapper<T>(ptr, result))
return napi_ok;
// Create a JS object with "new Class(external)".
napi_value object = internal::CreateInstance<T>(env);
if (!object)
return napi_generic_failure;
// Wrap the |ptr| into JS object.
auto* data = internal::Wrap<T>::Do(ptr);
using DataType = decltype(data);
napi_ref ref;
napi_status s = napi_wrap(env, object, data,
[](napi_env env, void* data, void* ptr) {
if (internal::CanCachePointer<T>::value) {
// If the weak ref has already been removed, do not run finalizer.
if (!InstanceData::Get(env)->DeleteWeakRef<T>(ptr))
return;
}
InstanceData::Get(env)->DeleteWrapper<T>(ptr);
internal::Finalize<T>::Do(static_cast<DataType>(data));
}, ptr, nullptr);
}, ptr, &ref);
if (s != napi_ok) {
internal::Finalize<T>::Do(data);
return s;
}
// Save weak reference.
if (internal::CanCachePointer<T>::value)
instance_data->AddWeakRef<T>(ptr, object);
// Save wrapper.
instance_data->AddWrapper<T>(ptr, ref);
*result = object;
return napi_ok;
}
Expand Down Expand Up @@ -92,12 +86,6 @@ struct Type<T*, std::enable_if_t<!std::is_const_v<T> &&
void* result;
if (napi_unwrap(env, value, &result) != napi_ok)
return std::nullopt;
if (internal::CanCachePointer<T>::value) {
// Check if the pointer has been released by user.
napi_value obj;
if (!InstanceData::Get(env)->GetWeakRef<T>(result, &obj))
return std::nullopt;
}
if (!internal::IsInstanceOf<T>(env, value))
return std::nullopt;
T* ptr = internal::Unwrap<T>::Do(result);
Expand Down
28 changes: 5 additions & 23 deletions src/prototype_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,6 @@ template<typename T>
struct HasDestructor<T, std::void_t<decltype(Type<T>::Destructor)>>
: std::true_type {};

// Whether the JS object can be cached using the pointer as key.
// For heap allocated types this is usually true because they have different
// pointers, however for stack allocated types this is false as the same type
// may be created frequently on stack with the same pointer (for example the
// painter pointer in on_draw handler).
template<typename, typename = void>
struct CanCachePointer : std::true_type {};

template<typename T>
struct CanCachePointer<
T, std::void_t<decltype(TypeBridge<T>::can_cache_pointer)>> {
static constexpr bool value = TypeBridge<T>::can_cache_pointer;
};

// By default JS object can only be created with "new Class", to allow function
// calls like "Class()", set allow_function_call to true.
template<typename, typename = void>
Expand Down Expand Up @@ -279,24 +265,20 @@ struct DefineClass<T, typename std::enable_if<is_function_pointer<
// Then wrap the native pointer.
auto* data = Wrap<T>::Do(ptr.value());
using DataType = decltype(data);
napi_ref ref;
napi_status s = napi_wrap(env, object, data,
[](napi_env env, void* data, void* ptr) {
if (internal::CanCachePointer<T>::value) {
// If the weak ref has already been removed, do not run finalizer.
if (!InstanceData::Get(env)->DeleteWeakRef<T>(ptr))
return;
}
InstanceData::Get(env)->DeleteWrapper<T>(ptr);
Finalize<T>::Do(static_cast<DataType>(data));
Destruct<T>::Do(static_cast<T*>(ptr));
}, ptr.value(), nullptr);
}, ptr.value(), &ref);
if (s != napi_ok) {
Finalize<T>::Do(data);
Destruct<T>::Do(ptr.value());
ThrowError(env, "Unable to wrap native object.");
}
// Save weak reference.
if (internal::CanCachePointer<T>::value)
InstanceData::Get(env)->AddWeakRef<T>(ptr.value(), object);
// Save wrapper.
InstanceData::Get(env)->AddWrapper<T>(ptr.value(), ref);
// For constructor call we should never return an object.
if (is_constructor_call)
return nullptr;
Expand Down

0 comments on commit dc8b93b

Please sign in to comment.