Compare commits

...

9 Commits

Author SHA1 Message Date
Kai Tamkun
8e2bdd1216 Merge branch 'main' into kai/napi-fixes-2 2025-08-13 13:25:46 -07:00
Kai Tamkun
a5e54e48fa Move NapiRef methods into NapiRef.cpp 2025-08-12 19:16:56 -07:00
Kai Tamkun
50692c4a9d Merge branch 'kai/napi-fixes' into kai/napi-fixes-2 2025-08-12 19:08:56 -07:00
Kai Tamkun
e62cc4aa04 Missing newline 2025-08-11 18:10:24 -07:00
Kai Tamkun
7990f60b7f Handle both pending VM exceptions and pending napi exceptions after scheduled napi finalizers 2025-08-11 17:58:28 -07:00
Kai Tamkun
edecc44778 Add test for napi deferred exception behavior 2025-08-11 17:48:12 -07:00
Kai Tamkun
b74ebd457c Add test for napi deferred exception behavior 2025-08-11 17:47:53 -07:00
Kai Tamkun
06a83f0a2b Make napi_get_value_string_any_encoding not fail when the napi_env has a pending exception 2025-08-11 16:31:37 -07:00
Kai Tamkun
830147b182 Don't immediately throw exceptions from napi 2025-08-11 16:23:59 -07:00
3 changed files with 75 additions and 56 deletions

View File

@@ -41,6 +41,70 @@ void NapiRef::clear()
globalObject.clear();
weakValueRef.clear();
strongRef.clear();
if (boundCleanup) {
env->removeFinalizer(*boundCleanup);
}
}
NapiRef::NapiRef(napi_env env, uint32_t count, Bun::NapiFinalizer finalizer, bool deleteSelf)
: env(env)
, globalObject(JSC::Weak<JSC::JSGlobalObject>(env->globalObject()))
, finalizer(WTFMove(finalizer))
, refCount(count)
, m_deleteSelf(deleteSelf)
{
}
void NapiRef::setValueInitial(JSC::JSValue value, bool can_be_weak)
{
if (refCount > 0) {
strongRef.set(globalObject->vm(), value);
}
// In NAPI non-experimental, types other than object, function and symbol can't be used as values for references.
// In NAPI experimental, they can be, but we must not store weak references to them.
if (can_be_weak) {
weakValueRef.set(value, Napi::NapiRefWeakHandleOwner::weakValueHandleOwner(), this);
}
if (value.isSymbol()) {
auto* symbol = jsDynamicCast<JSC::Symbol*>(value);
ASSERT(symbol != nullptr);
if (symbol->uid().isRegistered()) {
// Global symbols must always be retrievable,
// even if garbage collection happens while the ref count is 0.
m_isEternal = true;
if (refCount == 0) {
strongRef.set(globalObject->vm(), symbol);
}
}
}
}
void NapiRef::callFinalizer()
{
// Calling the finalizer may delete `this`, so we have to do state changes on `this` before
// calling the finalizer
Bun::NapiFinalizer saved_finalizer = this->finalizer;
this->finalizer.clear();
saved_finalizer.call(env, nativeObject, !env->mustDeferFinalizers() || !env->inGC());
(void)m_deleteSelf;
}
NapiRef::~NapiRef()
{
NAPI_LOG("destruct napi ref %p", this);
if (boundCleanup) {
boundCleanup->deactivate(env);
boundCleanup = nullptr;
}
if (!m_isEternal) {
strongRef.clear();
}
weakValueRef.clear();
}
}

View File

@@ -505,13 +505,7 @@ public:
void unref();
void clear();
NapiRef(napi_env env, uint32_t count, Bun::NapiFinalizer finalizer)
: env(env)
, globalObject(JSC::Weak<JSC::JSGlobalObject>(env->globalObject()))
, finalizer(WTFMove(finalizer))
, refCount(count)
{
}
NapiRef(napi_env env, uint32_t count, Bun::NapiFinalizer finalizer, bool deleteSelf = false);
JSC::JSValue value() const
{
@@ -522,57 +516,11 @@ public:
return strongRef.get();
}
void setValueInitial(JSC::JSValue value, bool can_be_weak)
{
if (refCount > 0) {
strongRef.set(globalObject->vm(), value);
}
void setValueInitial(JSC::JSValue value, bool can_be_weak);
// In NAPI non-experimental, types other than object, function and symbol can't be used as values for references.
// In NAPI experimental, they can be, but we must not store weak references to them.
if (can_be_weak) {
weakValueRef.set(value, Napi::NapiRefWeakHandleOwner::weakValueHandleOwner(), this);
}
void callFinalizer();
if (value.isSymbol()) {
auto* symbol = jsDynamicCast<JSC::Symbol*>(value);
ASSERT(symbol != nullptr);
if (symbol->uid().isRegistered()) {
// Global symbols must always be retrievable,
// even if garbage collection happens while the ref count is 0.
m_isEternal = true;
if (refCount == 0) {
strongRef.set(globalObject->vm(), symbol);
}
}
}
}
void callFinalizer()
{
// Calling the finalizer may delete `this`, so we have to do state changes on `this` before
// calling the finalizer
Bun::NapiFinalizer saved_finalizer = this->finalizer;
this->finalizer.clear();
saved_finalizer.call(env, nativeObject, !env->mustDeferFinalizers() || !env->inGC());
}
~NapiRef()
{
NAPI_LOG("destruct napi ref %p", this);
if (boundCleanup) {
boundCleanup->deactivate(env);
boundCleanup = nullptr;
}
if (!m_isEternal) {
strongRef.clear();
}
// The weak ref can lead to calling the destructor
// so we must first clear the weak ref before we call the finalizer
weakValueRef.clear();
}
~NapiRef();
napi_env env = nullptr;
JSC::Weak<JSC::JSGlobalObject> globalObject;
@@ -586,6 +534,7 @@ public:
private:
bool m_isEternal = false;
bool m_deleteSelf = false;
};
static inline napi_ref toNapi(NapiRef* val)

View File

@@ -530,6 +530,12 @@ describe("napi", () => {
it("behaves as expected when performing operations with an exception pending", async () => {
await checkSameOutput("test_deferred_exceptions", []);
});
describe("napi_ref deletion", () => {
it("works", async () => {
await checkSameOutput("test_ref_deletion", []);
});
});
});
async function checkSameOutput(test: string, args: any[] | string, envArgs: Record<string, string> = {}) {