• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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