Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/emnapi/include/node/emnapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
10 changes: 10 additions & 0 deletions packages/emnapi/include/node/js_native_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions packages/emnapi/include/node/js_native_api_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion packages/emnapi/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
98 changes: 95 additions & 3 deletions packages/emnapi/src/emnapi.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<void_p>): 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')
87 changes: 86 additions & 1 deletion packages/emnapi/src/memory.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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<number> | undefined
handleTable: WeakMap<SharedArrayBuffer, number>
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)
}
}
}
62 changes: 61 additions & 1 deletion packages/emnapi/src/value/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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_value>
): 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
*/
Expand Down
7 changes: 7 additions & 0 deletions packages/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions packages/test/script/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading