From cb1c1428cce68286898052a5e0d095a6fcde5f17 Mon Sep 17 00:00:00 2001 From: Ilya Erokhin Date: Thu, 10 Jul 2025 13:44:15 +0300 Subject: [PATCH 1/4] Changes used to start e2e Signed-off-by: Ilya Erokhin --- arkoala-arkts/BUILD.gn | 3 + arkoala-arkts/arkui/src/ets/ArkUIEntry.ets | 428 ++++++++++++++++++ arkoala-arkts/arkui/src/ets/UserView.ets | 17 + .../src/ets/handwritten/component/base.ets | 2 +- arkoala-arkts/arkui/ui2abcconfig-restart.json | 2 +- interop/src/cpp/ani/convertors-ani.cc | 6 +- 6 files changed, 453 insertions(+), 5 deletions(-) create mode 100644 arkoala-arkts/arkui/src/ets/ArkUIEntry.ets diff --git a/arkoala-arkts/BUILD.gn b/arkoala-arkts/BUILD.gn index 09397af16..a3a2c0c5e 100644 --- a/arkoala-arkts/BUILD.gn +++ b/arkoala-arkts/BUILD.gn @@ -80,6 +80,9 @@ ohos_shared_library("ArkoalaNative_ark") { ] external_deps = [ "hilog:hilog_ndk" ] + + subsystem_name = ace_engine_subsystem + part_name = ace_engine_part } ohos_shared_library("ArkoalaLoader") { diff --git a/arkoala-arkts/arkui/src/ets/ArkUIEntry.ets b/arkoala-arkts/arkui/src/ets/ArkUIEntry.ets new file mode 100644 index 000000000..b2f44a470 --- /dev/null +++ b/arkoala-arkts/arkui/src/ets/ArkUIEntry.ets @@ -0,0 +1,428 @@ +/* + * Copyright (c) 2024 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 { memo } from "@koalaui/runtime/annotations" +import { ComputableState, IncrementalNode, GlobalStateManager, StateManager, StateContext, memoEntry, MutableState, createAnimationTimer, callScheduledCallbacks } from "@koalaui/runtime" +import { int32, int64 } from "@koalaui/common" +import { DeserializerBase, pointer, nullptr, KPointer, InteropNativeModule, registerNativeModuleLibraryName, KSerializerBuffer } from "@koalaui/interop" +import { PeerNode } from "./PeerNode" +import { ArkUINativeModule } from "#components" +import { EventEmulator } from "./generated" +import { UserView, UserViewBuilder, EntryPoint } from "./UserView" +import { ClickEvent, ClickEventInternal } from "./generated" +import { checkEvents, setCustomEventsChecker } from "./Events" +import { checkArkoalaCallbacks } from "./CallbacksChecker" +import { setUIDetachedRootCreator } from "./CallbackTransformer" +import { enterForeignContext, leaveForeignContext } from "./handwritten" +import { wrapSystemCallback, KUint8ArrayPtr } from "@koalaui/interop" +import { deserializeAndCallCallback } from "./generated/peers/CallbackDeserializeCall" +import { Routed } from "./handwritten" + +setCustomEventsChecker(checkArkoalaCallbacks) + +enum EventType { + Click, + Text, + ExitApp +} + +class PartialUpdateRecord { + public update: () => void + public context: Object + public callback: (before: boolean) => void + + constructor(update: () => void, context: Object, callback: (before: boolean) => void) { + this.callback = callback + this.context = context + this.update = update + } +} + +let partialUpdates = new Array() +let _currentPartialUpdateContext: Object | undefined = undefined + +/** + * Provide partial update lambda and context. + * + * @param update - function that performs state update + * @param context - context available to UI code when state update effect happens + */ +export function addPartialUpdate(update: () => void, context: T, callback: (before: boolean) => void): void { + partialUpdates.push(new PartialUpdateRecord(update, context as Object, callback)) +} + +/** + * Current partial update context or undefined. + * + * @returns current partial update context + */ +export function currentPartialUpdateContext(): T | undefined { + return _currentPartialUpdateContext as (T | undefined) +} + +// TODO: move to Application class. +let detachedRoots: Map> = new Map>() + +// mark the tree create by BuilderNode +let detachedStatMgt: Map, WeakRef>> = new Map, WeakRef>>() + +export function createUiDetachedRoot( + peerFactory: () => PeerNode, + @memo + builder: () => void +): PeerNode { + const manager = GlobalStateManager.instance + const node = manager.updatableNode(peerFactory(), (context: StateContext) => { + const frozen = manager.frozen + manager.frozen = true + memoEntry(context, 0, builder) + manager.frozen = frozen + }) + detachedRoots.set(node.value.peer.ptr, node) + return node.value +} +setUIDetachedRootCreator(createUiDetachedRoot) + +//used By BuilderNode +export function createUiDetachedBuilderRoot( + peerFactory: () => PeerNode, + @memo + builder: () => void, + manager: StateManager +): ComputableState { + const node = manager.updatableNode(peerFactory(), (context: StateContext) => { + const frozen = manager.frozen + manager.frozen = true + memoEntry(context, 0, builder) + manager.frozen = frozen + }) + detachedRoots.set(node.value.peer.ptr, node) + detachedStatMgt.set(new WeakRef(manager), new WeakRef>(node)) + return node +} + +export function destroyUiDetachedRoot(node: PeerNode): void { + if (!detachedRoots.has(node.peer.ptr)) + throw new Error(`Root with id ${node.peer.ptr} is not registered`) + const root = detachedRoots.get(node.peer.ptr)! + detachedRoots.delete(node.peer.ptr) + root.dispose() +} + +function dumpTree(node: IncrementalNode, indent: int32 = 0) { + const indentToString = (indent: number) => { + let str = "" + for (let i = 0; i <= indent; i++) str += " " + return str + } + + if (indent == 0) InteropNativeModule._NativeLog("> Dump tree:") + + let child = node.firstChild + InteropNativeModule._NativeLog("> " + indentToString(indent++) + "| " + node) + + while (child) { + dumpTree(child!, indent + 1) + child = child!.nextSibling + } +} + +function errorInfo(crash: Object): string { + let msg = crash.toString() + "\n" + if (Object.hasOwn(crash, "stack")) msg += (crash as Error).stack + return msg +} + +let crashDumped = false +function drawCurrentCrash(crash: Object) { + let msg = errorInfo(crash) + if (!crashDumped) { + InteropNativeModule._NativeLog(msg) + crashDumped = true + } + ArkUINativeModule._ShowCrash(msg ?? "unknown error message") +} + +function registerSyncCallbackProcessor() { + wrapSystemCallback(1, (buff:KSerializerBuffer, len:int32) => { + deserializeAndCallCallback(new DeserializerBase(buff, len)) + return 0 + }) +} + +export class Application { + private manager: StateManager | undefined = undefined + private rootState: ComputableState | undefined = undefined + private timer: MutableState | undefined = undefined + private currentCrash: Object | undefined = undefined + private enableDumpTree = false + private exitApp: boolean = false + private userView: UserView | undefined = undefined + private entryPoint: EntryPoint | undefined = undefined + private moduleName: string = "" + private startUrl: string = "" + private startParam: string = "" + + private withLog = false + private useNativeLog = true + private rootPointer: KPointer = nullptr + + constructor(useNativeLog: boolean, moduleName: string, startUrl: string, startParam: string, userView: UserView | undefined, entryPoint: EntryPoint | undefined) { + this.useNativeLog = useNativeLog + this.moduleName = moduleName + this.startUrl = startUrl + this.startParam = startParam + this.userView = userView + this.entryPoint = entryPoint + } + + static createMemoRootState(manager: StateManager, + @memo + builder: () => void, // TODO UserViewBuilder + withRouter: boolean = true + ): ComputableState { + const peer = PeerNode.generateRootPeer() + return manager.updatableNode(peer, (context: StateContext) => { + const frozen = manager.frozen + manager.frozen = true + memoEntry(context, 0, () => { Routed(builder) } ) + manager.frozen = frozen + }) + } + + private computeRoot(): PeerNode { + // let handle = ArkUINativeModule._SystemAPI_StartFrame() + let result: PeerNode + try { + result = this.rootState!.value + this.rootPointer = result.peer.ptr + } finally { + // ArkUINativeModule._SystemAPI_EndFrame(handle) + } + return result + } + + start(): pointer { + if (this.withLog) UserView.startNativeLog(1) + let root: PeerNode | undefined = undefined + try { + this.manager = GlobalStateManager.instance + this.timer = createAnimationTimer(this.manager!) + @memo + let builder: () => void + if (this.entryPoint) { + builder = this.entryPoint!.entry + } else if (this.userView) { + builder = this.userView!.getBuilder() + } else { + throw new Error("Invalid EntryPoint") + } + this.rootState = Application.createMemoRootState(this.manager!, builder) + InteropNativeModule._NativeLog("ArkTS Application.start before computeRoot") + root = this.computeRoot() + InteropNativeModule._NativeLog("ArkTS Application.start after computeRoot") + } catch (e) { + if (e instanceof Error) { + InteropNativeModule._NativeLog("ArkTS Application.start error name: ${e.name} message: ${e.message}"); + const stack = e.stack + if (stack) { + InteropNativeModule._NativeLog("ArkTS Application.start stack trace: " + stack) + } + return nullptr + } + } + if (this.withLog) { + UserView.stopNativeLog(1) + if (this.useNativeLog) { + InteropNativeModule._PrintGroupedLog(1) + } else { + let log = UserView.getNativeLog(1) + if (log.length > 0) { + InteropNativeModule._NativeLog(log) + } + } + } + return root!.peer.ptr + } + + private checkEvents(what: int32) { + checkEvents() + } + + private checkUIContextCallbacks(): void { + InteropNativeModule._NativeLog('ArkUIEntry: checkUIContextCallbacks is not implemented') + } + + private updateState() { + this.updateStates(this.manager!, this.rootState!) + // Here we request to draw a frame and call custom components callbacks. + ArkUINativeModule._MeasureLayoutAndDraw(this.rootPointer) + // Call callbacks and sync. + callScheduledCallbacks() + } + + updateStates(manager: StateManager, root: ComputableState) { + // Ensure all current state updates took effect. + manager.syncChanges() + manager.updateSnapshot() + this.computeRoot() + for (const detachedRoot of detachedRoots.values()) + detachedRoot.value + + if (partialUpdates.length > 0) { + // If there are pending partial updates - we apply them one by one and provide update context. + for (let update of partialUpdates) { + // Set the context available via currentPartialUpdateContext() to @memo code. + _currentPartialUpdateContext = update.context + // Update states. + update.update() + // Propagate changes. + manager.syncChanges() + manager.updateSnapshot() + // Notify subscriber. + update.callback(true) + // Compute new tree state + try { + root.value + for (const detachedRoot of detachedRoots.values()) + detachedRoot.value + } catch (error) { + if (error instanceof Error) { + InteropNativeModule._NativeLog(`ArkTS has error in partialUpdates: ${error.name} message: ${error.message}`); + if (error.stack) { + InteropNativeModule._NativeLog(error.stack!.toString()) + } + } + } + // Notify subscriber. + update.callback(false) + // Clear context. + _currentPartialUpdateContext = undefined + } + // Clear partial updates list. + partialUpdates.splice(0, partialUpdates.length) + } + } + + private render() { + if (this.withLog) InteropNativeModule._NativeLog("ARKTS: render") + } + + enter(arg0: int32, arg1: int32, foreignContext: pointer): boolean { + enterForeignContext(foreignContext) + if (this.withLog) UserView.startNativeLog(1) + + if (this.currentCrash) { + drawCurrentCrash(this.currentCrash!) + } else { + try { + this.timer!.value = Date.now() as int64 + this.loopIteration(arg0, arg1) + if (this.enableDumpTree) { + dumpTree(this.rootState!.value) + } + } catch (error) { + if (error instanceof Error) { + InteropNativeModule._NativeLog(`ArkTS Application.enter error name: ${error.name} message: ${error.message}`); + if (error.stack) { + leaveForeignContext() + InteropNativeModule._NativeLog(error.stack!.toString()) + return true + } + } + this.currentCrash = error as Object + } + } + if (this.withLog) { + UserView.stopNativeLog(1) + if (this.useNativeLog) { + InteropNativeModule._PrintGroupedLog(1) + } else { + let log = UserView.getNativeLog(1) + if (log.length > 0) { + InteropNativeModule._NativeLog(log) + } + } + } + leaveForeignContext() + return this.exitApp + } + + loopIteration(arg0: int32, arg1: int32) { + if (this.withLog) InteropNativeModule._NativeLog("ARKTS: loopIteration") + this.checkEvents(arg0) + this.updateState() + this.render() + } + + // called at the tail of vsync + checkCallbacks(): void { + if (this.withLog) InteropNativeModule._NativeLog("ARKTS: checkCallbacks") + checkEvents() + this.checkUIContextCallbacks() + } + + // TODO: make [emitEvent] suitable to get string argument + emitEvent(type: int32, target: int32, arg0: int32, arg1: int32): string { + console.log("emitEvent for " + target) + const node = PeerNode.findPeerByNativeId(target) + if (node != undefined) { + try { + switch (type as EventType) { + case EventType.Click: { + EventEmulator.emitClickEvent(node.peer.ptr, makeClickEvent(10, 20)) + break + } + case EventType.Text: { + InteropNativeModule._NativeLog("ARKTS: [emitEvent] EventType.Text is not implemented." + type) + break + } + case EventType.ExitApp: { + this.exitApp = true + break + } + default: { + InteropNativeModule._NativeLog("ARKTS: [emitEvent] type = " + type + " is unknown.") + break + } + } + } catch (error) { + InteropNativeModule._NativeLog("emitEvent error: " + errorInfo(error as Object)) + } + } else { + console.log(`Cannot find target node ${target}`) + } + return "0" + } + + static createApplication(startUrl: string, startParams: string, useNativeLog: boolean, moduleName: string, userView: UserView | undefined, entryPoint: EntryPoint | undefined): Application { + if (!userView && !entryPoint) { + throw new Error(`Invalid EntryPoint`) + } + registerNativeModuleLibraryName("InteropNativeModule", "ArkoalaNative_ark.z") + registerNativeModuleLibraryName("ArkUINativeModule", "ArkoalaNative_ark.z") + registerNativeModuleLibraryName("ArkUIGeneratedNativeModule", "ArkoalaNative_ark.z") + registerNativeModuleLibraryName("TestNativeModule", "ArkoalaNative_ark.z") + registerSyncCallbackProcessor() + return new Application(useNativeLog, moduleName, startUrl, startParams, userView, entryPoint) + } +} + +function makeClickEvent(x: number, y: number): ClickEvent { + let result = new ClickEventInternal() + result.x = x + result.y = y + return result +} \ No newline at end of file diff --git a/arkoala-arkts/arkui/src/ets/UserView.ets b/arkoala-arkts/arkui/src/ets/UserView.ets index 51a85803c..2df5351ad 100644 --- a/arkoala-arkts/arkui/src/ets/UserView.ets +++ b/arkoala-arkts/arkui/src/ets/UserView.ets @@ -38,6 +38,23 @@ export class UserView { getBuilder(): UserViewBuilder { throw new Error("User must override this method"); } + + // TODO: these native functions are here temporary. + static startNativeLog(group: int32) { + NativeLog.Default.startNativeLog(group) + } + + static stopNativeLog(group: int32) { + NativeLog.Default.stopNativeLog(group) + } + + static getNativeLog(group: int32): string { + return NativeLog.Default.getNativeLog(group) + } + + static printNativeLog(group: int32) { + return NativeLog.Default.printNativeLog(group) + } } export function UserMemoWrapper( diff --git a/arkoala-arkts/arkui/src/ets/handwritten/component/base.ets b/arkoala-arkts/arkui/src/ets/handwritten/component/base.ets index 8fe2791db..2541cce8a 100644 --- a/arkoala-arkts/arkui/src/ets/handwritten/component/base.ets +++ b/arkoala-arkts/arkui/src/ets/handwritten/component/base.ets @@ -31,7 +31,7 @@ import { } from "../../generated" import { memo, memo_stable } from "@koalaui/runtime/annotations" import { rememberDisposable, remember, mutableState, MutableState, NodeAttach, RunEffect } from "@koalaui/runtime" -import { addPartialUpdate } from '../../Application' +import { addPartialUpdate } from '../../ArkUIEntry' import { CurrentRouterTransitionState, RouterTransitionVisibility } from "../Router"; import { PeerNode } from "../../PeerNode"; import { PageTransitionEnter, PageTransitionExit } from "../ArkPageTransition" diff --git a/arkoala-arkts/arkui/ui2abcconfig-restart.json b/arkoala-arkts/arkui/ui2abcconfig-restart.json index b4b98b48b..513646113 100644 --- a/arkoala-arkts/arkui/ui2abcconfig-restart.json +++ b/arkoala-arkts/arkui/ui2abcconfig-restart.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "package": "@ohos.arkui", + "package": "arkui", "outDir": "build-m3-impl", "baseUrl": "./src/ets", "paths": { diff --git a/interop/src/cpp/ani/convertors-ani.cc b/interop/src/cpp/ani/convertors-ani.cc index 7373e45d7..56da88d72 100644 --- a/interop/src/cpp/ani/convertors-ani.cc +++ b/interop/src/cpp/ani/convertors-ani.cc @@ -147,9 +147,9 @@ void AniExports::setClasspath(const char* module, const char *classpath) { static std::map g_defaultClasspaths = { {"InteropNativeModule", "@koalaui.interop.InteropNativeModule.InteropNativeModule"}, // todo leave just InteropNativeModule, define others via KOALA_ETS_INTEROP_MODULE_CLASSPATH - {"TestNativeModule", "@ohos.arkui.generated.arkts.TestNativeModule.TestNativeModule"}, - {"ArkUINativeModule", "@ohos.arkui.generated.arkts.ArkUINativeModule.ArkUINativeModule"}, - {"ArkUIGeneratedNativeModule", "@ohos.arkui.generated.arkts.ArkUIGeneratedNativeModule.ArkUIGeneratedNativeModule"}, + {"TestNativeModule", "arkui.generated.arkts.TestNativeModule.TestNativeModule"}, + {"ArkUINativeModule", "arkui.generated.arkts.ArkUINativeModule.ArkUINativeModule"}, + {"ArkUIGeneratedNativeModule", "arkui.generated.arkts.ArkUIGeneratedNativeModule.ArkUIGeneratedNativeModule"}, }; const std::string& AniExports::getClasspath(const std::string& module) { -- Gitee From 549a84cd859c6a7accdec3e8ae0725a2fe1e723e Mon Sep 17 00:00:00 2001 From: Ilya Erokhin Date: Fri, 11 Jul 2025 09:58:46 +0300 Subject: [PATCH 2/4] Change package name from @ohos.arkui to arkui in ui2abcconfig.json Signed-off-by: Ilya Erokhin --- arkoala-arkts/arkui/ui2abcconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arkoala-arkts/arkui/ui2abcconfig.json b/arkoala-arkts/arkui/ui2abcconfig.json index 411c24cfe..5b7d0387f 100644 --- a/arkoala-arkts/arkui/ui2abcconfig.json +++ b/arkoala-arkts/arkui/ui2abcconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "package": "@ohos.arkui", + "package": "arkui", "outDir": "build-m3-recheck", "baseUrl": "./src/ets", "paths": { -- Gitee From d7452b62bc35a7f6dfdd7b0b3aa380cb0b09b088 Mon Sep 17 00:00:00 2001 From: Ilya Erokhin Date: Fri, 11 Jul 2025 10:53:13 +0300 Subject: [PATCH 3/4] Change package name from @ohos.arkui to arkui in convertors-ets.cc and vmloader.cc Signed-off-by: Ilya Erokhin --- interop/src/cpp/ets/convertors-ets.cc | 6 +++--- interop/src/cpp/vmloader.cc | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/interop/src/cpp/ets/convertors-ets.cc b/interop/src/cpp/ets/convertors-ets.cc index c4383876f..c0af6782a 100644 --- a/interop/src/cpp/ets/convertors-ets.cc +++ b/interop/src/cpp/ets/convertors-ets.cc @@ -132,9 +132,9 @@ void EtsExports::setClasspath(const char* module, const char *classpath) { static std::map g_defaultClasspaths = { {"InteropNativeModule", "@koalaui/interop/InteropNativeModule/InteropNativeModule"}, // todo leave just InteropNativeModule, define others via KOALA_ETS_INTEROP_MODULE_CLASSPATH - {"TestNativeModule", "@ohos/arkui/generated/arkts/TestNativeModule/TestNativeModule"}, - {"ArkUINativeModule", "@ohos/arkui/generated/arkts/ArkUINativeModule/ArkUINativeModule"}, - {"ArkUIGeneratedNativeModule", "@ohos/arkui/generated/arkts/ArkUIGeneratedNativeModule/ArkUIGeneratedNativeModule"}, + {"TestNativeModule", "arkui/generated/arkts/TestNativeModule/TestNativeModule"}, + {"ArkUINativeModule", "arkui/generated/arkts/ArkUINativeModule/ArkUINativeModule"}, + {"ArkUIGeneratedNativeModule", "arkui/generated/arkts/ArkUIGeneratedNativeModule/ArkUIGeneratedNativeModule"}, }; const std::string& EtsExports::getClasspath(const std::string& module) { auto it = classpaths.find(module); diff --git a/interop/src/cpp/vmloader.cc b/interop/src/cpp/vmloader.cc index c08e62721..17243aadb 100644 --- a/interop/src/cpp/vmloader.cc +++ b/interop/src/cpp/vmloader.cc @@ -557,9 +557,9 @@ const AppInfo javaAppInfo = { #ifdef KOALA_ETS_NAPI const AppInfo pandaAppInfo = { - "@ohos/arkui/Application/Application", + "arkui/Application/Application", "createApplication", - "Lstd/core/String;Lstd/core/String;ZI:L@ohos/arkui/Application/Application;", + "Lstd/core/String;Lstd/core/String;ZI:Larkui/Application/Application;", "start", "JI:J", "enter", @@ -600,9 +600,9 @@ const AppInfo harnessAniAppInfo = { "i:i" }; const AppInfo aniAppInfo = { - "@ohos.arkui.Application.Application", + "arkui.Application.Application", "createApplication", - "C{std.core.String}C{std.core.String}C{std.core.String}zi:C{@ohos.arkui.Application.Application}", + "C{std.core.String}C{std.core.String}C{std.core.String}zi:C{arkui.Application.Application}", "start", "li:l", "enter", -- Gitee From 240b1addd37cfe0a271bdc10c8f1ff2e5187486a Mon Sep 17 00:00:00 2001 From: Ilya Erokhin Date: Fri, 11 Jul 2025 11:57:46 +0300 Subject: [PATCH 4/4] Rename @ohos.arkui to arkui Signed-off-by: Ilya Erokhin --- ets-tests/ets/TestHarnessApp.ets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ets-tests/ets/TestHarnessApp.ets b/ets-tests/ets/TestHarnessApp.ets index fb1178b3b..a75e91ae8 100644 --- a/ets-tests/ets/TestHarnessApp.ets +++ b/ets-tests/ets/TestHarnessApp.ets @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { UserView, UserViewBuilder } from '@ohos.arkui' +import { UserView, UserViewBuilder } from 'arkui' import { TestController } from './TestHarness' import { initPages } from './AllPages' import { suiteComponentLifeCycle } from './suites/ComponentLifeCycle' -- Gitee