blob: d817266feb5bf1bcf1a8b5eaf799c41e82f1ebf0 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Classes to manipulate Wasm memories.
*/
import { Pointer, PtrOffset, SizeOf } from "./ctypes";
import { Disposable } from "./types";
import { StringToUint8Array } from "./support";
import * as ctypes from "./ctypes";
const CALL_STACK_CACHE_LIMIT_BYTES = 64 * 1024;
// TVMWasmAllocSpace takes a signed int and rounds `(size + 7) / 8` in C++.
// Keep the input at the largest aligned value that cannot overflow that sum.
const MAX_WASM_ALLOCATION_BYTES = 0x7ffffff8;
export interface WasmByteArraySource {
buffer: ArrayBuffer | SharedArrayBuffer;
byteOffset: number;
byteLength: number;
}
class WasmAllocation implements Disposable {
private ptr: Pointer;
private freeSpace: ctypes.FTVMWasmFreeSpace;
constructor(ptr: Pointer, freeSpace: ctypes.FTVMWasmFreeSpace) {
this.ptr = ptr;
this.freeSpace = freeSpace;
}
dispose(): void {
if (this.ptr !== 0) {
this.freeSpace(this.ptr);
this.ptr = 0;
}
}
}
/**
* Wasm Memory wrapper to perform JS side raw memory access.
*/
export class Memory {
memory: WebAssembly.Memory;
wasm32 = true;
private buffer: ArrayBuffer | SharedArrayBuffer;
private viewU8: Uint8Array;
private viewU16: Uint16Array;
private viewI32: Int32Array;
private viewU32: Uint32Array;
private viewF32: Float32Array;
private viewF64: Float64Array;
constructor(memory: WebAssembly.Memory) {
this.memory = memory;
this.buffer = this.memory.buffer;
this.viewU8 = new Uint8Array(this.buffer);
this.viewU16 = new Uint16Array(this.buffer);
this.viewI32 = new Int32Array(this.buffer);
this.viewU32 = new Uint32Array(this.buffer);
this.viewF32 = new Float32Array(this.buffer);
this.viewF64 = new Float64Array(this.buffer);
}
loadU8(ptr: Pointer): number {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
return this.viewU8[ptr >> 0];
}
loadU16(ptr: Pointer): number {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
return this.viewU16[ptr >> 1];
}
loadU32(ptr: Pointer): number {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
return this.viewU32[ptr >> 2];
}
loadI32(ptr: Pointer): number {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
return this.viewI32[ptr >> 2];
}
loadI64(ptr: Pointer): number {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
const base = ptr >> 2;
// assumes little endian, for now truncate high.
return this.viewI32[base];
}
loadU64(ptr: Pointer): number {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
// WebAssembly is little-endian. Reject values JavaScript cannot represent exactly.
const base = ptr >> 2;
const value = this.viewU32[base] + this.viewU32[base + 1] * 0x100000000;
if (!Number.isSafeInteger(value)) {
throw new Error("Cannot represent uint64 value as a JavaScript number");
}
return value;
}
loadF32(ptr: Pointer): number {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
return this.viewF32[ptr >> 2];
}
loadF64(ptr: Pointer): number {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
return this.viewF64[ptr >> 3];
}
loadPointer(ptr: Pointer): Pointer {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
if (this.wasm32) {
return this.loadU32(ptr);
} else {
return this.loadI64(ptr);
}
}
loadUSize(ptr: Pointer): Pointer {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
if (this.wasm32) {
return this.loadU32(ptr);
} else {
return this.loadI64(ptr);
}
}
sizeofPtr(): number {
return this.wasm32 ? SizeOf.I32 : SizeOf.I64;
}
/**
* Load raw bytes from ptr.
* @param ptr The head address
* @param numBytes The number
*/
loadRawBytes(ptr: Pointer, numBytes: number): Uint8Array {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
const result = new Uint8Array(numBytes);
result.set(this.viewU8.subarray(ptr, ptr + numBytes));
return result;
}
/**
* Return a borrowed view of raw bytes in Wasm memory.
*
* The returned view aliases the current WebAssembly.Memory buffer and must
* not be retained across a call that can grow the memory.
*
* @param ptr The head address.
* @param numBytes The number of bytes.
*/
viewRawBytes(ptr: Pointer, numBytes: number): Uint8Array {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
if (!Number.isSafeInteger(ptr) || ptr < 0) {
throw new Error(`Invalid Wasm memory pointer: ${ptr}`);
}
if (!Number.isSafeInteger(numBytes) || numBytes < 0) {
throw new Error(`Invalid Wasm memory byte length: ${numBytes}`);
}
const end = ptr + numBytes;
if (!Number.isSafeInteger(end) || end > this.viewU8.byteLength) {
throw new Error(
`Wasm memory range [${ptr}, ${end}) exceeds memory size ` +
`${this.viewU8.byteLength}`,
);
}
return this.viewU8.subarray(ptr, end);
}
/**
* Load null-terminated C-string from ptr.
* @param ptr The head address
*/
loadCString(ptr: Pointer): string {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
// NOTE: the views are still valid for read.
const ret = [];
let ch = 1;
while (ch != 0) {
ch = this.viewU8[ptr];
if (ch != 0) {
ret.push(String.fromCharCode(ch));
}
++ptr;
}
return ret.join("");
}
/**
* Store raw bytes to the ptr.
* @param ptr The head address.
* @param bytes The bytes content.
*/
storeRawBytes(ptr: Pointer, bytes: Uint8Array): void {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
this.viewU8.set(bytes, ptr);
}
// the following functions are related to TVM FFI
/**
* Load the object type index from the object handle.
* @param objectHandle The handle of the object.
* @returns The object type index.
*/
loadObjectTypeIndex(objectHandle: Pointer): number {
// The object layout is [ref_counter (i64), type_index (i32), ...].
return this.loadI32(objectHandle + SizeOf.I64);
}
/**
* Load the type key from the type info pointer.
* @param typeInfoPtr The pointer to the type info.
* @returns The type key.
*/
loadTypeInfoTypeKey(typeInfoPtr: Pointer): string {
const typeKeyPtr = typeInfoPtr + 2 * SizeOf.I32;
return this.loadByteArrayAsString(typeKeyPtr);
}
/**
* Load small string from value pointer.
* @param ffiAnyPtr The pointer to the value.
* @returns The small string.
*/
loadSmallStr(ffiAnyPtr: Pointer): string {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
const sizePtr = ffiAnyPtr + SizeOf.I32;
const length = this.loadU32(sizePtr);
const dataPtr = ffiAnyPtr + SizeOf.I32 + SizeOf.I32;
const ret = [];
for (let i = 0; i < length; i++) {
ret.push(String.fromCharCode(this.viewU8[dataPtr + i]));
}
return ret.join("");
}
/**
* Load small bytes from value pointer.
* @param ffiAnyPtr
*/
loadSmallBytes(ffiAnyPtr: Pointer): Uint8Array {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
const sizePtr = ffiAnyPtr + SizeOf.I32;
const length = this.loadU32(sizePtr);
const dataPtr = ffiAnyPtr + SizeOf.I32 + SizeOf.I32;
const result = new Uint8Array(length);
result.set(this.viewU8.subarray(dataPtr, dataPtr + length));
return result;
}
/**
* Load bytearray as string from ptr.
* @param byteArrayPtr The head address of the bytearray.
*/
loadByteArrayAsString(byteArrayPtr: Pointer): string {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
const ptr = this.loadPointer(byteArrayPtr);
const length = this.loadUSize(byteArrayPtr + this.sizeofPtr());
// NOTE: the views are still valid for read.
const ret = [];
for (let i = 0; i < length; i++) {
ret.push(String.fromCharCode(this.viewU8[ptr + i]));
}
return ret.join("");
}
/**
* Load bytearray as bytes from ptr.
* @param byteArrayPtr The head address of the bytearray.
*/
loadByteArrayAsBytes(byteArrayPtr: Pointer): Uint8Array {
if (this.buffer != this.memory.buffer) {
this.updateViews();
}
const ptr = this.loadPointer(byteArrayPtr);
const length = this.loadUSize(byteArrayPtr + this.sizeofPtr());
const result = new Uint8Array(length);
result.set(this.viewU8.subarray(ptr, ptr + length));
return result;
}
// private functions
/**
* Update memory view after the memory growth.
*/
private updateViews(): void {
this.buffer = this.memory.buffer;
this.viewU8 = new Uint8Array(this.buffer);
this.viewU16 = new Uint16Array(this.buffer);
this.viewI32 = new Int32Array(this.buffer);
this.viewU32 = new Uint32Array(this.buffer);
this.viewF32 = new Float32Array(this.buffer);
this.viewF64 = new Float64Array(this.buffer);
}
}
/**
* Auxiliary call stack for the FFI calls.
*
* Lifecyle of a call stack.
* - Calls into allocXX to allocate space, mixed with storeXXX to store data.
* - Calls into ptrFromOffset, no further allocation(as ptrFromOffset can change),
* can still call into storeXX
* - Calls into commitToWasmMemory once.
* - reset.
*/
export class CachedCallStack implements Disposable {
/** List of temporay arguments that can be disposed during reset. */
tempArgs: Array<Disposable> = [];
private memory: Memory;
private cAllocSpace: ctypes.FTVMWasmAllocSpace;
private cFreeSpace: ctypes.FTVMWasmFreeSpace;
private buffer: ArrayBuffer;
private viewU8: Uint8Array;
private viewI32: Int32Array;
private viewU32: Uint32Array;
private viewF64: Float64Array;
private stackTop: PtrOffset = 0;
private basePtr: Pointer = 0;
private addressToSetTargetValue: Array<[PtrOffset, PtrOffset]> = [];
constructor(
memory: Memory,
allocSpace: ctypes.FTVMWasmAllocSpace,
freeSpace: ctypes.FTVMWasmFreeSpace
) {
const initCallStackSize = 128;
this.memory = memory;
this.cAllocSpace = allocSpace;
this.cFreeSpace = freeSpace;
this.buffer = new ArrayBuffer(initCallStackSize);
this.basePtr = this.cAllocSpace(initCallStackSize);
if (this.basePtr === 0) {
throw new Error(`Cannot allocate ${initCallStackSize} bytes in Wasm memory`);
}
this.viewU8 = new Uint8Array(this.buffer);
this.viewI32 = new Int32Array(this.buffer);
this.viewU32 = new Uint32Array(this.buffer);
this.viewF64 = new Float64Array(this.buffer);
this.updateViews();
}
dispose(): void {
this.reset();
if (this.basePtr != 0) {
this.cFreeSpace(this.basePtr);
this.basePtr = 0;
}
}
/**
* Rest the call stack so that it can be reused again.
*/
reset(): void {
this.stackTop = 0;
this.addressToSetTargetValue.length = 0;
while (this.tempArgs.length != 0) {
(this.tempArgs.pop() as Disposable).dispose();
}
}
/**
* Commit all the cached data to WasmMemory.
* This function can only be called once.
* No further store function should be called.
*
* @param nbytes Number of bytes to be stored.
*/
commitToWasmMemory(nbytes: number = this.stackTop): void {
// commit all pointer values.
while (this.addressToSetTargetValue.length != 0) {
const [targetOffset, valueOffset] = this.addressToSetTargetValue.pop() as [
number,
number
];
this.storePtr(targetOffset, this.ptrFromOffset(valueOffset));
}
this.memory.storeRawBytes(this.basePtr, this.viewU8.subarray(0, nbytes));
}
/**
* Allocate space by number of bytes
* @param nbytes Number of bytes.
* Note: This function always allocate space that aligns to 64bit.
*/
allocRawBytes(nbytes: number): PtrOffset {
// always aligns to 64bit
nbytes = this.alignToEight(nbytes);
const requiredSize = this.checkedAdd(this.stackTop, nbytes);
if (requiredSize > this.buffer.byteLength) {
const doubledSize = Math.min(
this.buffer.byteLength * 2,
MAX_WASM_ALLOCATION_BYTES
);
let newSize = Math.max(doubledSize, requiredSize);
if (this.buffer.byteLength < CALL_STACK_CACHE_LIMIT_BYTES &&
requiredSize <= CALL_STACK_CACHE_LIMIT_BYTES) {
newSize = Math.min(newSize, CALL_STACK_CACHE_LIMIT_BYTES);
} else if (this.buffer.byteLength <= CALL_STACK_CACHE_LIMIT_BYTES &&
requiredSize > CALL_STACK_CACHE_LIMIT_BYTES) {
// Headers and non-byte arguments may cross the soft cache cap. Grow
// only to the requested size instead of doubling past the cap.
newSize = requiredSize;
}
const newBuffer = new ArrayBuffer(newSize);
new Uint8Array(newBuffer).set(this.viewU8);
const newBasePtr = this.cAllocSpace(newSize);
if (newBasePtr === 0) {
throw new Error(`Cannot allocate ${newSize} bytes in Wasm memory`);
}
this.cFreeSpace(this.basePtr);
this.basePtr = newBasePtr;
this.buffer = newBuffer;
this.updateViews();
}
const retOffset = this.stackTop;
this.stackTop = requiredSize;
return retOffset;
}
/**
* Allocate space for pointers.
* @param count Number of pointers.
* @returns The allocated pointer array.
*/
allocPtrArray(count: number): PtrOffset {
return this.allocRawBytes(this.memory.sizeofPtr() * count);
}
/**
* Get the real pointer from offset values.
* Note that the returned value becomes obsolete if alloc is called on the stack.
* @param offset The allocated offset.
*/
ptrFromOffset(offset: PtrOffset): Pointer {
return this.basePtr + offset;
}
// Store APIs
storePtr(offset: PtrOffset, value: Pointer): void {
if (this.memory.wasm32) {
this.storeU32(offset, value);
} else {
this.storeI64(offset, value);
}
}
storeUSize(offset: PtrOffset, value: Pointer): void {
if (this.memory.wasm32) {
this.storeU32(offset, value);
} else {
this.storeI64(offset, value);
}
}
storeI32(offset: PtrOffset, value: number): void {
this.viewI32[offset >> 2] = value;
}
storeU32(offset: PtrOffset, value: number): void {
this.viewU32[offset >> 2] = value;
}
storeI64(offset: PtrOffset, value: number): void {
// For now, just store as 32bit
// NOTE: wasm always uses little endian.
const low = value & 0xffffffff;
const base = offset >> 2;
this.viewI32[base] = low;
// sign extend
this.viewI32[base + 1] = value < 0 ? -1 : 0;
}
storeF64(offset: PtrOffset, value: number): void {
this.viewF64[offset >> 3] = value;
}
storeRawBytes(offset: PtrOffset, bytes: Uint8Array): void {
this.viewU8.set(bytes, offset);
}
/**
* Allocate a byte array for a string and return the offset of the byte array.
* @param data The string to allocate.
* @returns The offset of the byte array.
*/
allocByteArrayForString(data: string): PtrOffset {
const dataUint8: Uint8Array = StringToUint8Array(data);
// Note: size of size_t equals sizeof ptr.
const headerOffset = this.allocRawBytes(this.memory.sizeofPtr() * 2);
const dataOffset = this.allocRawBytes(dataUint8.length);
this.storeUSize(headerOffset + this.memory.sizeofPtr(), data.length);
this.storeRawBytes(dataOffset, dataUint8);
this.addressToSetTargetValue.push([headerOffset, dataOffset]);
return headerOffset;
}
/**
* Allocate then set C-String pointer to the offset.
* This function will call into allocBytes to allocate necessary data.
* The address won't be set immediately(because the possible change of basePtr)
* and will be filled when we commit the data.
*
* @param offset The offset to set ot data pointer.
* @param data The string content.
*/
allocThenSetArgString(offset: PtrOffset, data: string): void {
const dataUint8: Uint8Array = StringToUint8Array(data);
const strOffset = this.allocRawBytes(dataUint8.length);
this.storeRawBytes(strOffset, dataUint8);
this.addressToSetTargetValue.push([offset, strOffset]);
}
/**
* Allocate then set the argument location with a TVMByteArray.
* Allocate new temporary space for bytes.
*
* @param offset The offset to set ot data pointer.
* @param data The string content.
*/
allocThenSetArgBytes(
offset: PtrOffset,
data: Uint8Array,
wasmSource?: WasmByteArraySource
): void {
if (wasmSource === undefined && data.buffer === this.memory.memory.buffer) {
wasmSource = {
buffer: data.buffer,
byteOffset: data.byteOffset,
byteLength: data.byteLength,
};
}
// Note: size of size_t equals sizeof ptr.
const dataLength = wasmSource?.byteLength ?? data.length;
const headerSize = this.alignToEight(this.memory.sizeofPtr() * 2);
const inlineDataSize = this.alignToEight(dataLength);
const cacheSizeWithHeader = this.checkedAdd(this.stackTop, headerSize);
const keepDataInline = cacheSizeWithHeader <= CALL_STACK_CACHE_LIMIT_BYTES &&
inlineDataSize <= CALL_STACK_CACHE_LIMIT_BYTES - cacheSizeWithHeader;
const headerOffset = this.allocRawBytes(this.memory.sizeofPtr() * 2);
this.storeUSize(headerOffset + this.memory.sizeofPtr(), dataLength);
this.addressToSetTargetValue.push([offset, headerOffset]);
if (keepDataInline) {
const dataOffset = this.allocRawBytes(dataLength);
this.storeRawBytes(dataOffset, this.resolveByteSource(data, wasmSource));
this.addressToSetTargetValue.push([headerOffset, dataOffset]);
} else {
const dataPtr = dataLength === 0 ? 0 : this.allocTransientBytes(data, wasmSource);
this.storePtr(headerOffset, dataPtr);
}
}
/** Allocate bytes directly in Wasm memory for the duration of one FFI call. */
private allocTransientBytes(
data: Uint8Array,
wasmSource?: WasmByteArraySource
): Pointer {
const dataLength = wasmSource?.byteLength ?? data.length;
this.checkAllocationSize(dataLength);
const dataPtr = this.cAllocSpace(dataLength);
if (dataPtr === 0) {
throw new Error(`Cannot allocate ${dataLength} bytes in Wasm memory`);
}
const allocation = new WasmAllocation(dataPtr, this.cFreeSpace);
this.tempArgs.push(allocation);
try {
this.memory.storeRawBytes(dataPtr, this.resolveByteSource(data, wasmSource));
} catch (err) {
this.tempArgs.pop();
allocation.dispose();
throw err;
}
return dataPtr;
}
private resolveByteSource(
data: Uint8Array,
wasmSource?: WasmByteArraySource
): Uint8Array {
if (wasmSource !== undefined && wasmSource.buffer !== this.memory.memory.buffer) {
return new Uint8Array(
this.memory.memory.buffer,
wasmSource.byteOffset,
wasmSource.byteLength
);
}
return data;
}
private alignToEight(nbytes: number): number {
this.checkAllocationSize(nbytes);
const remainder = nbytes % 8;
const aligned = remainder === 0 ? nbytes : nbytes + 8 - remainder;
this.checkAllocationSize(aligned);
return aligned;
}
private checkedAdd(lhs: number, rhs: number): number {
const result = lhs + rhs;
this.checkAllocationSize(result);
return result;
}
private checkAllocationSize(nbytes: number): void {
if (!Number.isSafeInteger(nbytes) || nbytes < 0 || nbytes > MAX_WASM_ALLOCATION_BYTES) {
throw new RangeError(
`Wasm allocation size must be an integer between 0 and ${MAX_WASM_ALLOCATION_BYTES}, ` +
`but received ${nbytes}`
);
}
}
/**
* Update internal cache views.
*/
private updateViews(): void {
this.viewU8 = new Uint8Array(this.buffer);
this.viewI32 = new Int32Array(this.buffer);
this.viewU32 = new Uint32Array(this.buffer);
this.viewF64 = new Float64Array(this.buffer);
}
}