@nxtedition/shared
Advanced tools
+2
-3
| { | ||
| "name": "@nxtedition/shared", | ||
| "version": "5.0.4", | ||
| "version": "5.0.5", | ||
| "type": "module", | ||
@@ -37,3 +37,2 @@ "main": "lib/index.js", | ||
| "dependencies": { | ||
| "nan": "^2.25.0", | ||
| "node-gyp-build": "^4.8.2" | ||
@@ -51,3 +50,3 @@ }, | ||
| }, | ||
| "gitHead": "c9b449408262ca9431467193d624f1a043d669bb" | ||
| "gitHead": "a58d43fbac9afd98840f23588497be3184bf874c" | ||
| } |
+122
-139
@@ -1,2 +0,3 @@ | ||
| #include <nan.h> | ||
| #define NAPI_EXPERIMENTAL | ||
| #include <node_api.h> | ||
@@ -358,63 +359,2 @@ #include <atomic> | ||
| // ─── ArrayBuffer detach-on-environment-cleanup ─────────────────────────────── | ||
| // | ||
| // V8 14.x has a bug in ArrayBufferSweeper::ReleaseAll that crashes when a | ||
| // worker thread exits and its isolate holds external ArrayBuffers (backed by | ||
| // non-V8-managed memory such as our mmap'd ring buffer). The crash occurs | ||
| // inside V8 before any deleter is called. | ||
| // | ||
| // The fix: register a cleanup hook via node::AddEnvironmentCleanupHook that | ||
| // fires BEFORE ArrayBufferSweeper::ReleaseAll during worker teardown. The hook | ||
| // detaches the ArrayBuffer (removing it from the sweeper's tracking list) and | ||
| // releases the native ring_buf_t reference. | ||
| // | ||
| // If the ArrayBuffer is collected by the GC normally (before the environment | ||
| // closes), the weak callback fires, removes the cleanup hook, and releases | ||
| // the ring_buf_t — preventing a double-release. | ||
| struct AbRef | ||
| { | ||
| v8::Global<v8::ArrayBuffer> global; | ||
| ring_buf_t *rb; | ||
| v8::Isolate *isolate; | ||
| }; | ||
| // Called by the environment cleanup hook (fires before ArrayBufferSweeper::ReleaseAll). | ||
| static void ab_cleanup_hook(void *arg) | ||
| { | ||
| AbRef *ref = static_cast<AbRef *>(arg); | ||
| if (!ref->global.IsEmpty()) | ||
| { | ||
| // AB is still alive — detach it to prevent V8 14.x crash in ReleaseAll, | ||
| // then release our ring buffer reference. | ||
| v8::HandleScope scope(ref->isolate); | ||
| auto ab = ref->global.Get(ref->isolate); | ||
| if (!ab.IsEmpty() && !ab->WasDetached()) | ||
| { | ||
| ab->Detach(); | ||
| } | ||
| // Clear the global so ab_weak_callback knows we already handled cleanup. | ||
| ref->global.Reset(); | ||
| ring_buf_release(ref->rb); | ||
| } | ||
| // If global is already empty the weak callback already ran and released rb. | ||
| delete ref; | ||
| } | ||
| // Called by V8 GC when the ArrayBuffer becomes unreachable (normal lifecycle). | ||
| // IMPORTANT: Must NOT call node::RemoveEnvironmentCleanupHook here — that | ||
| // function asserts the current environment is non-null, which is not | ||
| // guaranteed inside a GC callback (no JS context is active). Instead we use | ||
| // the empty global as a sentinel: ab_cleanup_hook checks IsEmpty() and skips | ||
| // the release if the weak callback already handled it. | ||
| static void ab_weak_callback(const v8::WeakCallbackInfo<AbRef> &info) | ||
| { | ||
| AbRef *ref = info.GetParameter(); | ||
| // Release the ring buffer ref now. The cleanup hook will see IsEmpty() == | ||
| // true and skip the release, preventing a double-free. | ||
| ref->global.Reset(); | ||
| ring_buf_release(ref->rb); | ||
| // Do NOT delete ref — the cleanup hook will do that. | ||
| } | ||
| // ─── Helper: extract ring_buf_t* from a SharedHandle ───────────────────────── | ||
@@ -424,24 +364,27 @@ | ||
| // ring_alloc. Returns nullptr and throws a TypeError on failure. | ||
| static ring_buf_t *rb_from_handle(v8::Local<v8::Value> val, const char *ctx) | ||
| static ring_buf_t *rb_from_handle(napi_env env, napi_value val, const char *ctx) | ||
| { | ||
| if (!val->IsSharedArrayBuffer()) | ||
| bool is_sab = false; | ||
| node_api_is_sharedarraybuffer(env, val, &is_sab); | ||
| if (!is_sab) | ||
| { | ||
| char msg[128]; | ||
| snprintf(msg, sizeof(msg), "%s: expected SharedArrayBuffer handle", ctx); | ||
| Nan::ThrowTypeError(msg); | ||
| napi_throw_type_error(env, nullptr, msg); | ||
| return nullptr; | ||
| } | ||
| auto sab = val.As<v8::SharedArrayBuffer>(); | ||
| auto bs = sab->GetBackingStore(); | ||
| void *data = nullptr; | ||
| size_t len = 0; | ||
| napi_get_arraybuffer_info(env, val, &data, &len); | ||
| if (!bs || bs->ByteLength() < STATE_BYTES) | ||
| if (!data || len < STATE_BYTES) | ||
| { | ||
| char msg[128]; | ||
| snprintf(msg, sizeof(msg), "%s: invalid handle (too small)", ctx); | ||
| Nan::ThrowTypeError(msg); | ||
| napi_throw_type_error(env, nullptr, msg); | ||
| return nullptr; | ||
| } | ||
| const uint32_t *s = static_cast<const uint32_t *>(bs->Data()); | ||
| const uint32_t *s = static_cast<const uint32_t *>(data); | ||
| if (s[MAGIC1_INDEX] != MAGIC1 || s[MAGIC2_INDEX] != MAGIC2) | ||
@@ -451,3 +394,3 @@ { | ||
| snprintf(msg, sizeof(msg), "%s: invalid handle (bad magic)", ctx); | ||
| Nan::ThrowTypeError(msg); | ||
| napi_throw_type_error(env, nullptr, msg); | ||
| return nullptr; | ||
@@ -461,24 +404,56 @@ } | ||
| // ─── NAN methods ───────────────────────────────────────────────────────────── | ||
| // ─── Callbacks ─────────────────────────────────────────────────────────────── | ||
| // Finalizer for the external ArrayBuffer returned by ring_get_array_buffer. | ||
| // napi_create_external_arraybuffer routes through Node.js's Buffer | ||
| // infrastructure which safely handles worker-thread teardown. | ||
| static void ab_finalizer(node_api_basic_env /*env*/, void * /*finalize_data*/, void *finalize_hint) | ||
| { | ||
| ring_buf_release(static_cast<ring_buf_t *>(finalize_hint)); | ||
| } | ||
| // Environment cleanup hook that releases the initial ring_buf_t reference held | ||
| // by the SAB-creating environment. Fires when that environment closes (e.g. | ||
| // process exit), so ring_buf_t and the mmap region are freed once all per-AB | ||
| // references have also been released. | ||
| static void sab_cleanup_hook(void *arg) { ring_buf_release(static_cast<ring_buf_t *>(arg)); } | ||
| // ─── NAPI methods ───────────────────────────────────────────────────────────── | ||
| // ring_alloc(size: number): SharedArrayBuffer | ||
| // | ||
| // Allocates a double-mapped ring buffer and a 192-byte state block. | ||
| // Returns a SharedArrayBuffer wrapping the state block. The SAB's | ||
| // BackingStore uses V8's cross-isolate ref-count: when the last thread that | ||
| // holds the SAB drops it, state_deleter fires — freeing the state block and | ||
| // releasing the native ring_buf_t. No explicit ring_free needed from JS. | ||
| NAN_METHOD(ring_alloc) | ||
| // Allocates a double-mapped ring buffer and a 128-byte state block. | ||
| // Returns a SharedArrayBuffer wrapping the state block (V8-managed memory). | ||
| // | ||
| // TODO (fix): NAPI has no napi_create_external_shared_array_buffer so we use | ||
| // node_api_create_sharedarraybuffer (V8-managed SAB memory) as a workaround. | ||
| // The initial ring_buf_t reference is released via sab_cleanup_hook when the | ||
| // creating environment closes. Once upstream adds external SAB support this | ||
| // can be simplified: https://github.com/nodejs/node/issues/62259 | ||
| static napi_value ring_alloc(napi_env env, napi_callback_info info) | ||
| { | ||
| if (info.Length() < 1 || !info[0]->IsUint32()) | ||
| size_t argc = 1; | ||
| napi_value args[1]; | ||
| napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); | ||
| if (argc < 1) | ||
| { | ||
| Nan::ThrowTypeError("ring_alloc: size must be a positive integer"); | ||
| return; | ||
| napi_throw_type_error(env, nullptr, "ring_alloc: size must be a positive integer"); | ||
| return nullptr; | ||
| } | ||
| uint32_t req_size = Nan::To<uint32_t>(info[0]).FromMaybe(0u); | ||
| napi_valuetype vtype; | ||
| napi_typeof(env, args[0], &vtype); | ||
| if (vtype != napi_number) | ||
| { | ||
| napi_throw_type_error(env, nullptr, "ring_alloc: size must be a positive integer"); | ||
| return nullptr; | ||
| } | ||
| uint32_t req_size = 0; | ||
| napi_get_value_uint32(env, args[0], &req_size); | ||
| if (req_size == 0) | ||
| { | ||
| Nan::ThrowRangeError("ring_alloc: size must be positive"); | ||
| return; | ||
| napi_throw_range_error(env, nullptr, "ring_alloc: size must be positive"); | ||
| return nullptr; | ||
| } | ||
@@ -490,25 +465,27 @@ | ||
| char msg[128]; | ||
| Nan::ThrowError(ring_buf_errmsg(msg, sizeof(msg))); | ||
| return; | ||
| napi_throw_error(env, nullptr, ring_buf_errmsg(msg, sizeof(msg))); | ||
| return nullptr; | ||
| } | ||
| // TODO (fix): V8 14.x crashes in ArrayBufferSweeper::ReleaseAll when a worker thread exits | ||
| // and holds a [Shared]ArrayBuffer with an external (non-V8-managed) backing store, even with | ||
| // EmptyDeleter. Workaround: use V8-managed SAB memory so V8 allocates and owns the state | ||
| // block. This avoids the crash. The ring_buf_t (mmap region) is intentionally leaked until | ||
| // an upstream V8 fix is available. | ||
| // | ||
| // The state block is zero-initialised by V8 (WRITE_INDEX and READ_INDEX start at 0). | ||
| v8::Isolate *isolate = info.GetIsolate(); | ||
| auto sab = v8::SharedArrayBuffer::New(isolate, STATE_BYTES); | ||
| void *state_data = nullptr; | ||
| napi_value sab; | ||
| if (node_api_create_sharedarraybuffer(env, STATE_BYTES, &state_data, &sab) != napi_ok) | ||
| { | ||
| auto bs = sab->GetBackingStore(); | ||
| uint32_t *s = static_cast<uint32_t *>(bs->Data()); | ||
| s[MAGIC1_INDEX] = MAGIC1; | ||
| s[MAGIC2_INDEX] = MAGIC2; | ||
| uintptr_t p = reinterpret_cast<uintptr_t>(rb); | ||
| s[PTR_LO_INDEX] = static_cast<uint32_t>(p & 0xFFFFFFFFu); | ||
| s[PTR_HI_INDEX] = static_cast<uint32_t>(static_cast<uint64_t>(p) >> 32); | ||
| ring_buf_free(rb); | ||
| napi_throw_error(env, nullptr, "ring_alloc: failed to create SharedArrayBuffer"); | ||
| return nullptr; | ||
| } | ||
| info.GetReturnValue().Set(sab); | ||
| // TODO (fix): We are leaking the initial ring_buf_t reference. We are missing | ||
| // napi_create_external_shared_array_buffer which would allow us to attach a | ||
| // finalizer to the SAB itself. | ||
| uint32_t *s = static_cast<uint32_t *>(state_data); | ||
| s[MAGIC1_INDEX] = MAGIC1; | ||
| s[MAGIC2_INDEX] = MAGIC2; | ||
| uintptr_t p = reinterpret_cast<uintptr_t>(rb); | ||
| s[PTR_LO_INDEX] = static_cast<uint32_t>(p & 0xFFFFFFFFu); | ||
| s[PTR_HI_INDEX] = static_cast<uint32_t>(static_cast<uint64_t>(p) >> 32); | ||
| return sab; | ||
| } | ||
@@ -519,38 +496,32 @@ | ||
| // Returns an ArrayBuffer backed by the double-mapped data region (2 * size | ||
| // bytes). Bumps the native refcount; data_deleter releases it when the | ||
| // ArrayBuffer (and all views derived from it) is GC'd in this isolate. | ||
| NAN_METHOD(ring_get_array_buffer) | ||
| // bytes). Bumps the native refcount; ab_finalizer releases it when the | ||
| // ArrayBuffer is collected or the environment closes. | ||
| static napi_value ring_get_array_buffer(napi_env env, napi_callback_info info) | ||
| { | ||
| if (info.Length() < 1) | ||
| size_t argc = 1; | ||
| napi_value args[1]; | ||
| napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); | ||
| if (argc < 1) | ||
| { | ||
| Nan::ThrowTypeError("ring_get_array_buffer: expected 1 argument"); | ||
| return; | ||
| napi_throw_type_error(env, nullptr, "ring_get_array_buffer: expected 1 argument"); | ||
| return nullptr; | ||
| } | ||
| ring_buf_t *rb = rb_from_handle(info[0], "ring_get_array_buffer"); | ||
| ring_buf_t *rb = rb_from_handle(env, args[0], "ring_get_array_buffer"); | ||
| if (!rb) | ||
| return; | ||
| return nullptr; | ||
| ring_buf_acquire(rb); | ||
| v8::Isolate *isolate = info.GetIsolate(); | ||
| napi_value ab; | ||
| if (napi_create_external_arraybuffer(env, rb->addr, rb->size * 2, ab_finalizer, rb, &ab) != | ||
| napi_ok) | ||
| { | ||
| ring_buf_release(rb); | ||
| napi_throw_error(env, nullptr, "ring_get_array_buffer: failed to create ArrayBuffer"); | ||
| return nullptr; | ||
| } | ||
| // Use EmptyDeleter: the ring_buf_t owns the mmap'd memory, not V8. | ||
| // ring_buf_release is called from either ab_cleanup_hook (worker teardown) | ||
| // or ab_weak_callback (normal GC), whichever fires first. | ||
| // auto backing = v8::ArrayBuffer::NewBackingStore(rb->addr, rb->size * 2, data_deleter, rb); | ||
| auto backing = v8::ArrayBuffer::NewBackingStore( | ||
| rb->addr, rb->size * 2, v8::BackingStore::EmptyDeleter, nullptr); | ||
| auto ab = v8::ArrayBuffer::New(isolate, std::move(backing)); | ||
| // Register a cleanup hook so the AB is detached before ArrayBufferSweeper::ReleaseAll | ||
| // runs during worker-thread teardown (workaround for V8 14.x bug). | ||
| AbRef *ref = new AbRef{}; | ||
| ref->rb = rb; | ||
| ref->isolate = isolate; | ||
| ref->global.Reset(isolate, ab); | ||
| ref->global.SetWeak(ref, ab_weak_callback, v8::WeakCallbackType::kParameter); | ||
| node::AddEnvironmentCleanupHook(isolate, ab_cleanup_hook, ref); | ||
| info.GetReturnValue().Set(ab); | ||
| return ab; | ||
| } | ||
@@ -564,15 +535,27 @@ | ||
| // returns true and the access is aligned. | ||
| NAN_METHOD(ring_is_int32_lock_free) | ||
| static napi_value ring_is_int32_lock_free(napi_env env, napi_callback_info /*info*/) | ||
| { | ||
| info.GetReturnValue().Set(std::atomic<uint32_t>::is_always_lock_free); | ||
| napi_value result; | ||
| napi_get_boolean(env, std::atomic<uint32_t>::is_always_lock_free, &result); | ||
| return result; | ||
| } | ||
| // NODE_MODULE_INIT() registers a context-aware addon that can be loaded in | ||
| // both the main thread and worker threads. NODE_MODULE() (non-context-aware) | ||
| // would fail with "Module did not self-register" in worker threads. | ||
| NODE_MODULE_INIT() | ||
| // NAPI_MODULE_INIT registers a context-aware addon loadable in both the main | ||
| // thread and worker threads. | ||
| NAPI_MODULE_INIT() | ||
| { | ||
| NAN_EXPORT(exports, ring_alloc); | ||
| NAN_EXPORT(exports, ring_get_array_buffer); | ||
| NAN_EXPORT(exports, ring_is_int32_lock_free); | ||
| napi_value fn; | ||
| napi_create_function(env, "ring_alloc", NAPI_AUTO_LENGTH, ring_alloc, nullptr, &fn); | ||
| napi_set_named_property(env, exports, "ring_alloc", fn); | ||
| napi_create_function(env, "ring_get_array_buffer", NAPI_AUTO_LENGTH, ring_get_array_buffer, | ||
| nullptr, &fn); | ||
| napi_set_named_property(env, exports, "ring_get_array_buffer", fn); | ||
| napi_create_function(env, "ring_is_int32_lock_free", NAPI_AUTO_LENGTH, ring_is_int32_lock_free, | ||
| nullptr, &fn); | ||
| napi_set_named_property(env, exports, "ring_is_int32_lock_free", fn); | ||
| return exports; | ||
| } |
Sorry, the diff of this file is not supported yet
1
-50%55691
-2.26%- Removed
- Removed