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, float64, int8, int32, int64, int32BitsFromFloat } from "@koalaui/common" 16import { pointer, KUint8ArrayPtr } from "./InteropTypes" 17import { ResourceId, ResourceHolder } from "./ResourceManager" 18import { KBuffer } from "./buffer" 19import { NativeBuffer } from "./NativeBuffer" 20import { InteropNativeModule } from "./InteropNativeModule" 21 22/** 23 * Value representing possible JS runtime object type. 24 * Must be synced with "enum RuntimeType" in C++. 25 */ 26export class RuntimeType { 27 static UNEXPECTED = -1 28 static NUMBER = 1 29 static STRING = 2 30 static OBJECT = 3 31 static BOOLEAN = 4 32 static UNDEFINED = 5 33 static BIGINT = 6 34 static FUNCTION = 7 35 static SYMBOL = 8 36 static MATERIALIZED = 9 37} 38 39/** 40 * Value representing object type in serialized data. 41 * Must be synced with "enum Tags" in C++. 42 */ 43export class Tags { 44 static UNDEFINED = 101 45 static INT32 = 102 46 static FLOAT32 = 103 47 static STRING = 104 48 static LENGTH = 105 49 static RESOURCE = 106 50 static OBJECT = 107 51} 52 53export function runtimeType(value: Object|String|number|undefined|null): int32 { 54 let type = typeof value 55 if (type == "number") return RuntimeType.NUMBER 56 if (type == "string") return RuntimeType.STRING 57 if (type == "undefined") return RuntimeType.UNDEFINED 58 if (type == "object") return RuntimeType.OBJECT 59 if (type == "boolean") return RuntimeType.BOOLEAN 60 if (type == "bigint") return RuntimeType.BIGINT 61 if (type == "function") return RuntimeType.FUNCTION 62 if (type == "symbol") return RuntimeType.SYMBOL 63 64 throw new Error(`bug: ${value} is ${type}`) 65} 66 67export function registerCallback(value: object): int32 { 68 // TODO: fix me! 69 return 42 70} 71 72function registerMaterialized(value: Object): int32 { 73 // TODO: fix me! 74 return 42 75} 76 77export function isResource(value: Object|undefined): boolean { 78 // TODO: fix me! 79 return false 80} 81 82export function isInstanceOf(className: string, value: Object|undefined): boolean { 83 // TODO: fix me! 84 return false 85} 86 87export interface CallbackResource { 88 resourceId: int32 89 hold: pointer 90 release: pointer 91} 92 93/* Serialization extension point */ 94export abstract class CustomSerializer { 95 protected supported: Array<string> 96 constructor(supported: Array<string>) { 97 this.supported = supported 98 } 99 supports(kind: string): boolean { return this.supported.includes(kind) } 100 abstract serialize(serializer: SerializerBase, value: object, kind: string): void 101 next: CustomSerializer | undefined = undefined 102} 103 104class DateSerializer extends CustomSerializer { 105 constructor() { 106 super(Array.of("Date" as string)) 107 } 108 109 serialize(serializer: SerializerBase, value: object, kind: string): void { 110 serializer.writeString((value as Date).toISOString()) 111 } 112} 113SerializerBase.registerCustomSerializer(new DateSerializer()) 114 115export class SerializerBase { 116 private position = 0 117 private buffer: KBuffer 118 119 private static customSerializers: CustomSerializer | undefined = new DateSerializer() 120 static registerCustomSerializer(serializer: CustomSerializer) { 121 if (SerializerBase.customSerializers == undefined) { 122 SerializerBase.customSerializers = serializer 123 } else { 124 let current = SerializerBase.customSerializers 125 while (current!.next != undefined) { 126 current = current!.next 127 } 128 current!.next = serializer 129 } 130 } 131 132 resetCurrentPosition(): void { this.position = 0 } 133 134 constructor() { 135 this.buffer = new KBuffer(96) 136 } 137 public release() { 138 this.releaseResources() 139 this.position = 0 140 } 141 asArray(): KUint8ArrayPtr { 142 return this.buffer.buffer 143 } 144 length(): int32 { 145 return this.position 146 } 147 currentPosition(): int32 { return this.position } 148 private checkCapacity(value: int32) { 149 if (value < 1) { 150 throw new Error(`${value} is less than 1`) 151 } 152 let buffSize = this.buffer.length 153 if (this.position > buffSize - value) { 154 const minSize = this.position + value 155 const resizedSize = Math.max(minSize, Math.round(3 * buffSize / 2)) 156 let resizedBuffer = new KBuffer(resizedSize as int32) 157 for (let i = 0; i < this.buffer.length; i++) { 158 resizedBuffer.set(i, this.buffer.get(i)) 159 } 160 this.buffer = resizedBuffer 161 } 162 } 163 private heldResources: Array<ResourceId> = new Array<ResourceId>() 164 holdAndWriteCallback(callback: object, hold: pointer = 0, release: pointer = 0, call: pointer = 0, callSync: pointer = 0): ResourceId { 165 const resourceId = ResourceHolder.instance().registerAndHold(callback) 166 this.heldResources.push(resourceId) 167 this.writeInt32(resourceId) 168 this.writePointer(hold) 169 this.writePointer(release) 170 this.writePointer(call) 171 this.writePointer(callSync) 172 return resourceId 173 } 174 holdAndWriteCallbackForPromiseVoid(hold: pointer = 0, release: pointer = 0, call: pointer = 0): [Promise<void>, ResourceId] { 175 let resourceId: ResourceId 176 const promise = new Promise<void>((resolve: (value: PromiseLike<void>) => void, reject: (err: Object|null|undefined) => void) => { 177 const callback = (err: string[]|undefined) => { 178 if (err !== undefined) 179 reject(err!) 180 else 181 resolve(Promise.resolve()) 182 } 183 resourceId = this.holdAndWriteCallback(callback, hold, release, call) 184 }) 185 return [promise, resourceId] 186 } 187 holdAndWriteCallbackForPromise<T>(hold: pointer = 0, release: pointer = 0, call: pointer = 0): [Promise<T>, ResourceId] { 188 let resourceId: ResourceId 189 const promise = new Promise<T>((resolve: (value: T|PromiseLike<T>) => void, reject: (err: Object|null|undefined) => void) => { 190 const callback = (value: T|undefined, err: string[]|undefined) => { 191 if (err !== undefined) 192 reject(err!) 193 else 194 resolve(value!) 195 } 196 resourceId = this.holdAndWriteCallback(callback, hold, release, call) 197 }) 198 return [promise, resourceId] 199 } 200 writeCallbackResource(resource: CallbackResource) { 201 this.writeInt32(resource.resourceId) 202 this.writePointer(resource.hold) 203 this.writePointer(resource.release) 204 } 205 writeResource(resource: object) { 206 const resourceId = ResourceHolder.instance().registerAndHold(resource) 207 this.heldResources.push(resourceId) 208 this.writeInt32(resourceId) 209 } 210 private releaseResources() { 211 for (const resourceId of this.heldResources) 212 InteropNativeModule._ReleaseCallbackResource(resourceId) 213 // todo think about effective array clearing/pushing 214 this.heldResources = new Array<ResourceId>() 215 } 216 writeCustomObject(kind: string, value: object) { 217 let current = SerializerBase.customSerializers 218 while (current) { 219 if (current!.supports(kind)) { 220 current!.serialize(this, value, kind) 221 return 222 } 223 current = current!.next 224 } 225 // console.log(`Unsupported custom serialization for ${kind}, write undefined`) 226 this.writeInt8(Tags.UNDEFINED as int32) 227 } 228 writeFunction(value: Object) { 229 this.writeInt32(registerCallback(value)) 230 } 231 writeTag(tag: int32): void { 232 this.buffer.set(this.position, tag as int8) 233 this.position++ 234 } 235 writeNumber(value: number|undefined) { 236 this.checkCapacity(5) 237 if (value == undefined) { 238 this.writeTag(Tags.UNDEFINED) 239 this.position++ 240 return 241 } 242 if ((value as float64) == Math.round(value)) { 243 this.writeTag(Tags.INT32) 244 this.writeInt32(value as int32) 245 return 246 } else { 247 this.writeTag(Tags.FLOAT32) 248 this.writeFloat32(value as float32) 249 } 250 } 251 writeInt8(value: int32) { 252 this.checkCapacity(1) 253 this.buffer.set(this.position, value as int8) 254 this.position += 1 255 } 256 private setInt32(position: int32, value: int32): void { 257 this.buffer.set(position + 0, ((value ) & 0xff) as int8) 258 this.buffer.set(position + 1, ((value >> 8) & 0xff) as int8) 259 this.buffer.set(position + 2, ((value >> 16) & 0xff) as int8) 260 this.buffer.set(position + 3, ((value >> 24) & 0xff) as int8) 261 } 262 writeInt32(value: int32) { 263 this.checkCapacity(4) 264 this.setInt32(this.position, value) 265 this.position += 4 266 } 267 writeInt64(value: int64) { 268 this.checkCapacity(8) 269 this.buffer.set(this.position + 0, ((value ) & 0xff) as int8) 270 this.buffer.set(this.position + 1, ((value >> 8) & 0xff) as int8) 271 this.buffer.set(this.position + 2, ((value >> 16) & 0xff) as int8) 272 this.buffer.set(this.position + 3, ((value >> 24) & 0xff) as int8) 273 this.buffer.set(this.position + 4, ((value >> 32) & 0xff) as int8) 274 this.buffer.set(this.position + 5, ((value >> 40) & 0xff) as int8) 275 this.buffer.set(this.position + 6, ((value >> 48) & 0xff) as int8) 276 this.buffer.set(this.position + 7, ((value >> 56) & 0xff) as int8) 277 this.position += 8 278 } 279 writeFloat32(value: float32) { 280 let bits = int32BitsFromFloat(value) 281 // TODO: this is wrong! 282 this.checkCapacity(4) 283 this.buffer.set(this.position + 0, ((bits ) & 0xff) as int8) 284 this.buffer.set(this.position + 1, ((bits >> 8) & 0xff) as int8) 285 this.buffer.set(this.position + 2, ((bits >> 16) & 0xff) as int8) 286 this.buffer.set(this.position + 3, ((bits >> 24) & 0xff) as int8) 287 this.position += 4 288 } 289 writePointer(value: pointer) { 290 if (typeof value === "bigint") 291 // todo where it is possible to be called from? 292 throw new Error("Not implemented") 293 this.writeInt64(value) 294 } 295 writeBoolean(value: boolean|undefined) { 296 this.checkCapacity(1) 297 if (value == undefined) { 298 this.buffer.set(this.position, RuntimeType.UNDEFINED as int32 as int8) 299 } else { 300 this.buffer.set(this.position, (value ? 1 : 0) as int8) 301 } 302 this.position++ 303 } 304 writeMaterialized(value: Object) { 305 this.writePointer(registerMaterialized(value)) 306 } 307 writeString(value: string) { 308 this.checkCapacity((4 + value.length * 4 + 1) as int32) // length, data 309 let encodedLength = InteropNativeModule._ManagedStringWrite(value, this.asArray(), this.position + 4) 310 this.setInt32(this.position, encodedLength) 311 this.position += encodedLength + 4 312 } 313 //TODO: Needs to be implemented 314 writeBuffer(value: NativeBuffer) { 315 this.writeCallbackResource({ 316 resourceId: value.resourceId, 317 hold: value.hold, 318 release: value.release 319 }) 320 this.writePointer(value.data) 321 this.writeInt64(value.length as int64) 322 } 323} 324 325