1/* 2 * Copyright (c) 2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15import { float32, int32, int64 } from "@koalaui/common" 16import { pointer, KPointer } from "./InteropTypes" 17import { wrapCallback } from "./InteropOps" 18import { InteropNativeModule } from "./InteropNativeModule" 19import { ResourceHolder, ResourceId } from "../arkts/ResourceManager" 20 21// imports required interfaces (now generation is disabled) 22// import { Resource } from "@arkoala/arkui" 23/** 24 * Value representing possible JS runtime object type. 25 * Must be synced with "enum RuntimeType" in C++. 26 */ 27export enum RuntimeType { 28 UNEXPECTED = -1, 29 NUMBER = 1, 30 STRING = 2, 31 OBJECT = 3, 32 BOOLEAN = 4, 33 UNDEFINED = 5, 34 BIGINT = 6, 35 FUNCTION = 7, 36 SYMBOL = 8, 37 MATERIALIZED = 9, 38} 39 40/** 41 * Value representing object type in serialized data. 42 * Must be synced with "enum Tags" in C++. 43 */ 44export enum Tags { 45 UNDEFINED = 101, 46 INT32 = 102, 47 FLOAT32 = 103, 48 STRING = 104, 49 LENGTH = 105, 50 RESOURCE = 106, 51 OBJECT = 107, 52} 53 54export function runtimeType(value: any): int32 { 55 let type = typeof value 56 if (type == "number") return RuntimeType.NUMBER 57 if (type == "string") return RuntimeType.STRING 58 if (type == "undefined") return RuntimeType.UNDEFINED 59 if (type == "object") return RuntimeType.OBJECT 60 if (type == "boolean") return RuntimeType.BOOLEAN 61 if (type == "bigint") return RuntimeType.BIGINT 62 if (type == "function") return RuntimeType.FUNCTION 63 if (type == "symbol") return RuntimeType.SYMBOL 64 65 throw new Error(`bug: ${value} is ${type}`) 66} 67 68export function isResource(value: unknown): boolean { 69 return value !== undefined 70 && typeof value === 'object' 71 && value !== null 72 && value.hasOwnProperty("bundleName") 73 && value.hasOwnProperty("moduleName") 74} 75 76// Poor man's instanceof, fails on subclasses 77export function isInstanceOf(className: string, value: object | undefined): boolean { 78 return value?.constructor.name === className 79} 80 81export function registerCallback(value: object|undefined): int32 { 82 return wrapCallback((args: Uint8Array, length: int32) => { 83 // TBD: deserialize the callback arguments and call the callback 84 return 42 85 }) 86} 87 88export function registerMaterialized(value: object|undefined): number { 89 // TODO: fix me! 90 return 42 91} 92 93export interface CallbackResource { 94 resourceId: int32 95 hold: pointer 96 release: pointer 97} 98 99/* Serialization extension point */ 100export abstract class CustomSerializer { 101 constructor(protected supported: Array<string>) {} 102 supports(kind: string): boolean { return this.supported.includes(kind) } 103 abstract serialize(serializer: SerializerBase, value: any, kind: string): void 104 next: CustomSerializer | undefined = undefined 105} 106 107export class SerializerBase { 108 private position = 0 109 private buffer: ArrayBuffer 110 private view: DataView 111 112 private static customSerializers: CustomSerializer | undefined = undefined 113 static registerCustomSerializer(serializer: CustomSerializer) { 114 if (SerializerBase.customSerializers == undefined) { 115 SerializerBase.customSerializers = serializer 116 } else { 117 let current = SerializerBase.customSerializers 118 while (current.next != undefined) { current = current.next } 119 current.next = serializer 120 } 121 } 122 constructor() { 123 this.buffer = new ArrayBuffer(96) 124 this.view = new DataView(this.buffer) 125 } 126 public release() { 127 this.releaseResources() 128 this.position = 0 129 } 130 asArray(): Uint8Array { 131 return new Uint8Array(this.buffer) 132 } 133 length(): int32 { 134 return this.position 135 } 136 currentPosition(): int32 { return this.position } 137 138 private checkCapacity(value: int32) { 139 if (value < 1) { 140 throw new Error(`${value} is less than 1`) 141 } 142 let buffSize = this.buffer.byteLength 143 if (this.position > buffSize - value) { 144 const minSize = this.position + value 145 const resizedSize = Math.max(minSize, Math.round(3 * buffSize / 2)) 146 let resizedBuffer = new ArrayBuffer(resizedSize) 147 // TODO: can we grow without new? 148 new Uint8Array(resizedBuffer).set(new Uint8Array(this.buffer)) 149 this.buffer = resizedBuffer 150 this.view = new DataView(resizedBuffer) 151 } 152 } 153 private heldResources: ResourceId[] = [] 154 holdAndWriteCallback(callback: object, hold: KPointer = 0, release: KPointer = 0, call: KPointer = 0, callSync: KPointer = 0): ResourceId { 155 const resourceId = ResourceHolder.instance().registerAndHold(callback) 156 this.heldResources.push(resourceId) 157 this.writeInt32(resourceId) 158 this.writePointer(hold) 159 this.writePointer(release) 160 this.writePointer(call) 161 this.writePointer(callSync) 162 return resourceId 163 } 164 holdAndWriteCallbackForPromiseVoid(hold: KPointer = 0, release: KPointer = 0, call: KPointer = 0, callSync = 0): [Promise<void>, ResourceId] { 165 let resourceId: ResourceId 166 const promise = new Promise<void>((resolve, reject) => { 167 const callback = (err: string[]|undefined) => { 168 if (err !== undefined) 169 reject(err) 170 else 171 resolve() 172 } 173 resourceId = this.holdAndWriteCallback(callback, hold, release, call, callSync) 174 }) 175 return [promise, resourceId] 176 } 177 holdAndWriteCallbackForPromise<T>(hold: KPointer = 0, release: KPointer = 0, call: KPointer = 0): [Promise<T>, ResourceId] { 178 let resourceId: ResourceId 179 const promise = new Promise<T>((resolve, reject) => { 180 const callback = (value: T|undefined, err: string[]|undefined) => { 181 if (err !== undefined) 182 reject(err) 183 else 184 resolve(value!) 185 } 186 resourceId = this.holdAndWriteCallback(callback, hold, release, call) 187 }) 188 return [promise, resourceId] 189 } 190 writeCallbackResource(resource: CallbackResource) { 191 this.writeInt32(resource.resourceId) 192 this.writePointer(resource.hold) 193 this.writePointer(resource.release) 194 } 195 private releaseResources() { 196 for (const resourceId of this.heldResources) 197 InteropNativeModule._ReleaseCallbackResource(resourceId) 198 // todo think about effective array clearing/pushing 199 this.heldResources = [] 200 } 201 writeCustomObject(kind: string, value: any) { 202 let current = SerializerBase.customSerializers 203 while (current) { 204 if (current.supports(kind)) { 205 current.serialize(this, value, kind) 206 return 207 } 208 current = current.next 209 } 210 console.log(`Unsupported custom serialization for ${kind}, write undefined`) 211 this.writeInt8(Tags.UNDEFINED) 212 } 213 writeNumber(value: number|undefined) { 214 this.checkCapacity(5) 215 if (value == undefined) { 216 this.view.setInt8(this.position, Tags.UNDEFINED) 217 this.position++ 218 return 219 } 220 if (value == Math.round(value)) { 221 this.view.setInt8(this.position, Tags.INT32) 222 this.view.setInt32(this.position + 1, value, true) 223 this.position += 5 224 return 225 } 226 this.view.setInt8(this.position, Tags.FLOAT32) 227 this.view.setFloat32(this.position + 1, value, true) 228 this.position += 5 229 } 230 writeInt8(value: int32) { 231 this.checkCapacity(1) 232 this.view.setInt8(this.position, value) 233 this.position += 1 234 } 235 writeInt32(value: int32) { 236 this.checkCapacity(4) 237 this.view.setInt32(this.position, value, true) 238 this.position += 4 239 } 240 writeInt64(value: int64) { 241 this.checkCapacity(8) 242 this.view.setBigInt64(this.position, BigInt(value), true) 243 this.position += 8 244 } 245 writePointer(value: pointer) { 246 this.checkCapacity(8) 247 this.view.setBigInt64(this.position, BigInt(value ?? 0), true) 248 this.position += 8 249 } 250 writeFloat32(value: float32) { 251 this.checkCapacity(4) 252 this.view.setFloat32(this.position, value, true) 253 this.position += 4 254 } 255 writeBoolean(value: boolean|undefined) { 256 this.checkCapacity(1) 257 this.view.setInt8(this.position, value == undefined ? RuntimeType.UNDEFINED : +value) 258 this.position++ 259 } 260 writeFunction(value: object | undefined) { 261 this.writeInt32(registerCallback(value)) 262 } 263 writeString(value: string) { 264 this.checkCapacity(4 + value.length * 4) // length, data 265 let encodedLength = 266 InteropNativeModule._ManagedStringWrite(value, new Uint8Array(this.view.buffer, 0), this.position + 4) 267 this.view.setInt32(this.position, encodedLength, true) 268 this.position += encodedLength + 4 269 } 270 writeBuffer(buffer: ArrayBuffer) { 271 const resourceId = ResourceHolder.instance().registerAndHold(buffer) 272 this.writeCallbackResource({ 273 resourceId, 274 hold: 0, 275 release: 0 276 }) 277 const ptr = InteropNativeModule._GetNativeBufferPointer(buffer) 278 this.writePointer(ptr) 279 this.writeInt64(buffer.byteLength) 280 } 281} 282 283class DateSerializer extends CustomSerializer { 284 constructor() { 285 super(["Date"]) 286 } 287 288 serialize(serializer: SerializerBase, value: object, kind: string): void { 289 serializer.writeString((value as Date).toISOString()) 290 } 291} 292SerializerBase.registerCustomSerializer(new DateSerializer()) 293 294export function unsafeCast<T>(value: unknown) { 295 return value as unknown as T 296} 297