| /* |
| * 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 { assert, StringToUint8Array } from "./support"; |
| |
| import * as ctypes from "./ctypes"; |
| |
| /** |
| * 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]; |
| } |
| |
| 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.slice(ptr, ptr + numBytes)); |
| return result; |
| } |
| /** |
| * 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.slice(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.slice(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); |
| 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 { |
| 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; |
| assert(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.slice(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 = ((nbytes + 7) >> 3) << 3; |
| |
| if (this.stackTop + nbytes > this.buffer.byteLength) { |
| const newSize = Math.max( |
| this.buffer.byteLength * 2, |
| this.stackTop + nbytes |
| ); |
| const oldU8 = this.viewU8; |
| this.buffer = new ArrayBuffer(newSize); |
| this.updateViews(); |
| this.viewU8.set(oldU8); |
| if (this.basePtr != 0) { |
| this.cFreeSpace(this.basePtr); |
| } |
| this.basePtr = this.cAllocSpace(newSize); |
| } |
| const retOffset = this.stackTop; |
| this.stackTop += nbytes; |
| 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): void { |
| // Note: size of size_t equals sizeof ptr. |
| const headerOffset = this.allocRawBytes(this.memory.sizeofPtr() * 2); |
| const dataOffset = this.allocRawBytes(data.length); |
| this.storeRawBytes(dataOffset, data); |
| this.storeUSize(headerOffset + this.memory.sizeofPtr(), data.length); |
| |
| this.addressToSetTargetValue.push([offset, headerOffset]); |
| this.addressToSetTargetValue.push([headerOffset, dataOffset]); |
| } |
| |
| /** |
| * 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); |
| } |
| } |