diff --git a/packages/emnapi/include/node/emnapi.h b/packages/emnapi/include/node/emnapi.h index 4bd7065e..7899dc2b 100644 --- a/packages/emnapi/include/node/emnapi.h +++ b/packages/emnapi/include/node/emnapi.h @@ -90,6 +90,31 @@ EMNAPI_EXTERN void emnapi_debug( int type ); +/** + * Get the handle for an external SharedArrayBuffer. + * This function must be called on the emnapi main thread. + */ +EMNAPI_EXTERN +napi_status emnapi_get_external_sharedarraybuffer_handle( + napi_env env, + napi_value sharedarraybuffer, + void** handle); + +/** + * Acquire a reference to an external SharedArrayBuffer handle. + * This function may be called from any thread. + */ +EMNAPI_EXTERN +void emnapi_acquire_external_sharedarraybuffer(void* handle); + +/** + * Release a reference to an external SharedArrayBuffer handle. + * When the refcount reaches zero, finalize_cb is invoked. + * This function may be called from any thread. + */ +EMNAPI_EXTERN +void emnapi_release_external_sharedarraybuffer(void* handle); + #define DEBUGGER_LOG(str, value, type) \ emnapi_debug(__FILE__, __LINE__, str, value, type) diff --git a/packages/emnapi/include/node/js_native_api.h b/packages/emnapi/include/node/js_native_api.h index 251d8c45..edb7b669 100644 --- a/packages/emnapi/include/node/js_native_api.h +++ b/packages/emnapi/include/node/js_native_api.h @@ -443,6 +443,16 @@ napi_create_external_arraybuffer(napi_env env, node_api_basic_finalize finalize_cb, void* finalize_hint, napi_value* result); +#ifdef NAPI_EXPERIMENTAL +#define NODE_API_EXPERIMENTAL_HAS_CREATE_EXTERNAL_SHAREDARRAYBUFFER +NAPI_EXTERN napi_status NAPI_CDECL +node_api_create_external_sharedarraybuffer(napi_env env, + void* external_data, + size_t byte_length, + node_api_noenv_finalize finalize_cb, + void* finalize_hint, + napi_value* result); +#endif // NAPI_EXPERIMENTAL #endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED NAPI_EXTERN napi_status NAPI_CDECL napi_get_arraybuffer_info( napi_env env, napi_value arraybuffer, void** data, size_t* byte_length); diff --git a/packages/emnapi/include/node/js_native_api_types.h b/packages/emnapi/include/node/js_native_api_types.h index 22d85564..ac30a84f 100644 --- a/packages/emnapi/include/node/js_native_api_types.h +++ b/packages/emnapi/include/node/js_native_api_types.h @@ -188,6 +188,10 @@ typedef void(NAPI_CDECL* node_api_nogc_finalize)(node_api_nogc_env env, #endif typedef node_api_nogc_finalize node_api_basic_finalize; +// A finalizer that can be called from any thread and at any time. +typedef void(NAPI_CDECL* node_api_noenv_finalize)(void* finalize_data, + void* finalize_hint); + typedef struct { // One of utf8name or name should be NULL. const char* utf8name; diff --git a/packages/emnapi/src/core/index.ts b/packages/emnapi/src/core/index.ts index 1234e1ee..ca328d9a 100644 --- a/packages/emnapi/src/core/index.ts +++ b/packages/emnapi/src/core/index.ts @@ -3,7 +3,7 @@ import * as initMod from './init' import * as asyncMod from './async' import * as memoryMod from './memory' -import { emnapiExternalMemory } from '../memory' +import { emnapiExternalMemory, emnapiExternalSAB } from '../memory' import { emnapiString } from '../string' import * as utilMod from '../util' @@ -36,12 +36,14 @@ import * as valueOperationMod from '../value-operation' import * as versionMod from '../version' emnapiExternalMemory.init() +emnapiExternalSAB.init() emnapiString.init() // emnapiTSFN.init() initMod.PThread.init() initMod.napiModule.emnapi.syncMemory = emnapiMod.$emnapiSyncMemory initMod.napiModule.emnapi.getMemoryAddress = emnapiMod.$emnapiGetMemoryAddress +initMod.napiModule.emnapi.acquireExternalSharedArrayBuffer = emnapiMod.$emnapiAcquireExternalSharedArrayBuffer function addImports (mod: any): void { const keys = Object.keys(mod) diff --git a/packages/emnapi/src/emnapi.ts b/packages/emnapi/src/emnapi.ts index 03b53259..f872babb 100644 --- a/packages/emnapi/src/emnapi.ts +++ b/packages/emnapi/src/emnapi.ts @@ -1,7 +1,7 @@ import { emnapiCtx, emnapiEnv, emnapiNodeBinding } from 'emnapi:shared' import { wasmMemory } from 'emscripten:runtime' -import { from64, makeSetValue, makeGetValue } from 'emscripten:parse-tools' -import { type MemoryViewDescriptor, type ArrayBufferPointer, emnapiExternalMemory } from './memory' +import { from64, makeSetValue, makeGetValue, POINTER_SIZE } from 'emscripten:parse-tools' +import { type MemoryViewDescriptor, type ArrayBufferPointer, emnapiExternalMemory, emnapiExternalSAB } from './memory' import { napi_add_finalizer } from './wrap' import { $CHECK_ARG, $PREAMBLE, $CHECK_ENV, $GET_RETURN_STATUS } from './macro' import { emnapiString } from './string' @@ -326,5 +326,97 @@ export function emnapi_get_runtime_version (env: napi_env, version: number): nap return envObject.clearLastError() } -// emnapiImplementHelper('$emnapiSyncMemory', undefined, emnapiSyncMemory, ['$emnapiExternalMemory'], 'syncMemory') +/** + * Get the handle for an external SharedArrayBuffer. + * Must be called on the emnapi main thread. + * @__sig ippp + */ +export function emnapi_get_external_sharedarraybuffer_handle (env: napi_env, sharedarraybuffer: napi_value, handle: Pointer): napi_status { + return $PREAMBLE!(env, (envObject) => { + $CHECK_ARG!(envObject, sharedarraybuffer) + $CHECK_ARG!(envObject, handle) + + from64('handle') + + const jsValue = emnapiCtx.jsValueFromNapiValue(sharedarraybuffer) + if (!emnapiExternalMemory.isSharedArrayBuffer(jsValue)) { + return envObject.setLastError(napi_status.napi_invalid_arg) + } + const metaPtr = emnapiExternalSAB.handleTable.get(jsValue as SharedArrayBuffer) + if (metaPtr === undefined) { + return envObject.setLastError(napi_status.napi_invalid_arg) + } + let p: number = metaPtr + makeSetValue('handle', 0, 'p', '*') + return $GET_RETURN_STATUS!(envObject) + }) +} + +/** + * Acquire a reference to an external SharedArrayBuffer. + * Can be called on any thread. + * @__sig vp + */ +export function emnapi_acquire_external_sharedarraybuffer (handle: void_p): void { + from64('handle') + Atomics.add(new Int32Array(wasmMemory.buffer, handle as number, 1), 0, 1) +} + +/** + * Release a reference to an external SharedArrayBuffer. + * Can be called on any thread. + * @__sig vp + */ +export function emnapi_release_external_sharedarraybuffer (handle: void_p): void { + from64('handle') + emnapiExternalSAB.release(handle as number) +} + +/** + * Acquire a reference to an external SharedArrayBuffer on the current thread. + * Can be called on any thread. Increments refcount and registers in + * the current thread's FinalizationRegistry. + * Exposed as Module.emnapiAcquireExternalSharedArrayBuffer (emscripten) or + * napiModule.emnapi.acquireExternalSharedArrayBuffer (core). + */ +export function $emnapiAcquireExternalSharedArrayBuffer (handle: number, sab?: SharedArrayBuffer): SharedArrayBuffer { + if (sab != null && !emnapiExternalMemory.isSharedArrayBuffer(sab)) { + throw new TypeError('Expected a SharedArrayBuffer') + } + if (!emnapiExternalSAB.registry) { + throw new Error('FinalizationRegistry is not supported in this environment') + } + from64('handle') + const meta = emnapiExternalSAB.readMeta(handle) + let external_data = meta.external_data + from64('external_data') + + if (sab == null) { + sab = new SharedArrayBuffer(meta.byte_length) + new Uint8Array(sab).set(new Uint8Array(wasmMemory.buffer, external_data, meta.byte_length)) + } else { + if (emnapiExternalSAB.handleTable.has(sab)) { + return sab + } + } + + // Increment refcount atomically + Atomics.add(new Int32Array(wasmMemory.buffer, handle, 1), 0, 1) + + // Register in emnapiExternalMemory.table for emnapi_sync_memory support + if (!emnapiExternalMemory.table.has(sab)) { + if (external_data) { + emnapiExternalMemory.table.set(sab, { + address: external_data as number, + ownership: ReferenceOwnership.kUserland, + runtimeAllocated: 0 + }) + } + } + + // Register in handleTable and FinalizationRegistry for this thread + emnapiExternalSAB.handleTable.set(sab, handle) + emnapiExternalSAB.registry.register(sab, handle) + return sab +} // emnapiImplementHelper('$emnapiGetMemoryAddress', undefined, emnapiGetMemoryAddress, ['$emnapiExternalMemory'], 'getMemoryAddress') diff --git a/packages/emnapi/src/memory.ts b/packages/emnapi/src/memory.ts index 8555d59c..2dd328b6 100644 --- a/packages/emnapi/src/memory.ts +++ b/packages/emnapi/src/memory.ts @@ -1,6 +1,6 @@ import { _free, wasmMemory, _malloc } from 'emscripten:runtime' import { emnapiCtx } from 'emnapi:shared' -import { from64, to64 } from 'emscripten:parse-tools' +import { from64, to64, makeDynCall, POINTER_SIZE, makeSetValue, makeGetValue } from 'emscripten:parse-tools' export type ViewConstuctor = new (...args: any[]) => ArrayBufferView @@ -158,3 +158,88 @@ export const emnapiExternalMemory: { return { address: address === 0 ? 0 : (address + view.byteOffset), ownership, runtimeAllocated, view } } } + +export interface ExternalSABInfo { + external_data: number + byte_length: number + finalize_cb: number + finalize_data: number + finalize_hint: number +} + +/** + * Metadata layout in wasm shared memory (allocated with _malloc): + * offset 0: refcount (int32, 4 bytes - for Atomics) + * offset POINTER_SIZE: external_data (pointer) + * offset 2*POINTER_SIZE: byte_length (size_t) + * offset 3*POINTER_SIZE: finalize_cb (pointer) + * offset 4*POINTER_SIZE: finalize_data (pointer) + * offset 5*POINTER_SIZE: finalize_hint (pointer) + * Total: 6 * POINTER_SIZE bytes + */ + +/** + * @__postset + * ``` + * emnapiExternalSAB.init(); + * ``` + */ +export const emnapiExternalSAB: { + registry: FinalizationRegistry | undefined + handleTable: WeakMap + init: () => void + allocMeta: (external_data: number, byte_length: number, finalize_cb: number, finalize_data: number, finalize_hint: number) => number + readMeta: (metaPtr: number) => ExternalSABInfo + release: (metaPtr: number) => void +} = { + registry: undefined, + handleTable: new WeakMap(), + + init: function () { + emnapiExternalSAB.handleTable = new WeakMap() + emnapiExternalSAB.registry = typeof FinalizationRegistry === 'function' + ? new FinalizationRegistry(function (metaPtr: number) { + emnapiExternalSAB.release(metaPtr) + }) + : undefined + }, + + allocMeta: function (external_data: number, byte_length: number, finalize_cb: number, finalize_data: number, finalize_hint: number): number { + const size: number = POINTER_SIZE * 6 + let metaPtr = _malloc(to64('size')) + if (!metaPtr) throw new Error('Out of memory') + from64('metaPtr') + // refcount = 1 + Atomics.store(new Int32Array(wasmMemory.buffer, metaPtr as number, 1), 0, 1) + makeSetValue('metaPtr', POINTER_SIZE, 'external_data', '*') + makeSetValue('metaPtr', POINTER_SIZE * 2, 'byte_length', '*') + makeSetValue('metaPtr', POINTER_SIZE * 3, 'finalize_cb', '*') + makeSetValue('metaPtr', POINTER_SIZE * 4, 'finalize_data', '*') + makeSetValue('metaPtr', POINTER_SIZE * 5, 'finalize_hint', '*') + return metaPtr as number + }, + + readMeta: function (metaPtr: number): ExternalSABInfo { + const external_data = makeGetValue('metaPtr', POINTER_SIZE, '*') + const byte_length = makeGetValue('metaPtr', POINTER_SIZE * 2, '*') + const finalize_cb = makeGetValue('metaPtr', POINTER_SIZE * 3, '*') + const finalize_data = makeGetValue('metaPtr', POINTER_SIZE * 4, '*') + const finalize_hint = makeGetValue('metaPtr', POINTER_SIZE * 5, '*') + return { external_data, byte_length, finalize_cb, finalize_data, finalize_hint } + }, + + release: function (metaPtr: number): void { + const oldRefcount = Atomics.sub(new Int32Array(wasmMemory.buffer, metaPtr, 1), 0, 1) + if (oldRefcount === 1) { + // refcount reached 0, we are the last holder + const info = emnapiExternalSAB.readMeta(metaPtr) + const finalize_cb = info.finalize_cb + if (finalize_cb) { + const finalize_data = info.finalize_data + const finalize_hint = info.finalize_hint + makeDynCall('vpp', 'finalize_cb')(finalize_data, finalize_hint) + } + _free(to64('metaPtr') as number) + } + } +} diff --git a/packages/emnapi/src/value/create.ts b/packages/emnapi/src/value/create.ts index 73730b35..1683b146 100644 --- a/packages/emnapi/src/value/create.ts +++ b/packages/emnapi/src/value/create.ts @@ -2,7 +2,7 @@ import { emnapiCtx, emnapiEnv } from 'emnapi:shared' import { wasmMemory, _malloc } from 'emscripten:runtime' import { from64, makeGetValue, makeSetValue, POINTER_SIZE, to64 } from 'emscripten:parse-tools' import { emnapiString } from '../string' -import { type MemoryViewDescriptor, emnapiExternalMemory } from '../memory' +import { type MemoryViewDescriptor, emnapiExternalMemory, emnapiExternalSAB } from '../memory' import { emnapi_create_memory_view } from '../emnapi' import { napi_add_finalizer } from '../wrap' import { $CHECK_ARG, $CHECK_ENV_NOT_IN_GC, $GET_RETURN_STATUS, $PREAMBLE, $RETURN_STATUS_IF_FALSE } from '../macro' @@ -183,6 +183,66 @@ export function napi_create_external_arraybuffer ( }) } +/** + * @__sig ipppppp + */ +export function node_api_create_external_sharedarraybuffer ( + env: napi_env, + external_data: void_p, + byte_length: size_t, + finalize_cb: napi_finalize, + finalize_hint: void_p, + result: Pointer +): napi_status { + let value: napi_value + + return $PREAMBLE!(env, (envObject) => { + $CHECK_ARG!(envObject, result) + from64('byte_length') + from64('external_data') + from64('result') + + byte_length = byte_length >>> 0 + + if (!external_data) { + byte_length = 0 + } + + if ((external_data as number + byte_length) > wasmMemory.buffer.byteLength) { + throw new RangeError('Memory out of range') + } + if (!emnapiExternalSAB.registry && finalize_cb) { + throw emnapiCtx.createNotSupportWeakRefError('node_api_create_external_sharedarraybuffer', 'Parameter "finalize_cb" must be 0(NULL)') + } + const sharedArrayBuffer = new SharedArrayBuffer(byte_length) + if (byte_length !== 0) { + const u8arr = new Uint8Array(sharedArrayBuffer) + u8arr.set(new Uint8Array(wasmMemory.buffer).subarray(external_data as number, external_data as number + byte_length)) + emnapiExternalMemory.table.set(sharedArrayBuffer, { + address: external_data as number, + ownership: ReferenceOwnership.kUserland, + runtimeAllocated: 0 + }) + } + value = emnapiCtx.napiValueFromJsValue(sharedArrayBuffer) + if (finalize_cb) { + from64('finalize_cb') + from64('finalize_hint') + const metaPtr = emnapiExternalSAB.allocMeta( + external_data as number, + byte_length as number, + finalize_cb as unknown as number, + external_data as number, + finalize_hint as number + ) + emnapiExternalSAB.handleTable.set(sharedArrayBuffer, metaPtr) + emnapiExternalSAB.registry!.register(sharedArrayBuffer, metaPtr) + } + makeSetValue('result', 0, 'value', '*') + return $GET_RETURN_STATUS!(envObject) + }) +} + /** * @__sig ipp */ diff --git a/packages/test/CMakeLists.txt b/packages/test/CMakeLists.txt index ba2569d2..45a561c0 100644 --- a/packages/test/CMakeLists.txt +++ b/packages/test/CMakeLists.txt @@ -357,6 +357,13 @@ add_test("number" "./number/binding.c;./number/test_null.c" OFF) add_test("symbol" "./symbol/binding.c" OFF) add_test("typedarray" "./typedarray/binding.c" OFF) add_test("sharedarraybuffer" "./sharedarraybuffer/binding.c" OFF) +if(IS_EMSCRIPTEN) + target_link_options("sharedarraybuffer" PRIVATE "-sEXPORTED_RUNTIME_METHODS=['emnapiInit','ExitStatus','wasmMemory','growMemory','emnapiAcquireExternalSharedArrayBuffer']") +endif() +if(IS_EMSCRIPTEN OR IS_WASI_THREADS) + add_test("sharedarraybuffer_mt" "./sharedarraybuffer/binding.c" ON) + target_compile_definitions("sharedarraybuffer_mt" PRIVATE "EMNAPI_SHAREDARRAYBUFFER_MT=1") +endif() add_test("buffer" "./buffer/binding.c" OFF) target_compile_definitions("buffer" PRIVATE "NAPI_VERSION=10") add_test("buffer_finalizer" "./buffer_finalizer/binding.c" OFF) diff --git a/packages/test/script/test.js b/packages/test/script/test.js index a415e1a8..6e549be1 100644 --- a/packages/test/script/test.js +++ b/packages/test/script/test.js @@ -35,6 +35,7 @@ const pthread = [ 'string/string-pthread.test.js', 'uv_threadpool_size/**/*', 'trap_in_thread/**/*', + 'sharedarraybuffer/sharedarraybuffer_mt.test.js', ] if (process.env.EMNAPI_TEST_NATIVE) { diff --git a/packages/test/sharedarraybuffer/binding.c b/packages/test/sharedarraybuffer/binding.c index 48ca143e..8e2a246a 100644 --- a/packages/test/sharedarraybuffer/binding.c +++ b/packages/test/sharedarraybuffer/binding.c @@ -1,12 +1,150 @@ #define NAPI_EXPERIMENTAL +#define NODE_API_EXPERIMENTAL_NO_WARNING #include #include "../common.h" #include "../entry_point.h" +#if defined(EMNAPI_SHAREDARRAYBUFFER_MT) +#include +#endif + #ifdef __wasm__ #include "emnapi.h" #endif +static int deleterCallCount = 0; + +static char externalSharedArrayBufferData[1]; + +static void freeExternalSharedArrayBuffer(void* data, void* hint) { + (void)hint; + NODE_API_BASIC_ASSERT_RETURN_VOID( + data == (void*)externalSharedArrayBufferData, + "SharedArrayBuffer points to wrong data"); + deleterCallCount++; +} + +static napi_value newExternalSharedArrayBuffer(napi_env env, + napi_callback_info info) { + napi_value sab; + NODE_API_CALL( + env, + node_api_create_external_sharedarraybuffer(env, + externalSharedArrayBufferData, + 1, + freeExternalSharedArrayBuffer, + NULL, + &sab)); + return sab; +} + +static napi_value getDeleterCallCount(napi_env env, napi_callback_info info) { + napi_value callCount; + NODE_API_CALL(env, napi_create_int32(env, deleterCallCount, &callCount)); + return callCount; +} + +#if defined(EMNAPI_SHAREDARRAYBUFFER_MT) +typedef struct { + void* handle; + int32_t refcountAfterAcquire; + int32_t refcountAfterRelease; +} ExternalSharedArrayBufferThreadContext; + +static int32_t loadExternalSharedArrayBufferRefcount(void* handle) { + return __atomic_load_n((int32_t*)handle, __ATOMIC_SEQ_CST); +} + +static void* acquireAndReleaseExternalSharedArrayBufferThread(void* data) { + ExternalSharedArrayBufferThreadContext* context = + (ExternalSharedArrayBufferThreadContext*)data; + emnapi_acquire_external_sharedarraybuffer(context->handle); + context->refcountAfterAcquire = + loadExternalSharedArrayBufferRefcount(context->handle); + emnapi_release_external_sharedarraybuffer(context->handle); + context->refcountAfterRelease = + loadExternalSharedArrayBufferRefcount(context->handle); + return NULL; +} + +static napi_value acquireAndReleaseExternalSharedArrayBufferInThread( + napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + int64_t rawHandle; + pthread_t thread; + ExternalSharedArrayBufferThreadContext context; + napi_value result; + napi_value refcountAfterAcquire; + napi_value refcountAfterRelease; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + NODE_API_CALL(env, napi_get_value_int64(env, args[0], &rawHandle)); + context.handle = (void*)(uintptr_t)rawHandle; + NODE_API_ASSERT(env, context.handle != NULL, "handle must not be null"); + + context.refcountAfterAcquire = 0; + context.refcountAfterRelease = 0; + + NODE_API_ASSERT(env, + pthread_create(&thread, + NULL, + acquireAndReleaseExternalSharedArrayBufferThread, + &context) == 0, + "Thread creation failed"); + NODE_API_ASSERT(env, pthread_join(thread, NULL) == 0, "Thread join failed"); + + NODE_API_CALL(env, napi_create_object(env, &result)); + NODE_API_CALL(env, napi_create_int32( + env, context.refcountAfterAcquire, &refcountAfterAcquire)); + NODE_API_CALL(env, napi_create_int32( + env, context.refcountAfterRelease, &refcountAfterRelease)); + NODE_API_CALL(env, + napi_set_named_property(env, + result, + "refcountAfterAcquire", + refcountAfterAcquire)); + NODE_API_CALL(env, + napi_set_named_property(env, + result, + "refcountAfterRelease", + refcountAfterRelease)); + return result; +} +#endif + +#ifdef __wasm__ +static napi_value newExternalSharedArrayBufferWithHandle(napi_env env, + napi_callback_info info) { + napi_value sab; + NODE_API_CALL( + env, + node_api_create_external_sharedarraybuffer(env, + externalSharedArrayBufferData, + 1, + freeExternalSharedArrayBuffer, + NULL, + &sab)); + void* handle = NULL; + NODE_API_CALL(env, + emnapi_get_external_sharedarraybuffer_handle(env, sab, &handle)); + + napi_value result; + NODE_API_CALL(env, napi_create_object(env, &result)); + + NODE_API_CALL(env, napi_set_named_property(env, result, "sab", sab)); + + napi_value handleValue; + NODE_API_CALL(env, napi_create_int64(env, (int64_t)(uintptr_t)handle, &handleValue)); + NODE_API_CALL(env, napi_set_named_property(env, result, "handle", handleValue)); + + return result; +} +#endif + static napi_value TestIsSharedArrayBuffer(napi_env env, napi_callback_info info) { size_t argc = 1; @@ -122,6 +260,18 @@ napi_value Init(napi_env env, napi_value exports) { TestGetSharedArrayBufferInfo), DECLARE_NODE_API_PROPERTY("TestSharedArrayBufferData", TestSharedArrayBufferData), + DECLARE_NODE_API_PROPERTY("newExternalSharedArrayBuffer", + newExternalSharedArrayBuffer), + DECLARE_NODE_API_PROPERTY("getDeleterCallCount", + getDeleterCallCount), +#ifdef __wasm__ + DECLARE_NODE_API_PROPERTY("newExternalSharedArrayBufferWithHandle", + newExternalSharedArrayBufferWithHandle), +#endif +#if defined(EMNAPI_SHAREDARRAYBUFFER_MT) + DECLARE_NODE_API_PROPERTY("acquireAndReleaseExternalSharedArrayBufferInThread", + acquireAndReleaseExternalSharedArrayBufferInThread), +#endif }; NODE_API_CALL( @@ -133,4 +283,4 @@ napi_value Init(napi_env env, napi_value exports) { return exports; } -EXTERN_C_END \ No newline at end of file +EXTERN_C_END diff --git a/packages/test/sharedarraybuffer/sharedarraybuffer.test.js b/packages/test/sharedarraybuffer/sharedarraybuffer.test.js index efb94d59..cae97cac 100644 --- a/packages/test/sharedarraybuffer/sharedarraybuffer.test.js +++ b/packages/test/sharedarraybuffer/sharedarraybuffer.test.js @@ -1,10 +1,13 @@ /* eslint-disable camelcase */ 'use strict' const assert = require('assert') +const util = require('util') +const tick = util.promisify(require('../tick')) const { load } = require('../util') // eslint-disable-next-line camelcase -module.exports = load('sharedarraybuffer').then(test_sharedarraybuffer => { +const loadPromise = load('sharedarraybuffer') +module.exports = loadPromise.then(async (test_sharedarraybuffer) => { { const sab = new SharedArrayBuffer(16) const ab = new ArrayBuffer(16) @@ -66,4 +69,41 @@ module.exports = load('sharedarraybuffer').then(test_sharedarraybuffer => { test_sharedarraybuffer.TestGetSharedArrayBufferInfo({}) }, { name: 'Error', message: 'Invalid argument' }) } + + // Test node_api_create_external_sharedarraybuffer + { + let sab = test_sharedarraybuffer.newExternalSharedArrayBuffer() + assert(util.types.isSharedArrayBuffer(sab)) + assert.strictEqual(sab.byteLength, 1) + sab = null + global.gc() + await tick(10) + assert.strictEqual(test_sharedarraybuffer.getDeleterCallCount(), 1) + } + + // Test emnapiAcquireExternalSharedArrayBuffer refcount (wasm only) + if (test_sharedarraybuffer.newExternalSharedArrayBufferWithHandle) { + const acquireFn = process.env.EMNAPI_TEST_WASI || process.env.EMNAPI_TEST_WASM32 + ? loadPromise.Module.emnapi.acquireExternalSharedArrayBuffer + : loadPromise.Module.emnapiAcquireExternalSharedArrayBuffer + + ;(function () { + const { sab, handle } = test_sharedarraybuffer.newExternalSharedArrayBufferWithHandle() + assert(util.types.isSharedArrayBuffer(sab)) + assert.strictEqual(sab.byteLength, 1) + assert.strictEqual(typeof handle, 'number') + assert(handle !== 0, 'handle should not be null') + + // Acquire increases refcount; on same thread, both FinalizationRegistry + // callbacks fire when the SAB is GC'd, so finalize is still called. + acquireFn(handle, sab) + acquireFn(handle) + })() + + // Force GC after function scope exits (all local refs are gone) + global.gc() + await tick(10) + // finalize_cb should have been called exactly once more + assert.strictEqual(test_sharedarraybuffer.getDeleterCallCount(), 2) + } }) diff --git a/packages/test/sharedarraybuffer/sharedarraybuffer_mt.test.js b/packages/test/sharedarraybuffer/sharedarraybuffer_mt.test.js new file mode 100644 index 00000000..fafd824f --- /dev/null +++ b/packages/test/sharedarraybuffer/sharedarraybuffer_mt.test.js @@ -0,0 +1,49 @@ +/* eslint-disable camelcase */ +'use strict' +const assert = require('assert') +const util = require('util') +const tick = util.promisify(require('../tick')) +const { load } = require('../util') + +const loadPromise = load('sharedarraybuffer_mt') + +module.exports = loadPromise.then(async (binding) => { + // Basic external SAB test (same as single-thread) + { + let sab = binding.newExternalSharedArrayBuffer() + assert(util.types.isSharedArrayBuffer(sab)) + assert.strictEqual(sab.byteLength, 1) + sab = null + global.gc() + await tick(10) + assert.strictEqual(binding.getDeleterCallCount(), 1) + } + + // Cross-thread refcount test + await (async function () { + const { sab, handle } = binding.newExternalSharedArrayBufferWithHandle() + assert(util.types.isSharedArrayBuffer(sab)) + assert.strictEqual(sab.byteLength, 1) + assert(handle !== 0, 'handle should not be null') + + const threadResult = binding.acquireAndReleaseExternalSharedArrayBufferInThread(handle) + assert.deepStrictEqual(threadResult, { + refcountAfterAcquire: 2, + refcountAfterRelease: 1 + }) + + // finalize should NOT have been called yet because the main thread still holds SAB. + assert.strictEqual(binding.getDeleterCallCount(), 1, + 'finalize should not fire while main holds reference') + })() + + // After the async IIFE returns, `sab` is out of scope. Force GC so the + // original JS-owned reference releases the final native refcount. + global.gc() + await tick(10) + global.gc() + await tick(10) + + assert.strictEqual(binding.getDeleterCallCount(), 2, + 'finalize should fire after all references are released') +})