开源鸿蒙平台 KMP 三方库「laygent」适配全流程
本文记录
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 原样编码的LayoutResultJSON; - 状态分类与上游对齐:
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 Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Eclipse Adoptium 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64;jvm() 保留作交叉验证 |
| Gradle | 8.14.1(2.2.21 旧线必须 8.x,wrapper 从上游 9.5.0 降下来) |
| kotlinx | serialization-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 product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.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.kt | quickjs-kt | 原样保留,JVM 交叉验证宿主 |
Laygent.kt / LaygentState.kt / LayoutRuntime.kt / LayoutCodeGenerator.kt / TokenRecordingLlmClient.kt | Compose + koog | 物理移入 src/composeMain/ 存档(纯移动) |
jvmTest/LayoutEngineTest.kt | compose-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 方法 | 覆盖内核 |
|---|---|---|
LaygentVersion | laygentVersion() | fork 版本握手(真机版本行的数据来源) |
LaygentDispatch | laygentDispatch(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.toml | Kotlin 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.kt | expect 从 commonMain 下沉(仅 JVM 侧消费) |
laygent/src/jvmTest/.../QuickJsLayoutCrossValidationTest.kt | 新增:3 项交叉验证冒烟测试 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、buildQuickjs* C 构建、cinterop、copyKnSo* 回填 |
nativeApp/.../laygent_shim.h/.c | C 桥面:trampoline、JS_UNDEFINED 透出、内存与异常边界 |
nativeApp/.../QuickJsHost.kt | QuickJS 生命周期(initialize/invoke)+ bootstrap JS + 异常分类 |
nativeApp/.../DataLayoutScope.kt | 上游 LayoutScope 契约的数据驱动实现(measure-once 等规则) |
nativeApp/.../Bridge.kt | @CName × 2、op 分发、状态三态、LayoutResult 原生 JSON 嵌入 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面(内置/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_PROMPT | LLM 系统提示词(Native 下发,与上游逐字一致) |
| 宿主 | QuickJsHost.initialize / invokeDoLayout | 与上游 LayoutJsRuntime actual 同构的两段式 |
| 宿主 | DataLayoutScope | 数据驱动 scope:measure-once / place-after-measure 规则 |
| Native | LaygentDispatch / LaygentVersion | C ABI JSON 导出(@CName × 2) |
| N-API | laygentDispatch(req) / laygentVersion() | 向 ArkTS 暴露能力 |
| ArkTS | runCurrentJs / 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 同一份契约代码):
| 用例 | 钉住什么 |
|---|---|
gridLayoutCodeRunsEndToEnd | 12 个 100×100 item、画布 800×600、dpScale=2:网格算法产出 332×448、12 个 placements、行列坐标逐点断言 |
invalidJavaScriptSurfacesAsLaygentJsException | JS 语法错误 → 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.so | 7,499,328 B(QuickJS 静态链入) |
x86_64/libohoslaygent.so | 7,174,608 B |
arm64-v8a/libc++_shared.so | 1,850,184 B |
x86_64/libc++_shared.so | 1,731,432 B |
6.4 功能用例
- 启动后标题区出现
Laygent 0.1.0-ohos.1 · Kotlin/Native ohosArm64/ohosX64(来自真实 Native 调用,非静态文字); dpScale自动注入真机屏幕密度(实测 3.500),item 宽高按 dp × dpScale 换算;- 内置-网格:12 个 50dp item 一键运行,画布按
setSize结果渲染网格 placements,← success进日志; - 内置-圆形 / 内置-流式换行:分别渲染圆环分布与逐行换行布局,三套算法走同一个
runLayout入口; - 手改 JS 为非法语法再运行:
javascriptError原文进状态行与日志,不产生渲染; - 构造双 measure 代码:
layoutError与规则违例文案出现在日志(Kotlin 侧规则在真机生效); - 配置 OpenAI Key + 模型后「AI 生成布局」:系统提示词来自 Native 下发,生成代码自动执行,
usage.total_tokens累加显示,失败自动纠错重试(≤3 次); - 每步操作按
[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 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:laygent真正编译成ohosArm64/ohosX64KLIB、让:example:nativeApp真正链接出双 ABI 动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;ClassNotFoundException: …cli.utilities.MainKt:~/.konan/kotlin-native-prebuilt-*/konan/lib被外部磁盘清理清空(目录还在、provisioned.ok还在,KGP 跳过重新 provision)——删掉对应 prebuilt 目录让 Gradle 重新下载,必要时./gradlew --stop清掉缓存了旧状态的 daemon;- 工具链钉版跟着 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)才是正解;
- serialization 制品线的元数据版本必须与 Kotlin 线配套:
1.11.0-0.1.0-07的 2.4.0 元数据 2.2.21 读不了,换1.9.1-1.0.0(换线不换语义); - Gradle 版本按工具链线区分:2.2.21 线必须 8.14.1(上游 wrapper 是 9.5.0,要降);
- Gradle Exec 在 Windows 上给 clang 传
-DCONFIG_VERSION="2025-04-26"会被 CRT 参数解析吞掉内嵌引号,必须写成\\\"…\\\"转义形式; - 多文件 C 编译忘传
-c时 clang 直接进入链接阶段,报undefined symbol: main(Scrt1.o)——报错位置离病因很远,先查编译参数; - 本版 cinterop 把不透明结构体(
typedef struct JSRuntime JSRuntime;)生成在cnames.structs包:import cnames.structs.JSContext才能拿到类型;同时kotlinx.cinterop.get(CPointer 下标操作符)没 import 时的连带报错是MatchGroup一类的怪异信息,极具迷惑性; - ohosMain 中间源集 + cinterop 的 commonization 接线不稳:放弃 commonization,共享源码放
src/ohosShared/kotlin用kotlin.srcDir(...)分别挂进ohosArm64Main/ohosX64Main,各自消费自己的 cinterop klib; - kotlinx-serialization-json 的
long是JsonPrimitive扩展属性,JsonObject没有long(name)——要自己写 helper; - 交叉验证实证:quickjs-kt 绑定函数抛出的异常不会被包成 QuickJsException,上游因此把 scope 规则违例归 LayoutError——鸿蒙桥必须用同样的分类(标记前缀跨 JS 边界),否则错误状态与上游漂移;
- KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填;- 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 三条经验
- 先验证生态制品线与工具链完整性,再定适配路线:compose/koog/quickjs-kt 断档决定了"内核真编译 + 宿主换载体"的手术范围;而 2.4.20 与 2.2.21 两条工具链线的取舍不由版本新旧决定,由 cinterop 的宿主 libclang 完整性决定——路线选择必须基于当天的实测,两条线的 Gradle 经验不能互相套用;
- 契约一致是跨语言复现的生命线:public
LayoutScope+ public 提示词让 ohos 宿主与上游共用同一份"法律文本";systemPrompt由 Native 下发后,LLM 在哪个平台上生成的代码都遵循同一套 API; - 交叉验证是"内核没漂"的唯一可信证明: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。
参考文档
更多推荐


所有评论(0)