/*
* Copyright (c) 2025 Huawei Device Co., Ltd.
* Licensed 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.
*/
import { float32, float64, int8, int32, int64, int32BitsFromFloat } from "@koalaui/common"
import { pointer, KUint8ArrayPtr } from "./InteropTypes"
import { ResourceId, ResourceHolder } from "./ResourceManager"
import { KBuffer } from "./buffer"
import { NativeBuffer } from "./NativeBuffer"
import { InteropNativeModule } from "./InteropNativeModule"
/**
* Value representing possible JS runtime object type.
* Must be synced with "enum RuntimeType" in C++.
*/
export class RuntimeType {
static UNEXPECTED = -1
static NUMBER = 1
static STRING = 2
static OBJECT = 3
static BOOLEAN = 4
static UNDEFINED = 5
static BIGINT = 6
static FUNCTION = 7
static SYMBOL = 8
static MATERIALIZED = 9
}
/**
* Value representing object type in serialized data.
* Must be synced with "enum Tags" in C++.
*/
export class Tags {
static UNDEFINED = 101
static INT32 = 102
static FLOAT32 = 103
static STRING = 104
static LENGTH = 105
static RESOURCE = 106
static OBJECT = 107
}
export function runtimeType(value: Object|String|number|undefined|null): int32 {
let type = typeof value
if (type == "number") return RuntimeType.NUMBER
if (type == "string") return RuntimeType.STRING
if (type == "undefined") return RuntimeType.UNDEFINED
if (type == "object") return RuntimeType.OBJECT
if (type == "boolean") return RuntimeType.BOOLEAN
if (type == "bigint") return RuntimeType.BIGINT
if (type == "function") return RuntimeType.FUNCTION
if (type == "symbol") return RuntimeType.SYMBOL
throw new Error(`bug: ${value} is ${type}`)
}
export function registerCallback(value: object): int32 {
// TODO: fix me!
return 42
}
function registerMaterialized(value: Object): int32 {
// TODO: fix me!
return 42
}
export function isResource(value: Object|undefined): boolean {
// TODO: fix me!
return false
}
export function isInstanceOf(className: string, value: Object|undefined): boolean {
// TODO: fix me!
return false
}
export interface CallbackResource {
resourceId: int32
hold: pointer
release: pointer
}
/* Serialization extension point */
export abstract class CustomSerializer {
protected supported: Array<string>
constructor(supported: Array<string>) {
this.supported = supported
}
supports(kind: string): boolean { return this.supported.includes(kind) }
abstract serialize(serializer: SerializerBase, value: object, kind: string): void
next: CustomSerializer | undefined = undefined
}
class DateSerializer extends CustomSerializer {
constructor() {
super(Array.of("Date" as string))
}
serialize(serializer: SerializerBase, value: object, kind: string): void {
serializer.writeString((value as Date).toISOString())
}
}
SerializerBase.registerCustomSerializer(new DateSerializer())
export class SerializerBase {
private position = 0
private buffer: KBuffer
private static customSerializers: CustomSerializer | undefined = new DateSerializer()
static registerCustomSerializer(serializer: CustomSerializer) {
if (SerializerBase.customSerializers == undefined) {
SerializerBase.customSerializers = serializer
} else {
let current = SerializerBase.customSerializers
while (current!.next != undefined) {
current = current!.next
}
current!.next = serializer
}
}
resetCurrentPosition(): void { this.position = 0 }
constructor() {
this.buffer = new KBuffer(96)
}
public release() {
this.releaseResources()
this.position = 0
}
asArray(): KUint8ArrayPtr {
return this.buffer.buffer
}
length(): int32 {
return this.position
}
currentPosition(): int32 { return this.position }
private checkCapacity(value: int32) {
if (value < 1) {
throw new Error(`${value} is less than 1`)
}
let buffSize = this.buffer.length
if (this.position > buffSize - value) {
const minSize = this.position + value
const resizedSize = Math.max(minSize, Math.round(3 * buffSize / 2))
let resizedBuffer = new KBuffer(resizedSize as int32)
for (let i = 0; i < this.buffer.length; i++) {
resizedBuffer.set(i, this.buffer.get(i))
}
this.buffer = resizedBuffer
}
}
private heldResources: Array<ResourceId> = new Array<ResourceId>()
holdAndWriteCallback(callback: object, hold: pointer = 0, release: pointer = 0, call: pointer = 0, callSync: pointer = 0): ResourceId {
const resourceId = ResourceHolder.instance().registerAndHold(callback)
this.heldResources.push(resourceId)
this.writeInt32(resourceId)
this.writePointer(hold)
this.writePointer(release)
this.writePointer(call)
this.writePointer(callSync)
return resourceId
}
holdAndWriteCallbackForPromiseVoid(hold: pointer = 0, release: pointer = 0, call: pointer = 0): [Promise<void>, ResourceId] {
let resourceId: ResourceId
const promise = new Promise<void>((resolve: (value: PromiseLike<void>) => void, reject: (err: Object|null|undefined) => void) => {
const callback = (err: string[]|undefined) => {
if (err !== undefined)
reject(err!)
else
resolve(Promise.resolve())
}
resourceId = this.holdAndWriteCallback(callback, hold, release, call)
})
return [promise, resourceId]
}
holdAndWriteCallbackForPromise<T>(hold: pointer = 0, release: pointer = 0, call: pointer = 0): [Promise<T>, ResourceId] {
let resourceId: ResourceId
const promise = new Promise<T>((resolve: (value: T|PromiseLike<T>) => void, reject: (err: Object|null|undefined) => void) => {
const callback = (value: T|undefined, err: string[]|undefined) => {
if (err !== undefined)
reject(err!)
else
resolve(value!)
}
resourceId = this.holdAndWriteCallback(callback, hold, release, call)
})
return [promise, resourceId]
}
writeCallbackResource(resource: CallbackResource) {
this.writeInt32(resource.resourceId)
this.writePointer(resource.hold)
this.writePointer(resource.release)
}
writeResource(resource: object) {
const resourceId = ResourceHolder.instance().registerAndHold(resource)
this.heldResources.push(resourceId)
this.writeInt32(resourceId)
}
private releaseResources() {
for (const resourceId of this.heldResources)
InteropNativeModule._ReleaseCallbackResource(resourceId)
// todo think about effective array clearing/pushing
this.heldResources = new Array<ResourceId>()
}
writeCustomObject(kind: string, value: object) {
let current = SerializerBase.customSerializers
while (current) {
if (current!.supports(kind)) {
current!.serialize(this, value, kind)
return
}
current = current!.next
}
// console.log(`Unsupported custom serialization for ${kind}, write undefined`)
this.writeInt8(Tags.UNDEFINED as int32)
}
writeFunction(value: Object) {
this.writeInt32(registerCallback(value))
}
writeTag(tag: int32): void {
this.buffer.set(this.position, tag as int8)
this.position++
}
writeNumber(value: number|undefined) {
this.checkCapacity(5)
if (value == undefined) {
this.writeTag(Tags.UNDEFINED)
this.position++
return
}
if ((value as float64) == Math.round(value)) {
this.writeTag(Tags.INT32)
this.writeInt32(value as int32)
return
} else {
this.writeTag(Tags.FLOAT32)
this.writeFloat32(value as float32)
}
}
writeInt8(value: int32) {
this.checkCapacity(1)
this.buffer.set(this.position, value as int8)
this.position += 1
}
private setInt32(position: int32, value: int32): void {
this.buffer.set(position + 0, ((value ) & 0xff) as int8)
this.buffer.set(position + 1, ((value >> 8) & 0xff) as int8)
this.buffer.set(position + 2, ((value >> 16) & 0xff) as int8)
this.buffer.set(position + 3, ((value >> 24) & 0xff) as int8)
}
writeInt32(value: int32) {
this.checkCapacity(4)
this.setInt32(this.position, value)
this.position += 4
}
writeInt64(value: int64) {
this.checkCapacity(8)
this.buffer.set(this.position + 0, ((value ) & 0xff) as int8)
this.buffer.set(this.position + 1, ((value >> 8) & 0xff) as int8)
this.buffer.set(this.position + 2, ((value >> 16) & 0xff) as int8)
this.buffer.set(this.position + 3, ((value >> 24) & 0xff) as int8)
this.buffer.set(this.position + 4, ((value >> 32) & 0xff) as int8)
this.buffer.set(this.position + 5, ((value >> 40) & 0xff) as int8)
this.buffer.set(this.position + 6, ((value >> 48) & 0xff) as int8)
this.buffer.set(this.position + 7, ((value >> 56) & 0xff) as int8)
this.position += 8
}
writeFloat32(value: float32) {
let bits = int32BitsFromFloat(value)
// TODO: this is wrong!
this.checkCapacity(4)
this.buffer.set(this.position + 0, ((bits ) & 0xff) as int8)
this.buffer.set(this.position + 1, ((bits >> 8) & 0xff) as int8)
this.buffer.set(this.position + 2, ((bits >> 16) & 0xff) as int8)
this.buffer.set(this.position + 3, ((bits >> 24) & 0xff) as int8)
this.position += 4
}
writePointer(value: pointer) {
if (typeof value === "bigint")
// todo where it is possible to be called from?
throw new Error("Not implemented")
this.writeInt64(value)
}
writeBoolean(value: boolean|undefined) {
this.checkCapacity(1)
if (value == undefined) {
this.buffer.set(this.position, RuntimeType.UNDEFINED as int32 as int8)
} else {
this.buffer.set(this.position, (value ? 1 : 0) as int8)
}
this.position++
}
writeMaterialized(value: Object) {
this.writePointer(registerMaterialized(value))
}
writeString(value: string) {
this.checkCapacity((4 + value.length * 4 + 1) as int32) // length, data
let encodedLength = InteropNativeModule._ManagedStringWrite(value, this.asArray(), this.position + 4)
this.setInt32(this.position, encodedLength)
this.position += encodedLength + 4
}
//TODO: Needs to be implemented
writeBuffer(value: NativeBuffer) {
this.writeCallbackResource({
resourceId: value.resourceId,
hold: value.hold,
release: value.release
})
this.writePointer(value.data)
this.writeInt64(value.length as int64)
}
}