Compare commits

...

8 Commits

Author SHA1 Message Date
Ben Grant
23dc0fed71 Test that threadsafe function finalizers run on the next tick 2024-10-04 15:24:01 -07:00
Ben Grant
afcf7b1eb6 Route all finalizers through NapiFinalizer 2024-10-04 15:23:23 -07:00
Ben Grant
2583f33a33 Make NAPI tests allocate in the finalizer 2024-10-04 11:08:37 -07:00
Ben Grant
dfa2a6b60b Fix tsfn finalizers 2024-10-03 18:48:25 -07:00
Ben Grant
e66ec2a10b Merge branch 'main' into jarred/napi-2 2024-10-03 17:56:38 -07:00
Dylan Conway
eb8d465c50 Merge branch 'main' into jarred/napi-2 2024-07-22 21:14:24 -07:00
Jarred-Sumner
418139358b Apply formatting changes 2024-07-19 08:20:27 +00:00
Jarred Sumner
1f5359705e Defer finalization for napi callbacks to the immediate task queue 2024-07-19 01:09:31 -07:00
15 changed files with 228 additions and 66 deletions

View File

@@ -13,7 +13,6 @@
#include "JavaScriptCore/PutPropertySlot.h"
#include "ScriptExecutionContext.h"
#include "headers-handwritten.h"
#include "node_api.h"
#include "ZigGlobalObject.h"
#include "headers.h"
#include "JSEnvironmentVariableMap.h"

View File

@@ -1108,10 +1108,7 @@ GlobalObject::GlobalObject(JSC::VM& vm, JSC::Structure* structure, WebCore::Scri
GlobalObject::~GlobalObject()
{
if (napiInstanceDataFinalizer) {
napi_finalize finalizer = reinterpret_cast<napi_finalize>(napiInstanceDataFinalizer);
finalizer(toNapi(this), napiInstanceData, napiInstanceDataFinalizerHint);
}
napiFinalizer.call(napiInstanceData);
if (auto* ctx = scriptExecutionContext()) {
ctx->removeFromContextsMap();

View File

@@ -52,6 +52,7 @@ class GlobalInternals;
#include "headers-handwritten.h"
#include "BunCommonStrings.h"
#include "BunGlobalScope.h"
#include "napi_finalizer.h"
namespace WebCore {
class WorkerGlobalScope;
@@ -473,8 +474,7 @@ public:
// This is not a correct implementation
// Addon modules can override each other's data
void* napiInstanceData = nullptr;
void* napiInstanceDataFinalizer = nullptr;
void* napiInstanceDataFinalizerHint = nullptr;
NapiFinalizer napiFinalizer = {};
Bun::JSMockModule mockModule;

View File

@@ -1,5 +1,6 @@
#include "node_api.h"
#include "root.h"
#include "JavaScriptCore/ConstructData.h"
#include "JavaScriptCore/DateInstance.h"
#include "JavaScriptCore/JSCast.h"
@@ -126,6 +127,7 @@ static inline Zig::GlobalObject* defaultGlobalObject(napi_env env)
class NapiRefWeakHandleOwner final : public JSC::WeakHandleOwner {
public:
// Equivalent to v8impl::Ownership::kUserland
void finalize(JSC::Handle<JSC::Unknown>, void* context) final
{
auto* weakValue = reinterpret_cast<NapiRef*>(context);
@@ -133,25 +135,40 @@ public:
auto finalizer = weakValue->finalizer;
if (finalizer.finalize_cb) {
weakValue->finalizer.finalize_cb = nullptr;
finalizer.call(weakValue->globalObject.get(), weakValue->data);
finalizer.call(weakValue->data);
}
}
};
class NapiRefSelfDeletingWeakHandleOwner final : public JSC::WeakHandleOwner {
public:
// Equivalent to v8impl::Ownership::kRuntime
void finalize(JSC::Handle<JSC::Unknown>, void* context) final
{
auto* weakValue = reinterpret_cast<NapiRef*>(context);
auto finalizer = weakValue->finalizer;
if (finalizer.finalize_cb) {
weakValue->finalizer.finalize_cb = nullptr;
finalizer.call(weakValue->data);
}
delete weakValue;
}
static NapiRefSelfDeletingWeakHandleOwner& weakValueHandleOwner()
{
static NeverDestroyed<NapiRefSelfDeletingWeakHandleOwner> jscWeakValueHandleOwner;
return jscWeakValueHandleOwner;
}
};
static NapiRefWeakHandleOwner& weakValueHandleOwner()
{
static NeverDestroyed<NapiRefWeakHandleOwner> jscWeakValueHandleOwner;
return jscWeakValueHandleOwner;
}
void NapiFinalizer::call(JSC::JSGlobalObject* globalObject, void* data)
{
if (this->finalize_cb) {
NAPI_PREMABLE
this->finalize_cb(toNapi(globalObject), data, this->finalize_hint);
}
}
void NapiRef::ref()
{
++refCount;
@@ -180,7 +197,7 @@ void NapiRef::unref()
void NapiRef::clear()
{
this->finalizer.call(this->globalObject.get(), this->data);
this->finalizer.call(this->data);
this->globalObject.clear();
this->weakValueRef.clear();
this->strongRef.clear();
@@ -916,7 +933,8 @@ node_api_create_external_string_latin1(napi_env env,
#if NAPI_VERBOSE
printf("[napi] string finalize_callback\n");
#endif
finalize_callback(toNapi(defaultGlobalObject()), nullptr, hint);
NapiFinalizer finalizer { hint, finalize_callback };
finalizer.call(str);
}
});
Zig::GlobalObject* globalObject = toJS(env);
@@ -952,10 +970,8 @@ node_api_create_external_string_utf16(napi_env env,
#if NAPI_VERBOSE
printf("[napi] string finalize_callback\n");
#endif
if (finalize_callback) {
finalize_callback(toNapi(defaultGlobalObject()), nullptr, hint);
}
NapiFinalizer finalizer { hint, finalize_callback };
finalizer.call(str);
});
Zig::GlobalObject* globalObject = toJS(env);
@@ -1065,7 +1081,11 @@ extern "C" napi_status napi_wrap(napi_env env,
auto* ref = new NapiRef(globalObject, 0);
ref->weakValueRef.set(value, weakValueHandleOwner(), ref);
if (result) {
ref->weakValueRef.set(value, weakValueHandleOwner(), ref);
} else {
ref->weakValueRef.set(value, NapiRefSelfDeletingWeakHandleOwner::weakValueHandleOwner(), ref);
}
if (finalize_cb) {
ref->finalizer.finalize_cb = finalize_cb;
@@ -1117,7 +1137,14 @@ extern "C" napi_status napi_remove_wrap(napi_env env, napi_value js_object,
if (result) {
*result = ref->data;
}
delete ref;
// RemoveWrap
if (ref->isOwnedByRuntime) {
delete ref;
} else {
ref->finalizer.finalize_cb = nullptr;
ref->finalizer.finalize_hint = nullptr;
}
return napi_ok;
}
@@ -1133,6 +1160,11 @@ extern "C" napi_status napi_unwrap(napi_env env, napi_value js_object,
return NAPI_OBJECT_EXPECTED;
}
// KeepWrap
if (!result) {
return napi_invalid_arg;
}
NapiRef* ref = nullptr;
if (auto* val = jsDynamicCast<NapiPrototype*>(value)) {
ref = val->napiRef;
@@ -1142,8 +1174,8 @@ extern "C" napi_status napi_unwrap(napi_env env, napi_value js_object,
ASSERT(false);
}
if (ref && result) {
*result = ref ? ref->data : nullptr;
if (ref) {
*result = ref->data;
}
return napi_ok;
@@ -1416,17 +1448,30 @@ extern "C" napi_status napi_add_finalizer(napi_env env, napi_value js_object,
JSC::VM& vm = globalObject->vm();
JSC::JSValue objectValue = toJS(js_object);
JSC::JSObject* object = objectValue.getObject();
if (!object) {
if (UNLIKELY(!objectValue.isObject())) {
return napi_object_expected;
}
if (UNLIKELY(!finalize_cb)) {
return napi_invalid_arg;
}
vm.heap.addFinalizer(object, [=](JSCell* cell) -> void {
#if NAPI_VERBOSE
printf("napi_add_finalizer: %p\n", finalize_hint);
#endif
finalize_cb(env, native_object, finalize_hint);
});
JSC::JSObject* object = objectValue.getObject();
if (result) {
// If they're expecting a Ref, use the ref.
auto* ref = new NapiRef(globalObject, 0);
ref->weakValueRef.set(object, weakValueHandleOwner(), ref);
ref->finalizer.finalize_cb = finalize_cb;
ref->finalizer.finalize_hint = finalize_hint;
ref->data = native_object;
*result = toNapi(ref);
} else {
// Otherwise, it's cheaper to just call .addFinalizer.
vm.heap.addFinalizer(object, [finalize_cb, native_object, finalize_hint](JSCell* cell) -> void {
NapiFinalizer finalizer { finalize_hint, finalize_cb };
finalizer.call(native_object);
});
}
return napi_ok;
}
@@ -2174,13 +2219,12 @@ extern "C" napi_status napi_create_external_buffer(napi_env env, size_t length,
Zig::GlobalObject* globalObject = toJS(env);
auto arrayBuffer = ArrayBuffer::createFromBytes({ reinterpret_cast<const uint8_t*>(data), length }, createSharedTask<void(void*)>([globalObject, finalize_hint, finalize_cb](void* p) {
auto arrayBuffer = ArrayBuffer::createFromBytes({ reinterpret_cast<const uint8_t*>(data), length }, createSharedTask<void(void*)>([finalize_hint, finalize_cb](void* p) {
#if NAPI_VERBOSE
printf("[napi] buffer finalize_callback\n");
#endif
if (finalize_cb != nullptr) {
finalize_cb(toNapi(globalObject), p, finalize_hint);
}
NapiFinalizer finalizer { finalize_hint, finalize_cb };
finalizer.call(p);
}));
auto* subclassStructure = globalObject->JSBufferSubclassStructure();
@@ -2202,13 +2246,12 @@ extern "C" napi_status napi_create_external_arraybuffer(napi_env env, void* exte
Zig::GlobalObject* globalObject = toJS(env);
JSC::VM& vm = globalObject->vm();
auto arrayBuffer = ArrayBuffer::createFromBytes({ reinterpret_cast<const uint8_t*>(external_data), byte_length }, createSharedTask<void(void*)>([globalObject, finalize_hint, finalize_cb](void* p) {
auto arrayBuffer = ArrayBuffer::createFromBytes({ reinterpret_cast<const uint8_t*>(external_data), byte_length }, createSharedTask<void(void*)>([finalize_hint, finalize_cb](void* p) {
#if NAPI_VERBOSE
printf("[napi] arraybuffer finalize_callback\n");
#endif
if (finalize_cb != nullptr) {
finalize_cb(toNapi(globalObject), p, finalize_hint);
}
NapiFinalizer finalizer { finalize_hint, finalize_cb };
finalizer.call(p);
}));
auto* buffer = JSC::JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(ArrayBufferSharingMode::Shared), WTFMove(arrayBuffer));
@@ -2400,7 +2443,7 @@ extern "C" napi_status napi_create_external(napi_env env, void* data,
JSC::VM& vm = globalObject->vm();
auto* structure = globalObject->NapiExternalStructure();
JSValue value = Bun::NapiExternal::create(vm, structure, data, finalize_hint, reinterpret_cast<void*>(finalize_cb));
JSValue value = Bun::NapiExternal::create(vm, structure, data, finalize_hint, finalize_cb);
JSC::EnsureStillAliveScope ensureStillAlive(value);
*result = toNapi(value, globalObject);
return napi_ok;
@@ -2627,8 +2670,7 @@ extern "C" napi_status napi_set_instance_data(napi_env env,
Zig::GlobalObject* globalObject = toJS(env);
globalObject->napiInstanceData = data;
globalObject->napiInstanceDataFinalizer = reinterpret_cast<void*>(finalize_cb);
globalObject->napiInstanceDataFinalizerHint = finalize_hint;
globalObject->napiFinalizer = { finalize_hint, finalize_cb };
return napi_ok;
}

View File

@@ -12,6 +12,7 @@
#include "JSFFIFunction.h"
#include "ZigGlobalObject.h"
#include "napi_handle_scope.h"
#include "napi_finalizer.h"
namespace JSC {
class JSGlobalObject;
@@ -51,14 +52,6 @@ static inline napi_env toNapi(JSC::JSGlobalObject* val)
return reinterpret_cast<napi_env>(val);
}
class NapiFinalizer {
public:
void* finalize_hint = nullptr;
napi_finalize finalize_cb;
void call(JSC::JSGlobalObject* globalObject, void* data);
};
// This is essentially JSC::JSWeakValue, except with a JSCell* instead of a
// JSObject*. Sometimes, a napi embedder might want to store a JSC::Exception, a
// JSC::HeapBigInt, JSC::Symbol, etc inside of a NapiRef. So we can't limit it
@@ -178,6 +171,7 @@ public:
NapiFinalizer finalizer;
void* data = nullptr;
uint32_t refCount = 0;
bool isOwnedByRuntime = false;
};
static inline napi_ref toNapi(NapiRef* val)

View File

@@ -5,11 +5,9 @@ namespace Bun {
NapiExternal::~NapiExternal()
{
if (finalizer) {
// We cannot call globalObject() here because it is in a finalizer.
// https://github.com/oven-sh/bun/issues/13001#issuecomment-2290022312
reinterpret_cast<napi_finalize>(finalizer)(toNapi(this->napi_env), m_value, m_finalizerHint);
}
// We cannot call globalObject() here because it is in a finalizer.
// https://github.com/oven-sh/bun/issues/13001#issuecomment-2290022312
m_finalizer.call(m_value);
}
void NapiExternal::destroy(JSC::JSCell* cell)

View File

@@ -6,6 +6,7 @@
#include "BunBuiltinNames.h"
#include "BunClientData.h"
#include "napi.h"
namespace Bun {
@@ -47,7 +48,7 @@ public:
JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
}
static NapiExternal* create(JSC::VM& vm, JSC::Structure* structure, void* value, void* finalizer_hint, void* finalizer)
static NapiExternal* create(JSC::VM& vm, JSC::Structure* structure, void* value, void* finalizer_hint, napi_finalize finalizer)
{
NapiExternal* accessor = new (NotNull, JSC::allocateCell<NapiExternal>(vm)) NapiExternal(vm, structure);
@@ -75,13 +76,13 @@ public:
return accessor;
}
void finishCreation(JSC::VM& vm, void* value, void* finalizer_hint, void* finalizer)
void finishCreation(JSC::VM& vm, void* value, void* finalizer_hint, napi_finalize finalizer)
{
Base::finishCreation(vm);
m_value = value;
m_finalizerHint = finalizer_hint;
m_finalizer.finalize_hint = finalizer_hint;
m_finalizer.finalize_cb = reinterpret_cast<napi_finalize>(finalizer);
napi_env = this->globalObject();
this->finalizer = finalizer;
}
static void destroy(JSC::JSCell* cell);
@@ -89,8 +90,7 @@ public:
void* value() const { return m_value; }
void* m_value;
void* m_finalizerHint;
void* finalizer;
NapiFinalizer m_finalizer;
JSGlobalObject* napi_env;
#if BUN_DEBUG
@@ -100,4 +100,4 @@ public:
#endif
};
} // namespace Zig
} // namespace Zig

View File

@@ -0,0 +1,14 @@
#include "napi_finalizer.h"
extern "C" void napi_enqueue_finalizer(napi_finalize finalize_cb, void* data, void* hint);
namespace Zig {
void NapiFinalizer::call(void* data)
{
if (this->finalize_cb) {
napi_enqueue_finalizer(this->finalize_cb, data, this->finalize_hint);
}
}
} // namespace Zig

View File

@@ -0,0 +1,15 @@
#pragma once
#include "js_native_api_types.h"
namespace Zig {
class NapiFinalizer {
public:
void* finalize_hint = nullptr;
napi_finalize finalize_cb = nullptr;
void call(void* data);
};
} // namespace Zig

View File

@@ -766,6 +766,7 @@ pub const EventLoop = struct {
///
/// Having two queues avoids infinite loops creating by calling `setImmediate` in a `setImmediate` callback.
immediate_tasks: Queue = undefined,
napi_finalizer_queue: JSC.napi.Finalizer.Queue = undefined,
next_immediate_tasks: Queue = undefined,
concurrent_tasks: ConcurrentTask.Queue = ConcurrentTask.Queue{},
@@ -1258,6 +1259,7 @@ pub const EventLoop = struct {
pub fn tickImmediateTasks(this: *EventLoop, virtual_machine: *VirtualMachine) void {
_ = this.tickQueueWithCount(virtual_machine, "immediate_tasks");
JSC.napi.Finalizer.drain(&this.napi_finalizer_queue, this.global);
}
fn tickConcurrent(this: *EventLoop) void {

View File

@@ -1587,6 +1587,7 @@ pub const VirtualMachine = struct {
this.has_enabled_macro_mode = true;
this.macro_event_loop.tasks = EventLoop.Queue.init(default_allocator);
this.macro_event_loop.immediate_tasks = EventLoop.Queue.init(default_allocator);
this.macro_event_loop.napi_finalizer_queue = JSC.napi.Finalizer.Queue.init(default_allocator);
this.macro_event_loop.next_immediate_tasks = EventLoop.Queue.init(default_allocator);
this.macro_event_loop.tasks.ensureTotalCapacity(16) catch unreachable;
this.macro_event_loop.global = this.global;
@@ -1681,6 +1682,7 @@ pub const VirtualMachine = struct {
vm.regular_event_loop.immediate_tasks = EventLoop.Queue.init(
default_allocator,
);
vm.regular_event_loop.napi_finalizer_queue = JSC.napi.Finalizer.Queue.init(default_allocator);
vm.regular_event_loop.next_immediate_tasks = EventLoop.Queue.init(
default_allocator,
);
@@ -1785,6 +1787,7 @@ pub const VirtualMachine = struct {
vm.regular_event_loop.tasks = EventLoop.Queue.init(
default_allocator,
);
vm.regular_event_loop.napi_finalizer_queue = JSC.napi.Finalizer.Queue.init(default_allocator);
vm.regular_event_loop.immediate_tasks = EventLoop.Queue.init(
default_allocator,
);
@@ -1920,6 +1923,7 @@ pub const VirtualMachine = struct {
.debug_thread_id = if (Environment.allow_assert) std.Thread.getCurrentId() else {},
};
vm.source_mappings.init(&vm.saved_source_map_table);
vm.regular_event_loop.napi_finalizer_queue = JSC.napi.Finalizer.Queue.init(default_allocator);
vm.regular_event_loop.tasks = EventLoop.Queue.init(
default_allocator,
);

View File

@@ -1474,6 +1474,24 @@ pub export fn napi_remove_env_cleanup_hook(env: napi_env, fun: ?*const fn (?*any
pub const Finalizer = struct {
fun: napi_finalize,
data: ?*anyopaque = null,
hint: ?*anyopaque = null,
pub const Queue = std.fifo.LinearFifo(Finalizer, .Dynamic);
pub fn drain(this: *Finalizer.Queue, env: napi_env) void {
while (this.readItem()) |*finalizer| {
const handle_scope = NapiHandleScope.open(env, false);
defer if (handle_scope) |scope| scope.close(env);
finalizer.fun.?(env, finalizer.data, finalizer.hint);
}
}
/// Node defers finalizers to the immediate task queue.
/// This is most likely to account for napi addons which cause GC inside of the finalizer.
pub export fn napi_enqueue_finalizer(fun: napi_finalize, data: ?*anyopaque, hint: ?*anyopaque) callconv(.C) void {
const vm = JSC.VirtualMachine.get();
vm.eventLoop().napi_finalizer_queue.writeItem(.{ .fun = fun, .data = data, .hint = hint }) catch bun.outOfMemory();
}
};
// TODO: generate comptime version of this instead of runtime checking
@@ -1626,7 +1644,7 @@ pub const ThreadSafeFunction = struct {
this.unref();
if (this.finalizer.fun) |fun| {
fun(this.event_loop.global, this.finalizer.data, this.ctx);
Finalizer.napi_enqueue_finalizer(fun, this.finalizer.data, this.ctx);
}
if (this.callback == .js) {

View File

@@ -461,6 +461,15 @@ struct ThreadsafeFunctionData {
ThreadsafeFunctionData *data =
reinterpret_cast<ThreadsafeFunctionData *>(finalize_data);
delete data;
// try allocating
napi_value str;
assert(napi_create_string_utf8(env, "hello", NAPI_AUTO_LENGTH, &str) ==
napi_ok);
char buf[6];
size_t bytes;
assert(napi_get_value_string_utf8(env, str, buf, 6, &bytes) == napi_ok);
printf("string created in tsfn finalizer = %s\n", buf);
}
static void tsfn_callback(napi_env env, napi_value js_callback, void *context,
@@ -646,6 +655,60 @@ napi_value eval_wrapper(const Napi::CallbackInfo &info) {
return ret;
}
class WrappedNumber {
double m_number;
public:
WrappedNumber(double number) : m_number(number) {}
double get() const { return m_number; }
static void finalize(napi_env env, void *data, void *hint) {
// check we can allocate
napi_value str;
assert(napi_create_string_utf8(env, "hello", NAPI_AUTO_LENGTH, &str) ==
napi_ok);
char buf[6];
size_t bytes;
assert(napi_get_value_string_utf8(env, str, buf, 6, &bytes) == napi_ok);
printf("string created in external finalizer = %s\n", buf);
auto *wrapped = reinterpret_cast<WrappedNumber *>(data);
delete wrapped;
}
};
// wrap_number_in_external(5.0)
napi_value wrap_number_in_external(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value num = info[0];
double c_num;
if (napi_get_value_double(env, num, &c_num) != napi_ok) {
napi_value undefined;
assert(napi_get_undefined(env, &undefined) == napi_ok);
return undefined;
}
auto *wrapped = new WrappedNumber(c_num);
napi_value external;
assert(napi_create_external(env, reinterpret_cast<void *>(wrapped),
WrappedNumber::finalize, nullptr,
&external) == napi_ok);
return external;
}
napi_value get_number_from_external(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value external = info[0];
void *wrapped;
assert(napi_get_value_external(env, external, &wrapped) == napi_ok);
napi_value num;
assert(napi_create_double(env,
reinterpret_cast<WrappedNumber *>(wrapped)->get(),
&num) == napi_ok);
return num;
}
Napi::Value RunCallback(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
// this function is invoked without the GC callback
@@ -699,6 +762,10 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports1) {
exports.Set("call_and_get_exception",
Napi::Function::New(env, call_and_get_exception));
exports.Set("eval_wrapper", Napi::Function::New(env, eval_wrapper));
exports.Set("wrap_number_in_external",
Napi::Function::New(env, wrap_number_in_external));
exports.Set("get_number_from_external",
Napi::Function::New(env, get_number_from_external));
return exports;
}

View File

@@ -44,4 +44,10 @@ nativeTests.test_get_exception = (_, value) => {
}
};
nativeTests.test_external = () => {
let external = nativeTests.wrap_number_in_external(4.5);
console.log("from JS, external looks like", typeof external);
console.log(nativeTests.get_number_from_external(external));
};
module.exports = nativeTests;

View File

@@ -275,6 +275,12 @@ describe("napi", () => {
checkSameOutput("eval_wrapper", ["shouldNotExist"]);
});
});
describe("napi_external", () => {
it("can wrap a value and run a finalizer", () => {
checkSameOutput("test_external", []);
});
});
});
function checkSameOutput(test: string, args: any[] | string) {