Skip to content
Open
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
11 changes: 10 additions & 1 deletion packages/core/src/emnapi/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,21 @@ export declare interface NapiModule {
filename: string
childThread: boolean
emnapi: {
/**
* Synchronize data between the wasm memory and an ArrayBuffer / view.
*
* Note: when a non-shared wasm memory has grown, a view over the
* detached old buffer is re-created over the current buffer through the
* intrinsic base-class constructor, so for view inputs the returned view
* may be a base-class instance (e.g. `Uint8Array` or `DataView`)
* instead of the subclass that was passed in.
*/
syncMemory<T extends ArrayBuffer | ArrayBufferView> (
js_to_wasm: boolean,
arrayBufferOrView: T,
offset?: number,
len?: number
): T
): T extends ArrayBufferView ? ArrayBufferView : T
getMemoryAddress (arrayBufferOrView: ArrayBuffer | ArrayBufferView): PointerInfo
addSendListener (worker: any): boolean
}
Expand Down
47 changes: 47 additions & 0 deletions packages/core/src/emnapi/sync-memory.type-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Compile-time assertions for the NapiModule['emnapi'].syncMemory return type
// (F12). Under the repo's ES2021 lib a typed array structurally satisfies
// ArrayBuffer, so a buffer-first conditional would hand a subclass input back
// as its subclass type while the runtime returns a base-class view after a
// growth. The signature therefore tests ArrayBufferView FIRST. This file emits
// no runtime code; a reverted conditional makes it fail to typecheck.
import type { NapiModule } from './index'

type SyncMemory = NapiModule['emnapi']['syncMemory']

type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false
type Expect<T extends true> = T

// the actual public syncMemory, resolved through NapiModule
declare const syncMemory: SyncMemory

class MyU8 extends Uint8Array { extra (): number { return 1 } }
class MyDV extends DataView<ArrayBufferLike> { extra (): number { return 1 } }

declare const myU8: MyU8
declare const myDV: MyDV
declare const u8: Uint8Array
declare const ab: ArrayBuffer
declare const sab: SharedArrayBuffer

const rSubTA = syncMemory(true, myU8)
const rSubDV = syncMemory(true, myDV)
const rU8 = syncMemory(true, u8)
const rAB = syncMemory(true, ab)
const rSAB = syncMemory(true, sab)

// @ts-expect-error a typed-array subclass method must NOT be promised on the
// result (the runtime may return a base Uint8Array after growth)
rSubTA.extra()

export type Assertions = [
// typed-array subclass -> wide ArrayBufferView, not the subclass
Expect<Equal<typeof rSubTA, ArrayBufferView>>,
// DataView subclass -> wide ArrayBufferView
Expect<Equal<typeof rSubDV, ArrayBufferView>>,
// base-class view -> wide ArrayBufferView
Expect<Equal<typeof rU8, ArrayBufferView>>,
// ArrayBuffer input keeps its identity (never reconstructed)
Expect<Equal<typeof rAB, ArrayBuffer>>,
// SharedArrayBuffer input keeps its identity
Expect<Equal<typeof rSAB, SharedArrayBuffer>>
]
22 changes: 16 additions & 6 deletions packages/emnapi/src/emnapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ export function emnapi_create_memory_view (
}
const Ctor = viewDescriptor.Ctor
const typedArray = typedarray_type === emnapi_memory_view_type.emnapi_buffer
? emnapiCtx.features.Buffer!.from(wasmMemory.buffer, viewDescriptor.address, viewDescriptor.length)
// capture Buffer.from at creation so a later refreshed view uses it
? emnapiExternalMemory.getBufferFrom()(wasmMemory.buffer, viewDescriptor.address, viewDescriptor.length)
: new Ctor(wasmMemory.buffer, viewDescriptor.address, viewDescriptor.length)
value = emnapiCtx.napiValueFromJsValue(typedArray)
emnapiExternalMemory.wasmMemoryViewTable.set(typedArray, viewDescriptor)
Expand Down Expand Up @@ -155,12 +156,21 @@ export function emnapi_is_node_binding_available (): int {
return emnapiNodeBinding ? 1 : 0
}

/**
* Synchronize data between the wasm memory and an ArrayBuffer / view.
*
* Note: when a non-shared wasm memory has grown, a view over the detached
* old buffer is re-created over the current buffer through the intrinsic
* base-class constructor, so for view inputs the returned view may be a
* base-class instance (e.g. `Uint8Array` or `DataView`) instead of the
* subclass that was passed in.
*/
export function $emnapiSyncMemory<T extends ArrayBufferLike | ArrayBufferView> (
js_to_wasm: boolean,
arrayBufferOrView: T,
offset?: number,
len?: int
): T {
): T extends ArrayBufferView ? ArrayBufferView : T {
offset = offset ?? 0
offset = offset >>> 0
let view: Uint8Array
Expand All @@ -171,7 +181,7 @@ export function $emnapiSyncMemory<T extends ArrayBufferLike | ArrayBufferView> (
len = arrayBufferOrView.byteLength - offset
}
len = len >>> 0
if (len === 0) return arrayBufferOrView
if (len === 0) return arrayBufferOrView as (T extends ArrayBufferView ? ArrayBufferView : T)
view = new Uint8Array(arrayBufferOrView, offset, len)

const wasmMemoryU8 = new Uint8Array(wasmMemory.buffer)
Expand All @@ -181,7 +191,7 @@ export function $emnapiSyncMemory<T extends ArrayBufferLike | ArrayBufferView> (
wasmMemoryU8.set(view, pointer as number)
}

return arrayBufferOrView
return arrayBufferOrView as (T extends ArrayBufferView ? ArrayBufferView : T)
}

if (ArrayBuffer.isView(arrayBufferOrView)) {
Expand All @@ -193,7 +203,7 @@ export function $emnapiSyncMemory<T extends ArrayBufferLike | ArrayBufferView> (
len = latestView.byteLength - offset
}
len = len >>> 0
if (len === 0) return latestView
if (len === 0) return latestView as (T extends ArrayBufferView ? ArrayBufferView : T)
view = new Uint8Array(latestView.buffer, latestView.byteOffset + offset, len)

const wasmMemoryU8 = new Uint8Array(wasmMemory.buffer)
Expand All @@ -203,7 +213,7 @@ export function $emnapiSyncMemory<T extends ArrayBufferLike | ArrayBufferView> (
wasmMemoryU8.set(view, pointer as number)
}

return latestView
return latestView as (T extends ArrayBufferView ? ArrayBufferView : T)
}
throw new TypeError('emnapiSyncMemory expect ArrayBuffer or ArrayBufferView as first parameter')
}
Expand Down
4 changes: 4 additions & 0 deletions packages/emnapi/src/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ export function napi_call_function (
}
const ret = v8func.apply(v8recv, args)
if (result) {
// resolving v BEFORE the write matters: the store target is evaluated
// before the RHS, so an RHS running user JS could otherwise write
// through a stale heap view after a growth
v = emnapiCtx.napiValueFromJsValue(ret)
makeSetValue('result', 0, 'v', '*')
}
Expand Down Expand Up @@ -161,6 +164,7 @@ export function napi_new_instance (
ret = new BoundCtor()
}
if (result) {
// see napi_call_function: resolve v before the write
v = emnapiCtx.napiValueFromJsValue(ret)
makeSetValue('result', 0, 'v', '*')
}
Expand Down
Loading