本文记录 laygent 接入 OpenHarmony 的完整过程,覆盖 AI 原生布局库盘点、鸿蒙 Kotlin 制品线选型、源码集手术、ohosArm64 / ohosX64 目标、QuickJS 引擎内嵌与 cinterop、Kotlin/Native 动态库、C ABI / N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

laygent 是 AI 原生的 Compose Multiplatform 布局库:用一句自然语言描述布局,LLM 在运行时生成布局算法 JavaScript(形如 function doLayout() { … }),由 JS 宿主执行,通过 layoutContext 完成对组件的 measure / place / setSize,算法提交后按帧缓存复用。上游 0.1.0 已发布 Maven Central(com.zachklipp:laygent),支持 jvm / iOS / wasmJs——JVM 与 iOS 的 JS 宿主是 quickjs-kt,wasmJs 直接用浏览器引擎。本次适配与社区已审核的 cmp_qrose(纯 Kotlin 编码器真编译 + 原生光栅化)和 KStore(整库真编译 + 唯一 8 行 actual)同属一条脉络:根构建替换(cmp_qrose 模式)+ 真实内核上机 + C ABI / N-API 桥 + ArkUI 验收宿主。laygent 的特殊之处在于它的"引擎"本身就是一段 JS 宿主——鸿蒙侧断档的不是数据结构而是执行载体,所以核心工作量是把同源 QuickJS 引擎内嵌进 Kotlin/Native,让上游的布局语义原样跑在真机上。

项目地址: AtomGit/oh-tpc/laygent

上游仓库: zach-klippenstein/laygent

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

laygent 为 Compose Multiplatform 应用提供"运行时布局算法生成"这一件事的完整答案:rememberLaygentState(config) { 提示词 } 一行建状态,LLM 按系统提示词约定的 layoutContext API 生成 doLayout() JS,宿主先跑一遍验证(JS 错误 / 布局违规回传 LLM 纠错迭代),通过后提交为布局实现;子组件用 Modifier.laygentTag("…") 打标签供算法决策。OpenHarmony 应用同样需要这套"提示词即布局"的能力,但上游构建(Kotlin 2.3.20 / Gradle 9.5 / Compose 1.10.3 + koog 0.8.0)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示文本框,却无法证明布局引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证布局数据模型、LayoutScope 契约、系统提示词、JS 引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.3.20 / Gradle 9.5 与鸿蒙工具链无法共存于一个 Gradle 构建;旧钉法 Kotlin 2.2.21-1.0.0 读不了 2.3/2.4 元数据。
依赖矩阵断档Compose runtime/ui、koog(LLM 代理框架)、androidx.savedstate、quickjs-kt 在 ohos 上没有任何制品,commonMain 无法整体直编。
JS 宿主语义布局算法是 LLM 生成的任意 JS:ArkTS Stage 模型不能 eval 任意 JS;把执行逻辑搬到 C++/JSVM 又会让 Kotlin 库变成旁观者。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:平台无关内核(布局数据模型 / LayoutScope 契约 / 状态机 / LLM 系统提示词)的 ohos 真编译、同源 QuickJS 引擎内嵌、C ABI / N-API 桥、ArkUI 验收宿主。Compose UI 与 koog 的源码原样存档进不参与编译的 src/composeMain/(cmp_qrose 的 Painter 先例),等鸿蒙 CMP 工具链对齐后整目录接回。ArkTS 只负责调 LLM(HTTP)、调 Native、解析 JSON 和渲染结果。

1.2 库提供的能力

Demo 提供一个真实的布局生成会话,对应桥接的全部入口:

  • 版本握手:laygentVersion() 返回 0.1.0-ohos.1,来自真实 Native 调用而非静态文字;
  • 系统提示词下发:laygentDispatch({"op":"systemPrompt"}) 返回上游 LAYGENT_SYSTEM_PROMPT 全文——与上游 JVM 侧 LlmLayoutCodeGenerator 使用的提示词逐字节一致,保证 LLM 生成的代码遵循同一份 API 契约;
  • 运行布局:laygentDispatch({"op":"runLayout",…}) 在内嵌 QuickJS 中加载 doLayout(),按上游 LayoutScope 契约执行 measure / place / setSize / dpToPx / 四个 intrinsic,返回 kotlinx-serialization 原样编码的 LayoutResult JSON;
  • 状态分类与上游对齐:success / javascriptError / layoutError 三态对应上游 LayoutCodeProposalResult 的 Success / JavascriptError / LayoutError——JS 引擎错误与布局规则违例(双 measure、place 未 measure)分属两类,后者由 Kotlin 侧标记跨 JS 边界传回;
  • AI 生成闭环:ArkTS 调 OpenAI Chat Completions(Key / 模型可配),失败时把错误按上游迭代协议回传 LLM 修正,最多 3 次,全程累加 usage.total_tokens;
  • 内置算法兜底:网格 / 圆形 / 流式换行三段内置 JS,无 Key 也能完整验收引擎链路。

引擎侧完整能力(LayoutScope 全部 10 个成员、measure-once 规则、LayoutResult 组装、数据模型序列化)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 3 项交叉验证冒烟测试,证明 ohos .so 里执行的布局语义没有漂移。

1.3 实现适配

维度要求
代码复用commonMain 纯内核(数据模型 / LayoutScope 契约 / 状态机 / 系统提示词)原样编译,不提取、不重写;JS 宿主按同一契约换挂 QuickJS。
平台目标:laygent 声明 jvm() + ohosArm64 + ohosX64;jvm 保留作交叉验证;库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);demo / demo-shared / demo-web 留盘不进 settings。
桥接稳定单一同步 JSON 入口 LaygentDispatch + 握手 LaygentVersion,异常永不跨 FFI。
依赖纪律ohos 制品线(serialization-json 1.9.1-1.0.0)与 JVM 交叉验证线(quickjs-kt 1.0.3 + coroutines 1.10.2-1.0.0)同一坐标体系,换线不换语义。
UI 完整单页覆盖全部桥接入口:版本握手、系统提示词、内置算法、AI 生成、纠错循环、token 统计、日志。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。鸿蒙侧 Demo 用 ArkUI 渲染布局结果,item 是数据驱动的固定尺寸组件(宽高按 dp × dpScale 计算)——与 cmp_qrose 用原生光栅化复现 Compose Painter 是同一种边界划法:库里跑的是上游原件,Demo 画的是同一份语义。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、验证 eazytec ohos 制品线、确定交付物
第 2 阶段:工具链钉版     ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0、Gradle 8.14.1
第 3 阶段:源码集手术     ── composeMain 隔离、expect 下沉、契约 public 化、系统提示词抽取
第 4 阶段:原生引擎       ── QuickJS 内嵌 + 静态库交叉编译 + cinterop + QuickJsHost
第 5 阶段:桥与宿主       ── @CName JSON 桥、C++ N-API、ArkUI 验收页(内置/AI/纠错)
第 6 阶段:示例与验证     ── JVM 交叉验证、双 ABI 链接、符号验收、HAP、签名与真机

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
laygent/                             上游库(本 fork 唯一的库模块)
  src/commonMain/                    纯内核:布局数据模型 + LayoutScope 契约 + 状态机 + 系统提示词
  src/jsHostMain/                    LayoutJsRuntime expect(JS 宿主契约,仅 JVM 侧消费)
  src/quickjsMain/                   quickjs-kt actual(JVM 交叉验证宿主,上游原样)
  src/composeMain/                   上游 Compose/koog 实现存档(不参与本 fork 编译)
  src/jvmTestUpstream/               上游 JVM 测试存档(依赖 compose-test,不参与编译)
  src/jvmTest/                       新增:3 项交叉验证冒烟测试
example/nativeApp/                   Kotlin/Native 动态库(QuickJS 内嵌 + 布局桥)
example/harmonyApp/                  DevEco Stage、CMake N-API 和 ArkUI 页面
demo/ demo-shared/ demo-web/         上游演示模块,留盘不进 settings
docs/                                上游文档站与 wasm 产物,原样保留
doc/                                 本篇适配文章与效果图

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :laygent 与 :example:nativeApp,rootProject.name = "ohos_laygent"。上游的 compose 插件、vanniktech 发布插件、JetBrains dev 仓库全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 Eclipse Adoptium 21)
OpenHarmony 目标ohosArm64 + ohosX64;jvm() 保留作交叉验证
Gradle8.14.1(2.2.21 旧线必须 8.x,wrapper 从上游 9.5.0 降下来)
kotlinxserialization-json 1.9.1-1.0.0(ohos);coroutines 1.10.2-1.0.0(jvm 交叉验证)
JS 引擎QuickJS 2025-04-26(Bellard 上游发行版,quickjs-kt 同源引擎家族)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.laygent
1.3 源码依赖度盘点
源文件依赖形态处置
LaygentStatus.kt纯 Kotlin原样编译(jvm + 双 ohos)
data/*.kt(4 文件)kotlinx-serialization;Constraints 转换器涉及 compose数据类原样编译并 public 化;compose 转换器拆去 composeMain
LayoutScope.kt接口本体纯 Kotlin;ComposeLayoutScope 与 buildMeasureResult 涉及 compose接口与 NoopLayoutScope 留 commonMain;Compose 实现拆去 composeMain
LaygentSystemPrompt.kt(新增)纯 Kotlin系统提示词从 LayoutCodeGenerator 抽出,公开下发
LayoutJsRuntime.kt(expect,全库唯一)JS 宿主契约下沉 jsHostMain,仅 JVM 侧消费,ohos 目标不感知
quickjsMain/LayoutJsRuntime.ktquickjs-kt原样保留,JVM 交叉验证宿主
Laygent.kt / LaygentState.kt / LayoutRuntime.kt / LayoutCodeGenerator.kt / TokenRecordingLlmClient.ktCompose + koog物理移入 src/composeMain/ 存档(纯移动)
jvmTest/LayoutEngineTest.ktcompose-ui-test物理移入 src/jvmTestUpstream/ 存档

expect 声明全库只有 1 处,且它的唯一消费方 LayoutRuntime 本身就在 composeMain 存档里——把 expect 与 actual 一起挂到 JVM 侧(jsHostMain → quickjsMain → jvmMain),ohos 目标的 commonMain 不含任何 expect,一行 actual 都不用补。

1.4 页面边界
标题区          LAYGENT · OPENHARMONY / 版本握手行(Laygent 0.1.0-ohos.1 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换        布局演示 / 关于
配置区          OpenAI Key(可留空)、模型(默认 gpt-5.4-mini)、item 数量 Slider(1–20)、
                item 边长 Slider(24–96dp)、dpScale = display.densityPixels
布局代码区      内置三算法下拉(网格/圆形/流式换行)+ 可编辑 JS TextArea
提示词区        发给 LLM 的布局提示词(默认 "Arrange items in a 3-column grid with 8 dp…")
操作区          运行当前 JS(免 Key 主路径)/ AI 生成布局(纠错重试 ≤3)/ 清空画布
状态行          status + tokensUsed 累加
画布区          白色圆角 Stack 绝对定位渲染(tag 哈希取色,显示 setSize 结果尺寸)
日志区          带时间戳操作日志(最新在前,上限 100 条)
关于区          库简介、分层架构图、桥协议说明、仓库与社区链接、已知限制

第 2 阶段:目标与依赖打通

2.1 制品线验证(本适配的成败手)

先按 KStore 的方法用 .module 元数据逐个验证社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 变体:

org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.1-1.0.0
  variants: ohosArm64ApiElements / ohosX64ApiElements / jvmApiElements(齐全)
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2-1.0.0
  variants: 同上(jvm 交叉验证与 ohos 同版本号)
org.jetbrains.kotlin:kotlin-stdlib:2.2.21-1.0.0
  qrose 已实证可用(ohos klib 全线就位)

三个关键结论:其一,laygent 的 ohos 侧硬依赖只有 serialization-json(数据模型全部 @Serializable),1.9.1-1.0.0 线一行依赖坐标不用改语义;其二,coroutines 只有 JVM 交叉验证需要(quickjs-kt 传递依赖),1.10.2-1.0.0 与工具链同代;其三,koog / Compose / androidx.savedstate / quickjs-kt 四者确认无 ohos 制品——这正是第 3 阶段源码集手术的依据。

2.2 工具链钉版(为什么是 2.2.21-1.0.0)

社区此前批准的钉法有两条:2.2.21-1.0.0(qrose / haze)与 2.4.20-0.1.0-07(KStore)。两条线本项目都实测过:2.4.20 线连 ohos KLIB 编译都通,但 cinterop 在 Windows 宿主上不可用——该线的宿主 LLVM 指向精简版 llvm-21-x86_64-windows-essentials-150(只有 llvm-ar / llvm-cov / llvm-profdata,没有 libclang.dll),补拷 LLVM 19 的 libclang 又因 clangstubs.dll 按 LLVM 21 符号编译而"找不到指定的程序",完整的 llvm-21-x86_64-windows-dev-150 归档在 dependenciesUrl 上 404,无法补齐。而 2.2.21-1.0.0 线的宿主 LLVM 直接配的就是自带 libclang.dll 的 cpf-llvm-19-x86_64-windows-dev-20,cinterop 开箱即用——这正是 qrose 已审核钉法。

工具链版本跟着依赖走、更要跟着工具链完整性走:laygent 的 ohos 依赖恰好只有 serialization,1.9.1-1.0.0 线完美配套(1.11.0-0.1.0-07 的 2.4.0 元数据 2.2.21 读不了),所以选择可复现性最高的 2.2.21-1.0.0 组合。Gradle 经验按工具链线区分:2.2.21 线必须降 8.14.1,2.4.20 线才能用 9.x——两条线的经验不能互相套用。

settings.gradle.kts 把 eazytec 双仓写在最前:

pluginManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        google()
        mavenCentral()
    }
}

rootProject.name = "ohos_laygent"

include(":laygent")
include(":example:nativeApp")
2.3 编译真实引擎

根 build.gradle.kts 只保留版本声明与 ohos 测试禁用:

plugins {
    alias(libs.plugins.kotlin.multiplatform) apply false
}

allprojects {
    group = "com.zachklipp"
    version = providers.gradleProperty("version").orElse("0.1.0-ohos.1").get()
}

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

:laygent/build.gradle.kts 收敛为纯 KMP 模块(serialization 插件模块级显式声明——上游在根构建统一 apply 的行最容易在根构建替换时丢,丢了编译期不报错,运行期才炸):

plugins {
  alias(libs.plugins.kotlin.multiplatform)
  alias(libs.plugins.kotlin.serialization)
}

kotlin {
  applyDefaultHierarchyTemplate()
  jvmToolchain(21)

  compilerOptions {
    freeCompilerArgs.add("-Xcontext-parameters")
    freeCompilerArgs.add("-Xexpect-actual-classes")
  }

  jvm()
  ohosArm64()
  ohosX64()

  sourceSets {
    commonMain.dependencies {
      implementation(libs.kotlinx.serialization.json)
    }

    // JS 宿主契约与 quickjs actual 只挂 JVM:ohos 无 quickjs-kt 制品,
    // 鸿蒙侧 JS 宿主在 example/nativeApp,驱动同一份 public LayoutScope 契约
    val jsHostMain by creating { dependsOn(commonMain.get()) }
    val quickjsMain by creating {
      dependsOn(jsHostMain)
      dependencies {
        implementation(libs.quickjs)
        implementation(libs.quickjs.converter.ktxserialization)
        implementation(libs.kotlinx.coroutines)
      }
    }
    getByName("jvmMain").dependsOn(quickjsMain)

    jvmTest.dependencies {
      implementation(kotlin("test"))
    }
  }
}

实测:

./gradlew :laygent:compileKotlinJvm          # JVM 交叉验证编译   BUILD SUCCESSFUL
./gradlew :laygent:compileKotlinOhosArm64    # BUILD SUCCESSFUL
./gradlew :laygent:compileKotlinOhosX64      # BUILD SUCCESSFUL

内核与契约的全部 commonMain 文件一个没动(除 public 化与 compose 引用拆分),鸿蒙侧没有补任何 ohos actual。

2.4 测试依赖的处置:compose-test 没有 ohos 制品

上游 LayoutEngineTest 依赖 compose-ui-test-junit4,本构建不编译——处置是两条纪律的组合:上游测试文件一个字节不改,整体存档进 src/jvmTestUpstream/(与 kstore-storage 留盘同理),另新增 3 项跑在 quickjs-kt + commonMain 原件上的交叉验证冒烟测试;根构建全局禁用 Ohos*Test* 任务。这样上游测试资产完整保留,ohos 侧只承担编译与链接。

第 3 阶段:桥协议建模

3.1 JSON 契约
com.zachklipp.laygent/**(上游内核原样编译:数据模型 + LayoutScope 契约 + 系统提示词)
        ↓ public LayoutScope / LayoutResult / LAYGENT_SYSTEM_PROMPT
QuickJsHost(内嵌 QuickJS 宿主)+ DataLayoutScope(数据驱动 scope)+ Bridge
        ↓ UTF-8 JSON
N-API laygentDispatch() / laygentVersion()
        ↓
ArkUI Index.ets(布局演示 / 关于 双 Tab)

请求与响应:

请求 : {"op":"version"}
       {"op":"systemPrompt"}
       {"op":"runLayout","layoutCode":"<doLayout js>","dpScale":3.25,
        "constraints":{"minWidth":0,"minHeight":0,"maxWidth":1080,"maxHeight":600},
        "measurables":[{"id":0,"tag":"item-0","width":163,"height":163}]}
响应 : {"ok":true,"version":"0.1.0-ohos.1"}
       {"ok":true,"prompt":"<上游系统提示词全文>"}
       {"ok":true,"status":"success","result":<LayoutResult 原生 JSON>}
       {"ok":true,"status":"javascriptError","error":"…"}
       {"ok":true,"status":"layoutError","error":"…"}
       {"ok":false,"error":"…"}

三个不对称设计:result 是 kotlinx-serialization 直接编码的 LayoutResult 原生 JSON 嵌入(KStore "get 原生 JSON 嵌入"先例),ArkTS 一次 JSON.parse 即得;status 三态与上游 LayoutCodeProposalResult 一一对应,ArkTS 纠错循环按状态决定是否回传 LLM;measurables 的宽高是 ArkTS 预先算好的像素值,桥内不做任何 UI 测量。

3.2 载荷为什么是数据驱动 measurables

桥内 scope 这样构造:

val scope = DataLayoutScope(
  measurables = measurables,          // id + tag
  intrinsicSizes = intrinsicSizes,    // id → (width, height) 像素
  constraints = constraints,
  dpScale = dpScale,
)
QuickJsHost(QuickJsLayoutNative(scope)).use { host ->
  host.initialize(layoutCode)   // eval 上游同构:LLM 生成的 doLayout 定义为全局函数
  host.invokeDoLayout()         // 调用 doLayout()
}

DataLayoutScope 实现上游 public 的 LayoutScope 接口:measurable 是数据驱动的固定尺寸(cmp_qrose 用原生光栅化复现 Painter 的同款划法),强制上游同样的规则——measure 至多一次、place 只许 place 已 measure 的 id、intrinsic 返回固定尺寸——并把结果组装成上游的 LayoutResult(children 只含已 place 项,带 tag)。这份契约代码就是 :laygent commonMain 的原件,桥只是给它换了一个 JS 引擎和数据源。

3.3 错误通道

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("LaygentDispatch")
fun laygentDispatch(request: String): String = try {
  dispatch(request)
} catch (t: Throwable) {
  """{"ok":false,"error":"${t.message ?: t.toString()}"}"""
}

非法 JSON、缺 op / layoutCode / measurables、不支持的 op,全部走同一个 ok:false 通道;runLayout 内部再把错误分为两态——JS 引擎错误(解析 / eval / 执行抛错)归 javascriptError,Kotlin 侧布局规则违例归 layoutError。后者的分类依据来自 JVM 交叉验证的实证:quickjs-kt 的绑定函数抛出的异常不会被包成 QuickJsException,上游 LayoutRuntime 因此把 scope 规则违例归为 LayoutError。鸿蒙桥用同样的语义:违例以 laygent:layout-error: 标记前缀穿过 JS 边界,宿主识别后抛 QuickJsLayoutException 归入 layoutError。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkUI Index.ets
  │ JSON string
  ▼
C++ N-API entry(liblaygent_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohoslaygent.so,内嵌 QuickJS 静态链入)
  │ LaygentDispatch / LaygentVersion
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复刻布局语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("LaygentVersion")
fun laygentVersion(): String = "0.1.0-ohos.1"   // 版本握手

@CName("LaygentDispatch")
fun laygentDispatch(request: String): String     // version / systemPrompt / runLayout 一口进
C ABI 符号ArkTS 方法覆盖内核
LaygentVersionlaygentVersion()fork 版本握手(真机版本行的数据来源)
LaygentDispatchlaygentDispatch(req)QuickJsHost 生命周期 + DataLayoutScope 全部 10 个契约成员 + LayoutResult 序列化
4.3 引擎内嵌与 C 桥面(本适配最硬的一块)

QuickJS 是按值结构体、JS_UNDEFINED 等是宏、JS_NewCFunction 回调签名带按值结构体——都不适合直接暴露给 Kotlin。适配把引擎交互收敛成一个 C 桥面(laygent_shim.h/.c):

typedef char *(*LaygentNativeFn)(void *userdata, const char *op, const double *args, int argc,
                                 char **err_out);

void laygent_install_native(JSContext *ctx, void *userdata, LaygentNativeFn fn);  // 装 __laygentNative
JSValue laygent_undefined(void);                                                  // JS_UNDEFINED 宏透出

C 侧 trampoline 负责:取 op 字符串、取最多 6 个 double 参数、调 Kotlin 回调、结果字符串 JS_NewStringLen 包回、错误串 JS_Throw 成 JS 异常、所有 malloc 内存自己 free。Kotlin 回调(staticCFunction + StableRef 携带宿主状态)只处理 (userdata, op, double[], errOut) → String? 这一纯 Kotlin 签名。

QuickJS 本体(quickjs.c / dtoa.c / libregexp.c / libunicode.c / cutils.c,2025-04-26 发行版,quickjs-libc 不需要——布局算法只用 Math/JSON/Array 等核心对象)由 Gradle 任务调用 Kotlin/Native 工具链的 clang 编成静态库:

clang --target=aarch64-linux-ohos --sysroot=<ohos sysroot> -std=c11 -fPIC -O2
      -D_GNU_SOURCE -DCONFIG_VERSION=\"2025-04-26\" -c <六个 C 文件>
llvm-ar rcs libquickjs.a <六个 .o>          # arm64 约 1.1MB

QuickJsHost.kt 按上游 LayoutJsRuntime 的 initialize / invoke 两段式组织:启动时装 trampoline 并 eval 一段 bootstrap JS,按 LAYGENT_SYSTEM_PROMPT 描述的形状构造 layoutContext——measurables / constraints 是 JSON.parse(__laygentNative('…Json')),measure 是一次逗号五元组交换,其余全部数值直传;数据交换只走 JSON 字符串和 number,C 面积最小。数据交换与宿主状态全程单线程(ArkTS 顺序调用),与上游 propose/commit 的语义一一对应。

4.4 C++ N-API 与 CMake

N-API 层与 qrose/KStore 完全同款:每个回调统一三步——napi_typeof 校验 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoslaygent_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoslaygent_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号)。

add_library(ohoslaygent SHARED IMPORTED)
set_target_properties(ohoslaygent PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(laygent_napi SHARED napi_init.cpp)
target_link_libraries(laygent_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoslaygent)

OHOS_ARCH 为空或 libohoslaygent.so 缺失直接 FATAL_ERROR(提示先跑 gradle link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const laygentDispatch: (request: string) => string;
export const laygentVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 握手与参数注入
aboutToAppear(): void {
  const density = display.getDefaultDisplaySync().densityPixels;
  this.dpScale = Math.round(density * 1000) / 1000;   // 真机 3.500
  this.version = laygentVersion();                     // 0.1.0-ohos.1,真实 Native 调用
  const resp = JSON.parse(laygentDispatch(
    JSON.stringify({ op: 'systemPrompt' } as SystemPromptRequest))) as SystemPromptResponse;
  this.systemPrompt = resp.prompt;                     // 与上游 JVM 侧一字不差
}
5.2 页面交互

「运行当前 JS」把 TextArea 的代码连同配置直接发给 Native:

const req: RunLayoutRequest = {
  op: 'runLayout', layoutCode: this.layoutCode, dpScale: this.dpScale,
  constraints: { minWidth: 0, minHeight: 0, maxWidth: boardPxWidth, maxHeight: boardPxHeight },
  measurables: items,       // {id, tag:"item-i", width, height},宽高 = dp × dpScale
};
const resp = JSON.parse(laygentDispatch(JSON.stringify(req))) as NativeResponse;

success 时把 resp.result.children 映射为画布上的绝对定位组件(Stack + position + size,tag 哈希取固定调色板,画布上方显示 setSize → w × h px);javascriptError / layoutError 写状态行与日志。「AI 生成布局」走 OpenAI Chat Completions(系统提示词即 Native 下发的那份),生成后先执行,错误时按上游迭代协议追加 assistant + user 纠错消息重试,最多 3 次,usage.total_tokens 全程累加。

5.3 对比度和错误边界

主操作「运行当前 JS」为深色主按钮(免 Key 可验全链路),AI 按钮次级;四类桥响应(success / javascriptError / layoutError / ok:false)分别渲染,错误原文进状态行与日志;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志区。日志带 [HH:mm:ss] 时间戳,→ 行显示请求摘要、← 行显示响应结果,最新在前、上限 100 条。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoslaygent)+ buildQuickjs* + cinterop + copyKnSo*
│   └── src/
│       ├── ohosMain/cpp/quickjs/             内嵌 QuickJS 2025-04-26 核心(16 文件,含 LICENSE)
│       ├── ohosMain/cpp/laygent_shim.h/.c    C 桥面(trampoline + laygent_undefined)
│       ├── ohosMain/cinterop/quickjs.def     cinterop 声明(headers/headerFilter/package)
│       └── ohosShared/kotlin/…/ohos/         QuickJsHost.kt / DataLayoutScope.kt / Bridge.kt(srcDir 双挂)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.laygent、versionName 0.1.0-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/          linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
        ├── src/main/cpp/CMakeLists.txt       IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp        N-API 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohoslaygent_api.h
        ├── src/main/cpp/types/liblaygent_napi/{index.d.ts, oh-package.json5}
        ├── src/main/module.json5             entry 模块 + ohos.permission.INTERNET
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面(布局演示 / 关于)

ohosShared 是一个物理目录、两个挂载点:用 kotlin.srcDir(...) 分别挂进 ohosArm64Main / ohosX64Main,各目标编译消费自己的 cinterop klib——绕开了 cinterop commonization 与自定义目标组合的接线问题,共享代码零复制。

6.2 构建、签名和安装
./gradlew :laygent:compileKotlinJvm :laygent:jvmTest            # JVM 交叉验证
./gradlew :laygent:compileKotlinOhosArm64 :laygent:compileKotlinOhosX64
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。

hdc list targets -v
hdc -t <设备序列号> install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start -a EntryAbility -b org.terminator.ohos.laygent -m entry

四、完整代码对照

4.1 整体架构

com.zachklipp.laygent/**(上游内核原样编译)
    │ 布局数据模型 / LayoutScope 契约 / 状态机 / LAYGENT_SYSTEM_PROMPT
QuickJsHost + DataLayoutScope + Bridge(ohos 宿主层,Kotlin)
    │ QuickJS 2025-04-26 静态链入 + @CName × 2
libohoslaygent.so(双 ABI)
    │ kGray JSON in / JSON out
liblaygent_napi.so(C++ N-API 包装)
    │ JSON.parse / JSON.stringify
Index.ets(ArkUI 验收页 + OpenAI HTTP)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_laygent、demo 模块移出
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 + ohos kotlinx 制品线钉版
build.gradle.kts(根)版本 0.1.0-ohos.1、全局禁用 Ohos*Test* 任务
laygent/build.gradle.kts纯 KMP:jvm + 双 ohos、jsHostMain/quickjsMain 源集链、serialization 插件模块级补回
laygent/src/composeMain/**上游 Compose/koog 源文件存档(纯移动,不编译,注册即可回接)
laygent/src/commonMain/.../LaygentSystemPrompt.kt新增:系统提示词下沉 commonMain 供 Native 下发
laygent/src/jsHostMain/.../LayoutJsRuntime.ktexpect 从 commonMain 下沉(仅 JVM 侧消费)
laygent/src/jvmTest/.../QuickJsLayoutCrossValidationTest.kt新增:3 项交叉验证冒烟测试
example/nativeApp/build.gradle.kts双 ABI sharedLib、buildQuickjs* C 构建、cinterop、copyKnSo* 回填
nativeApp/.../laygent_shim.h/.cC 桥面:trampoline、JS_UNDEFINED 透出、内存与异常边界
nativeApp/.../QuickJsHost.ktQuickJS 生命周期(initialize/invoke)+ bootstrap JS + 异常分类
nativeApp/.../DataLayoutScope.kt上游 LayoutScope 契约的数据驱动实现(measure-once 等规则)
nativeApp/.../Bridge.kt@CName × 2、op 分发、状态三态、LayoutResult 原生 JSON 嵌入
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(内置/AI/纠错/日志/关于)
README.OpenHarmony_CN.md鸿蒙适配主文档

4.3 关键 API 对照

层次API作用
引擎契约LayoutScope.measure / place / setSize / dpToPx / 4×intrinsic上游布局 API,JS 侧 layoutContext 的 Kotlin 实现
引擎契约LayoutResult / LayoutChildResult布局结果模型(kotlinx-serialization 原生 JSON 嵌入)
引擎契约LAYGENT_SYSTEM_PROMPTLLM 系统提示词(Native 下发,与上游逐字一致)
宿主QuickJsHost.initialize / invokeDoLayout与上游 LayoutJsRuntime actual 同构的两段式
宿主DataLayoutScope数据驱动 scope:measure-once / place-after-measure 规则
NativeLaygentDispatch / LaygentVersionC ABI JSON 导出(@CName × 2)
N-APIlaygentDispatch(req) / laygentVersion()向 ArkTS 暴露能力
ArkTSrunCurrentJs / runAiGeneration / dispatchRunLayout触发执行、LLM 纠错循环、结果渲染

4.4 ArkTS 与 Kotlin 的边界

const resp = JSON.parse(laygentDispatch(JSON.stringify({
  op: 'runLayout', layoutCode: jsCode, dpScale: 3.5,
  constraints: { minWidth: 0, minHeight: 0, maxWidth: 1080, maxHeight: 600 },
  measurables: [{ id: 0, tag: 'item-0', width: 175, height: 175 }]
})));
// resp.ok === true && resp.status === 'success' → resp.result.children 直接渲染
"runLayout" -> {
  val scope = DataLayoutScope(measurables, intrinsicSizes, constraints, dpScale)
  QuickJsHost(QuickJsLayoutNative(scope)).use { host ->
    host.initialize(layoutCode)   // QuickJsException → javascriptError
    host.invokeDoLayout()
  }
  buildJsonObject {
    put("ok", true); put("status", "success")
    put("result", json.encodeToJsonElement(LayoutResult.serializer(), scope.buildLayoutResult()))
  }.toString()
}

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构;LLM 对话留在 ArkTS(HTTP),布局执行留在 Kotlin(QuickJS + 契约),系统提示词由 Kotlin 下发——三层各守一边。

五、关键决策说明

决策 1:真编译内核 + 内嵌同源引擎,而不是提取零依赖内核

零依赖内核提取是早期社区路线。laygent 的内核——数据模型、LayoutScope 契约、系统提示词——本来就不依赖任何断档生态,真正断档的是它的执行载体(JS 宿主)。所以正确的手术不是抽内核,而是给内核换载体:QuickJS 与 JVM 侧 quickjs-kt 同源,JVM 冒烟测试证明同一份契约代码在两个引擎上行为一致。也不用系统 JSVM:那会把布局驱动状态机搬到 C++/ArkTS,Kotlin 库沦为数据搬运工,违背"Kotlin 库真上机"的适配目标。

决策 2:工具链钉 2.2.21-1.0.0 + Gradle 8.14.1,制品线随之换 1.9.1-1.0.0

两条工具链线都实测:2.4.20-0.1.0-07(KStore 钉法)KLIB 编译能过,但 Windows 宿主 cinterop 因精简版 llvm-21-essentials 缺 libclang.dll 而不可用,完整归档 404;2.2.21-1.0.0(qrose 钉法)宿主 llvm 即 cpf-llvm-19(自带 libclang),cinterop 开箱即用。laygent 的 ohos 依赖只有 serialization,1.9.1-1.0.0 线完美配套——路线选择基于当天的制品可达性与工具链完整性,而不是历史结论;Gradle 版本按线区分(2.2.21 线必须 8.x),两条线的经验不能互相套用。

决策 3:expect 下沉 jsHostMain,ohos 侧宿主放 nativeApp

上游唯一的 expect(LayoutJsRuntime)的唯一消费方 LayoutRuntime 本身就是 Compose 存档。把 expect 与 quickjs actual 一起挂到 JVM 侧,ohos 的 commonMain 就不含 expect——不需要为 ohos 硬造一个 actual。ohos 的 JS 宿主实现在 example/nativeApp,与 qrose 把光栅化放 nativeApp 是同一分工:库模块保持纯净,平台实现集中在 app 桥。

决策 4:契约类型 public 化 + 系统提示词下沉 commonMain

桥要实现 public 的 LayoutScope、用 kotlinx-serialization 序列化 LayoutResult、给 ArkTS 下发与上游逐字一致的提示词。因此把契约类型从 internal 放开为 public(最小可见性手术,上游回接无破坏),并把 private 的 LAYGENT_SYSTEM_PROMPT 提升为 commonMain public const(${Constraints.Infinity} → ${Int.MAX_VALUE},求值结果逐字节一致)。LLM 收到的契约与上游一字不差,是本适配"语义未漂移"的另一半证据。

决策 5:JVM 交叉验证 + 上游测试零改动存档

上游 LayoutEngineTest 依赖 compose-ui-test,本构建不编译——原样存档于 src/jvmTestUpstream/,一个字节不改。新增的 3 项冒烟测试跑的是上游 quickjs-kt + commonMain 原件,其中"双 measure → LayoutError"一条还反过来校准了鸿蒙桥的状态分类。交叉验证是"内核没漂"的唯一可信证明:JVM 与 ohos 跑同一份 commonMain,测试零改动才让结论有说服力。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21(Eclipse Adoptium)、DevEco Studio 5.1.0 Release、hvigor / hdc 命令行工具与鸿蒙真机(arm64,dpScale = 3.500)。

6.2 JVM 交叉验证

./gradlew :laygent:jvmTest

3 项用例全部通过(跑的是上游 quickjs-kt 宿主 + commonMain 同一份契约代码):

用例钉住什么
gridLayoutCodeRunsEndToEnd12 个 100×100 item、画布 800×600、dpScale=2:网格算法产出 332×448、12 个 placements、行列坐标逐点断言
invalidJavaScriptSurfacesAsLaygentJsExceptionJS 语法错误 → LaygentJsException(上游 JavascriptError 语义)
doubleMeasureRuleViolationSurfacesAsLayoutError双 measure → 绑定异常原样透出(上游 LayoutError 语义),并由此校准鸿蒙桥的状态分类

ohos .so 里的 commonMain 内核与 JVM 测试跑的是同一份代码;鸿蒙侧 QuickJS 与 JVM 侧 quickjs-kt 是同一引擎家族。这条链就是"引擎语义未漂移"的证据。

6.3 HAP 验证

:laygent:compileKotlinJvm / compileKotlinOhosArm64 / compileKotlinOhosX64 : BUILD SUCCESSFUL
:laygent:jvmTest : tests="3" failures="0" errors="0"
:example:nativeApp:linkDebugSharedOhosArm64 / linkDebugSharedOhosX64 : BUILD SUCCESSFUL
llvm-nm -D(arm64): 00000000002b8904 T LaygentDispatch / 00000000002b8ab0 T LaygentVersion(双 ABI 同构)
BuildNativeWithCmake: finished(双 ABI 编出 liblaygent_napi.so)
CompileArkTS / PackageHap: finished
install bundle successfully / start ability successfully

实测产物:

产物大小
arm64-v8a/libohoslaygent.so7,499,328 B(QuickJS 静态链入)
x86_64/libohoslaygent.so7,174,608 B
arm64-v8a/libc++_shared.so1,850,184 B
x86_64/libc++_shared.so1,731,432 B

6.4 功能用例

  1. 启动后标题区出现 Laygent 0.1.0-ohos.1 · Kotlin/Native ohosArm64/ohosX64(来自真实 Native 调用,非静态文字);
  2. dpScale 自动注入真机屏幕密度(实测 3.500),item 宽高按 dp × dpScale 换算;
  3. 内置-网格:12 个 50dp item 一键运行,画布按 setSize 结果渲染网格 placements,← success 进日志;
  4. 内置-圆形 / 内置-流式换行:分别渲染圆环分布与逐行换行布局,三套算法走同一个 runLayout 入口;
  5. 手改 JS 为非法语法再运行:javascriptError 原文进状态行与日志,不产生渲染;
  6. 构造双 measure 代码:layoutError 与规则违例文案出现在日志(Kotlin 侧规则在真机生效);
  7. 配置 OpenAI Key + 模型后「AI 生成布局」:系统提示词来自 Native 下发,生成代码自动执行,usage.total_tokens 累加显示,失败自动纠错重试(≤3 次);
  8. 每步操作按 [HH:mm:ss] 时间戳滚动留痕,关于页展示分层架构与仓库链接。

6.5 验证结论

布局数据模型、LayoutScope 契约、系统提示词、内嵌 QuickJS 引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的布局生成会话:版本握手、dpScale 注入、内置算法执行、错误分类与 AI 生成闭环(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/laygent.git
cd laygent
export JAVA_HOME="/path/to/jdk-21"
./gradlew :laygent:compileKotlinOhosArm64 :laygent:compileKotlinOhosX64   # KLIB
./gradlew :laygent:jvmTest                                                # 3 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.laygent -m entry
hdc shell hilog | grep Laygent

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :laygent 真正编译成 ohosArm64 / ohosX64 KLIB、让 :example:nativeApp 真正链接出双 ABI 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. ClassNotFoundException: …cli.utilities.MainKt:~/.konan/kotlin-native-prebuilt-*/konan/lib 被外部磁盘清理清空(目录还在、provisioned.ok 还在,KGP 跳过重新 provision)——删掉对应 prebuilt 目录让 Gradle 重新下载,必要时 ./gradlew --stop 清掉缓存了旧状态的 daemon;
  4. 工具链钉版跟着 cinterop 可用性走:2.4.20 线宿主 llvm 是精简版 essentials(无 libclang.dll),cinterop 直接 UnsatisfiedLinkError;补拷 LLVM 19 的 libclang 又与按 LLVM 21 编译的 clangstubs.dll 二进制不兼容——换 2.2.21-1.0.0 线(宿主 llvm 即 cpf-llvm-19)才是正解;
  5. serialization 制品线的元数据版本必须与 Kotlin 线配套:1.11.0-0.1.0-07 的 2.4.0 元数据 2.2.21 读不了,换 1.9.1-1.0.0(换线不换语义);
  6. Gradle 版本按工具链线区分:2.2.21 线必须 8.14.1(上游 wrapper 是 9.5.0,要降);
  7. Gradle Exec 在 Windows 上给 clang 传 -DCONFIG_VERSION="2025-04-26" 会被 CRT 参数解析吞掉内嵌引号,必须写成 \\\"…\\\" 转义形式;
  8. 多文件 C 编译忘传 -c 时 clang 直接进入链接阶段,报 undefined symbol: main(Scrt1.o)——报错位置离病因很远,先查编译参数;
  9. 本版 cinterop 把不透明结构体(typedef struct JSRuntime JSRuntime;)生成在 cnames.structs 包:import cnames.structs.JSContext 才能拿到类型;同时 kotlinx.cinterop.get(CPointer 下标操作符)没 import 时的连带报错是 MatchGroup 一类的怪异信息,极具迷惑性;
  10. ohosMain 中间源集 + cinterop 的 commonization 接线不稳:放弃 commonization,共享源码放 src/ohosShared/kotlin 用 kotlin.srcDir(...) 分别挂进 ohosArm64Main / ohosX64Main,各自消费自己的 cinterop klib;
  11. kotlinx-serialization-json 的 long 是 JsonPrimitive 扩展属性,JsonObject 没有 long(name)——要自己写 helper;
  12. 交叉验证实证:quickjs-kt 绑定函数抛出的异常不会被包成 QuickJsException,上游因此把 scope 规则违例归 LayoutError——鸿蒙桥必须用同样的分类(标记前缀跨 JS 边界),否则错误状态与上游漂移;
  13. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  14. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填;
  15. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(napi so / KN so / libc++_shared)缺一不可。

8.2 已知问题

  • Compose UI 与 koog LLM 框架没有 ohos 制品,相关源码隔离进 src/composeMain 存档不编译;鸿蒙侧由 ArkTS 页面 + Native 桥按同一协议复现完整管线,等鸿蒙 CMP 工具链对齐后评估接回;
  • Demo 的 item 是数据驱动的固定尺寸(dp × dpScale),不做文本自适应测量——验收目标是布局算法引擎本身,不是测量管线;
  • LLM 对话需要外网与 OpenAI Key;无 Key 时用三个内置算法仍可验收全部引擎链路;
  • ohos 侧 QuickJS 为 2025-04-26 发行版,与 JVM 侧 quickjs-kt 1.0.3 内嵌的引擎同源不同版(语义一致由冒烟测试背书);
  • demo / demo-shared / demo-web 与 docs/ 上游产物留盘不进构建;MAINTAINERS.md 为上游发布文档,本 fork 不发 Maven Central;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.laygent 匹配的签名。

8.3 未来优化方向

  • 评估 .har 形态发布,让 ArkTS 工程直接依赖 so 三件套;
  • runLayout 增加批量提议 / 自动纠错闭环(当前由 ArkTS 驱动单步);
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本;
  • 关注鸿蒙 CMP 工具链进展,评估 composeMain 存档的接回时机。

九、总结

9.1 核心难点回顾

上游 laygent commonMain(纯内核 + Compose/koog 混合)
    → 验证 ohos 制品线(serialization 1.9.1-1.0.0 就位;compose/koog/quickjs-kt 断档)
    → 源码集手术:composeMain 隔离 + expect 下沉 + 契约 public 化 + 提示词下沉
    → 工具链对齐 Kotlin 2.2.21-1.0.0(cinterop 宿主 libclang 完整性决定)+ Gradle 8.14.1
    → ohosArm64 / ohosX64 KLIB(零 ohos actual)
    → QuickJS 2025-04-26 内嵌:KN clang 交叉编译静态库 + cinterop + C 桥面
    → QuickJsHost / DataLayoutScope(上游契约的 ohos 宿主)
    → C ABI(@CName × 2)+ 状态三态(对齐上游 LayoutCodeProposalResult)
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析 + OpenAI HTTP + 纠错循环
    → ArkUI 真机双 Tab 验收(内置算法 / AI 生成 / 错误边界 / 日志)

9.2 封装层次

  • 内核层:上游原版布局数据模型、LayoutScope 契约、LaygentStatus、LAYGENT_SYSTEM_PROMPT;
  • 宿主层:上游原版 quickjs actual(JVM 交叉验证)+ fork 的 QuickJsHost / DataLayoutScope(ohos 宿主,实现同一 public 契约);
  • 引擎层:QuickJS 2025-04-26 静态链入双 ABI 动态库,经 C 桥面暴露最小交互面;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、LLM 对话与纠错循环、JSON 校验、结果渲染、日志与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先验证生态制品线与工具链完整性,再定适配路线:compose/koog/quickjs-kt 断档决定了"内核真编译 + 宿主换载体"的手术范围;而 2.4.20 与 2.2.21 两条工具链线的取舍不由版本新旧决定,由 cinterop 的宿主 libclang 完整性决定——路线选择必须基于当天的实测,两条线的 Gradle 经验不能互相套用;
  2. 契约一致是跨语言复现的生命线:public LayoutScope + public 提示词让 ohos 宿主与上游共用同一份"法律文本";systemPrompt 由 Native 下发后,LLM 在哪个平台上生成的代码都遵循同一套 API;
  3. 交叉验证是"内核没漂"的唯一可信证明:3 项冒烟测试跑的是上游 quickjs-kt + commonMain 原件,还反向校准了桥的错误分类(双 measure → LayoutError);JVM 与 ohos 跑同一份 commonMain,测试才让"换载体不换语义"有说服力。

9.4 适配成果

当前适配已完成:

  • :laygent 双 ohos 目标 KLIB 编译(commonMain 纯内核零重写、零 ohos actual,JVM 目标保留交叉验证);
  • QuickJS 2025-04-26 内嵌 + cinterop 的 ohos JS 宿主(QuickJsHost / DataLayoutScope),上游契约全量实现;
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证双 ABI 全局 T 符号);
  • DevEco Stage ArkUI 双 Tab 验收页面(内置算法、AI 生成纠错闭环、token 统计、操作日志、关于页);
  • 3 项 JVM 交叉验证全绿 + 双 ABI 动态库(arm64 7.5MB / x64 7.2MB,QuickJS 静态链入)、HAP 构建、签名、设备安装和真机验证;
  • AtomGit 项目文档、OpenHarmony README(中英双份)和真机效果图。

适配过程 FAQ

以下问题来自 ohosArm64 目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。

Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?

  • 确认 DevEco Studio 版本与工程 build-profile.json5 的 compatibleSdkVersion 一致;
  • 在 File > Settings > Build, Execution, Deployment > Ohpm 配置可用的 ohpm 仓库镜像或代理;
  • 删除 oh_modules、oh-package-lock.json5 与 .hvigor 缓存后重新 Sync;
  • 公司网络需要代理时,同时给 ohpm 与 hvigor 配置 http.proxy。

Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?

  • 先在 example 目录执行 ./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用 linkDebugSharedOhosX64);
  • 确认 libxxx.so 与 libc++_shared.so 已复制到 ohos/entry/libs/<abi>/;
  • 检查 entry/build-profile.json5 的 abiFilters 与设备 ABI 是否一致(真机 arm64-v8a,模拟器 x86_64);
  • 清理 entry/.cxx 与 entry/build 后重试。

Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?

  • 确认 CMake 的 add_library(<target> SHARED ...) 名称与 import ... from 'libxxx_napi.so' 完全一致;
  • 解包 HAP,确认 libs/<abi>/libxxx_napi.so 已打包;
  • 删除 entry/build、entry/.cxx 与 oh_modules 后重新构建;
  • 检查 module.json5 的 srcEntry 与页面路径是否正确。

Q4:真机安装报 signature verification failed 或 install failed?

  • 在 DevEco Studio 的 Signing Configs 配置证书、Profile 与密码;
  • bundleName 必须与签名 profile 中的 bundle name 一致;
  • 设备需加入 profile 的 deviceIds,调试证书有效期需覆盖当前时间;
  • 用 hdc list targets 确认设备在线,再执行 hdc install -r xxx-signed.hap。

Q5:应用启动白屏、闪退或 NAPI 调用崩溃?

  • 用 hdc hilog | grep -i "napi\|kotlin" 查看原生日志;
  • 确认 libc++_shared.so 与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致;
  • 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用 XxxFree 释放;
  • 在 DevEco 的 Native 调试配置中附加 lldb,定位空指针与越界访问。

Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?

  • 使用鸿蒙发行版 Kotlin(如 2.2.21-1.0.0),并在 settings.gradle.kts 配置 eazytec 仓库;
  • Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
  • 确认 kotlin { ohosArm64() } 已应用,且 local.properties 的 SDK 路径有效;
  • 清理 ~/.gradle/caches 中对应版本的 Kotlin/Native 缓存后重试。

Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?

  • 对齐 DevEco Studio、HarmonyOS SDK 与 build-profile.json5 中的 compatibleSdkVersion;
  • 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免 modelVersion 与 IDE 不匹配;
  • 命令行构建时设置 DEVECO_SDK_HOME 指向 DevEco 的 sdk 目录。

Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?

  • 使用 DevEco 自带的 node 与 hvigor:
    "$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...;
  • 设置 JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME;
  • 加上 --no-daemon 排除守护进程缓存影响,并清理 entry/build 与 .hvigor。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐