开源鸿蒙平台 KMP_CMP 三方库「Clikt」适配全流程
本文记录
clikt接入 OpenHarmony 的完整过程,覆盖 KMP 工程盘点、ohosArm64
目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、命令行解析示例、
HAP 构建、签名和真机验收。Clikt 本身是 Kotlin Multiplatform 命令行解析库,不是 Compose Multiplatform UI
组件库。因此本次适配复用 KMP 侧的真实解析器和测试模型,使用 ArkUI 作为
OpenHarmony 宿主页面,验证同一份 Kotlin 代码可以从 JVM 进入 OpenHarmony ARM64
运行时,而不是在 ArkTS 页面中重新实现一套解析规则。
项目地址: AtomGit/oh-tpc/clikt
上游仓库: ajalt/clikt
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做开源鸿蒙平台 KMP/CMP 适配
Clikt 的核心能力是把命令行参数解析为类型安全的 Kotlin 值,并统一处理默认值、
选项、flag、校验和错误退出码。原仓库面向 JVM、JS、Wasm、Apple、Linux 和 Windows
等平台,OpenHarmony 侧需要增加真正的 Kotlin/Native 目标,才能证明解析器进入了
OpenHarmony 运行时。
如果只把页面重写成 ArkTS,再在页面里手工判断 --name、--count 和 --verbose,
页面虽然可以显示几个字符串,却无法证明共享 Kotlin 解析器、Native 动态库和 N-API
桥接已经连通。本次适配需要解决以下问题:
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | KMP 模块默认没有 OpenHarmony 目标,必须加入 ohosArm64() 才能生成 KLIB 和动态库。 |
| 工具链不一致 | Kotlin/Native、Gradle、OpenHarmony LLVM、Native SDK 和 DevEco Hvigor 必须使用匹配版本。 |
| 平台边界 | ArkTS 不能直接持有 Kotlin data class,需要使用 C ABI、C++ N-API 和 JSON。 |
| 参数生命周期 | 输入框内容、快捷预设和解析结果必须保持一致,避免页面展示的参数与实际解析参数不同。 |
| 错误可见性 | Clikt 的校验错误、退出码和 Native 异常都要返回页面,不能只在 Native 日志里失败。 |
| 内存释放 | Kotlin/Native 返回的 C 字符串必须由明确的 CliktFree 入口释放。 |
| 交付链路复杂 | Native 动态库、CMake、HAP、签名、设备安装和 ARM64 依赖需要分别验证。 |
因此,本项目把适配边界放在三个地方:Kotlin/Native 目标配置、C ABI/N-API 桥接层和
ArkUI 示例页面。参数解析和错误语义仍由 Clikt 维护,ArkTS 只负责输入、按钮交互、
结果展示和页面状态。
1.2 Clikt 示例提供的能力
example/shared 中的公共示例使用 Clikt 核心 API 定义了一个简单命令:
--name/-n:字符串选项,默认值为OpenHarmony;--count/-c:整数选项,默认值为1,有效范围为1..5;--verbose/-v:flag 选项,用于切换详细输出;CommandLineParser.tokenize:把输入框中的命令行文本拆成参数列表;CliktError:将参数错误转换为可展示的错误文本和退出码;runChecks:在 JVM 示例和 OpenHarmony Native 页面中复用相同的验收检查。
解析结果使用以下结构:
| 字段 | 含义 |
|---|---|
id | 当前结果的来源标识,例如 command 或 preset-1 |
argv | 实际参与解析的参数文本 |
output | 成功时的命令输出,失败时的 Clikt 错误信息 |
success | 是否解析和执行成功 |
exitCode | 成功为 0,参数错误通常为 1 |
页面中的三个按钮职责不同:
| 按钮 | 行为 | 示例 |
|---|---|---|
| 默认参数 | 写入预设并立即解析 | --name OpenHarmony --count 1 |
| 详细模式 | 写入带 flag 的预设并立即解析 | --name Clikt --verbose |
| 解析命令 | 只解析输入框当前内容 | 用户可以自行编辑任意参数 |
1.3 实现适配矩阵
| 维度 | 适配方式 |
|---|---|
| 代码复用 | 公共命令、Clikt 参数声明、tokenize、错误处理和检查逻辑由 Kotlin 共享。 |
| 平台目标 | 为核心 clikt 和示例工程加入 ohosArm64,生成 libclikt.so。 |
| 桥接稳定 | 使用少量 C ABI 函数和 UTF-8 JSON,避免把 Kotlin 对象地址交给 ArkTS。 |
| UI 完整 | 页面支持输入参数、默认预设、详细预设、手动解析、成功结果和错误结果。 |
| 可测试 | JVM 测试、Native 链接、ELF 依赖、HAP 构建、设备安装分别验收。 |
| 签名安全 | 源码只保留签名入口,证书、profile、p12 和密码由开发者本机配置。 |
| 仓库规范 | 项目、文章、截图和示例链接统一使用 AtomGit。 |
本项目没有把 Compose Multiplatform UI 强行移植到 ArkUI。KMP 负责共享 Clikt
解析能力,ArkUI 负责 OpenHarmony 宿主页面;这种边界可以让其他 KMP/CMP 应用复用
Clikt 核心 API,并自行决定页面实现方式。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 KMP 模块、解析示例和 OpenHarmony 工程边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、独立 example 工程和 Native 构建任务
第 3 阶段:解析与序列化 ── 建立 CliktExample、错误状态和稳定 JSON 契约
第 4 阶段:原生桥接 ── Kotlin/Native C ABI、C++ N-API、类型检查和内存释放
第 5 阶段:ArkUI 页面 ── 输入框、预设按钮、手动解析、状态提示和错误展示
第 6 阶段:示例与验证 ── HAP 构建、签名、设备安装、按钮交互和效果图
每个阶段都使用真实产物作为下一阶段输入:先用 JVM 验证 Clikt 解析,再把同一份
commonMain 代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,
最后安装签名 HAP 观察设备页面上的解析结果。
三、逐步实现过程
第 1 阶段:项目初始化
1.1 盘点公共 API 和工程边界
项目采用清晰的 KMP、Native 和 DevEco 分层:
clikt/ Clikt 核心多平台库和 ohosArm64 目标
clikt-mordant/ Mordant 输出扩展
clikt-mordant-markdown/ Markdown 输出扩展
example/shared/ 公共命令示例和 JVM 验收测试
example/nativeApp/ ohosArm64 Kotlin/Native 动态库
example/ohosApp/ DevEco Stage 工程、C++ N-API 和 ArkUI 页面
scripts/ Native、HAP、签名工程和依赖检查脚本
docs/openharmony/ OpenHarmony 验收记录和真机效果图
example 是独立 Gradle 工程,不把 DevEco 工程直接当作根工程的 Kotlin 子模块。
这样可以分别执行 Gradle 和 Hvigor,也可以把 ohosApp 复制到另一个目录后在
DevEco Studio 中配置签名。
1.2 固定工具链和版本矩阵
本次适配使用以下版本约定:
| 项目 | 配置 | 用途 |
|---|---|---|
| Kotlin Multiplatform | 2.2.21-1.0.0 | JVM、Kotlin/Native 和 KLIB |
| Gradle | 8.14.3 | 根工程和 example 工程构建 |
| JDK | 21 | Gradle、Kotlin 编译和 Native 任务 |
| OpenHarmony 目标 | ohosArm64 | ARM64 真机动态库 |
| DevEco product | 6.0.0(20) | 工程兼容和目标版本 |
| DevEco Studio | 6.0.0 Release | IDE 版本,负责工程同步、HAP 构建与签名 |
| ABI | arm64-v8a | HAP 原生库架构 |
执行 Gradle 脚本前先选择 JDK 21:
export JAVA_HOME="/path/to/jdk-21"
java -version
OpenHarmony SDK 版本满足编译条件,只说明工程可以生成 HAP;应用是否能安装和运行,
还要由 DevEco 和 ARM64 设备单独验证。
1.3 创建 OpenHarmony 示例目录
示例页面围绕“当前输入、解析结果、退出码、检查状态和最近一次解析”组织:
标题区 Clikt 工作台 / KMP NATIVE
输入区 命令行输入框、解析次数和快捷示例
操作区 默认参数 / 详细模式 / 解析命令
结果区 SUCCESS 或 EXIT、输出文本和实际 argv
检查区 公共检查通过数量和 Native 错误文本
快捷按钮用于快速准备可重复的输入,解析按钮用于验证任意手动编辑的命令行。每次
解析都会更新状态文本和刷新次数,避免“参数没有变化所以看起来没有响应”。
第 2 阶段:目标与依赖打通
2.1 在核心模块加入 ohosArm64
clikt/build.gradle.kts 保留所有原有目标,并增加 OpenHarmony ARM64:
kotlin {
jvm()
js { nodejs() }
wasmJs { nodejs() }
wasmWasi { nodejs() }
linuxX64()
linuxArm64()
ohosArm64()
mingwX64()
macosX64()
macosArm64()
iosArm64()
iosX64()
iosSimulatorArm64()
}
适配的关键是让公共 commonMain 代码在 ohosArm64 上可编译,而不是复制一份只
能在 ArkTS 中运行的解析实现。Mordant 模块仍然保持原有目标矩阵,因为公开 Mordant
制品没有对应的 OpenHarmony 变体。
2.2 独立 example 工程消费 Clikt
example/shared 使用平台无关的公共源码,并通过项目属性消费本地 Clikt 制品:
kotlin {
explicitApi()
jvm()
ohosArm64()
sourceSets {
commonMain.dependencies {
api("com.github.ajalt.clikt:clikt-core:${property("cliktVersion")}")
}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
根脚本先发布适配后的本地 Maven 制品,示例工程再使用相同版本执行 JVM 测试和
Native 链接。这样可以确认“被消费的 artifact”与刚刚修改的源码一致。
2.3 构建 Native 动态库
example/nativeApp 生成一个受 linker map 约束的 shared library:
kotlin {
ohosArm64 {
binaries.sharedLib {
baseName = "clikt"
linkerOpts("--entry=0", "--version-script=${project.file(
"src/ohosArm64Main/linker/shared-library.map"
)}")
linkerOpts("-lace_napi.z", "-luv", "-lhilog_ndk.z")
}
}
}
prepareOhos 在链接成功后复制两个构建产物:
entry/libs/arm64-v8a/libclikt.so
entry/src/main/cpp/include/libclikt_api.h
动态库、生成头文件和 HAP 属于构建产物,仓库会忽略它们;每台开发机都可以从源码
重新生成与本机 SDK 匹配的文件。
第 3 阶段:解析与序列化
3.1 为什么需要统一 JSON 契约
ArkTS、C++ 和 Kotlin/Native 不能直接共享 Kotlin 对象,因此边界使用 UTF-8 JSON:
TextInput command line
↓ string
CliktClient.ets
↓ N-API run(commandLine)
C++ N-API entry
↓ C string
Kotlin/Native CliktRun
↓ CliktExamples.execute
CliktExample JSON
↓
ArkUI result card
页面只消费 JSON,不复制 option().default()、.int()、.flag() 和 .check() 的
业务规则。这样 JVM 测试和设备页面都使用同一份 Clikt 解析逻辑。
3.2 公共 DemoCommand
示例命令位于 example/shared/src/commonMain:
private class DemoCommand : CoreCliktCommand(name = "clikt-demo") {
private val name by option("--name", "-n").default("OpenHarmony")
private val count by option("--count", "-c")
.int()
.default(1)
.check("count must be between 1 and 5") { it in 1..5 }
private val verbose by option("--verbose", "-v").flag()
var output: String = ""
private set
override fun run() {
val greeting = List(count) { "Hello, $name!" }.joinToString(" ")
output = if (verbose) "[verbose] $greeting" else greeting
}
}
CliktExamples.execute 先调用 CommandLineParser.tokenize,再把参数列表交给
command.parse(argv)。成功结果带 exitCode = 0;CliktError 会被转换为失败结果,
保留错误消息和状态码。
3.3 结果 JSON
正常解析结果保持稳定字段:
{
"id": "command",
"argv": "--name HarmonyOS --count 2",
"output": "Hello, HarmonyOS! Hello, HarmonyOS!",
"success": true,
"exitCode": 0
}
Native 异常则返回:
{
"error": "Native Clikt error"
}
ArkTS 侧会把缺少正常字段的响应转换为结构完整的失败结果,避免页面渲染时访问
undefined 字段。
3.4 公共检查
CliktExamples.runChecks() 覆盖七项检查:
- 默认选项可以解析;
- 自定义名称和次数可以解析;
- 整数选项控制问候语重复次数;
verboseflag 会改变输出前缀;count = 0会被校验拒绝;- 公共 API 不依赖平台类型;
- OpenHarmony 桥接只传递不可变 JSON 值。
页面将结果展示为 7/7 公共检查通过,这个数字来自 Native 返回,而不是页面写死。
第 4 阶段:原生桥接(技术难点)
4.1 Kotlin/Native 对象不能直接交给 ArkTS
Kotlin/Native 的 CliktExample 属于 Kotlin 运行时对象。ArkTS 只能接收 JavaScript
值,C++ 不能把 Kotlin 对象地址直接当成 JavaScript 对象。
最终采用三层桥接:
ArkTS
│ JSON string
▼
C++ N-API entry
│ const char*
▼
Kotlin/Native C ABI
│ CliktExamples
▼
UTF-8 JSON + explicit free
4.2 方案对比
| 方案 | 优点 | 缺点 | 选用 |
|---|---|---|---|
| 直接导出 Kotlin 对象 | 代码少 | ABI、生命周期和类型不可控 | ❌ |
| 只导出解析后的字符串 | 实现简单 | 页面无法展示参数、错误和退出码 | ❌ |
| C ABI + JSON | 边界清晰、易扩展、易调试 | 有一次序列化开销 | ✅ |
| 在 ArkTS 重写完整解析器 | 页面调用简单 | KMP 和 ArkTS 逻辑容易分叉 | ❌ |
4.3 Kotlin/Native 导出函数
NativeBridge.kt 对外导出四个 C ABI 符号:
@CName("CliktCatalog")
public fun catalogNative(): CPointer<ByteVar> = response { ... }
@CName("CliktRun")
public fun runNative(commandLine: CPointer<ByteVar>?): CPointer<ByteVar> = response {
CliktExamples.execute(commandLine?.toKString().orEmpty()).toJson()
}
@CName("CliktRunChecks")
public fun checksNative(): CPointer<ByteVar> = response { ... }
@CName("CliktFree")
public fun freeNative(pointer: CPointer<ByteVar>?) {
if (pointer != null) nativeHeap.free(pointer.rawValue)
}
返回值使用 Native heap 分配的、以 0 结尾的 C 字符串。每一块返回缓冲区都由
CliktFree 释放。
4.4 C++ N-API 方法分发
C++ 注册三个 ArkTS 方法:
napi_property_descriptor methods[] = {
{"getCatalog", nullptr, Catalog, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"run", nullptr, Run, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
napi_default, nullptr},
};
run 先读取 ArkTS 字符串长度,再写入可变 std::string 缓冲区,最后调用
CliktRun。ReadNative 创建 ArkTS 字符串后立即调用 CliktFree,保证成功和异常
路径都不泄漏 Native 缓冲区。
CMake 将 Kotlin/Native 动态库作为 imported library:
add_library(clikt SHARED IMPORTED)
set_target_properties(clikt PROPERTIES
IMPORTED_LOCATION
"${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libclikt.so")
add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE clikt libace_napi.z.so)
4.5 N-API 生命周期
ArkTS run(commandLine)
│
▼
napi_get_value_string_utf8
│
▼
CliktRun(commandLine)
│
▼
napi_create_string_utf8
│
▼
CliktFree(nativeBuffer)
│
▼
return JS string
C++ 负责完成跨语言字符串转换和 Native 释放,ArkTS 页面不需要知道 Kotlin/Native
的堆实现。
第 5 阶段:ArkUI 页面
5.1 页面状态
Index.ets 保存输入参数、解析结果、检查状态、错误文本和解析次数:
@State private commandLine: string = '--name OpenHarmony --count 1';
@State private selected: CliktExample = emptyExample();
@State private status: string = '正在连接 Native Clikt';
@State private errorText: string = '';
@State private parseCount: number = 0;
@State private lastAction: string = '等待操作';
页面加载时会自动执行一次 refresh()。输入框的 onChange 只更新命令行文本,真正
点击“解析命令”时才调用 Native run,避免每次键入字符都触发完整解析。
5.2 三个按钮的实现
private usePreset(commandLine: string): void {
this.refresh(commandLine);
}
Button('默认参数')
.onClick(() => this.usePreset('--name OpenHarmony --count 1'))
Button('详细模式')
.onClick(() => this.usePreset('--name Clikt --verbose'))
Button('解析命令')
.onClick(() => this.refresh(this.commandLine))
预设按钮将参数直接传给 refresh,不会在状态尚未刷新时读取旧的输入框值。手动解析
按钮始终使用当前输入内容,并在结果卡片中显示“解析完成 · 已刷新第 N 次”。
5.3 成功与失败展示
成功时页面显示 SUCCESS、Clikt 输出和实际 argv;失败时显示 EXIT 1、错误文本
和公共检查状态。Native 返回格式错误或 JSON 解析异常也会被 CliktClient.ets
转换为失败结果,不会让页面因为字段缺失直接崩溃。
第 6 阶段:示例与验证
6.1 example 工程结构
example/
├── shared/
│ ├── src/commonMain/.../CliktExamples.kt
│ └── src/commonTest/.../CliktExamplesTest.kt
├── nativeApp/
│ ├── src/ohosArm64Main/.../NativeBridge.kt
│ └── src/ohosArm64Main/linker/shared-library.map
└── ohosApp/
├── AppScope/
├── entry/src/main/cpp/
│ ├── CMakeLists.txt
│ └── napi_init.cpp
└── entry/src/main/ets/
├── pages/Index.ets
└── clikt/CliktClient.ets
shared 验证公共 Clikt 模型,nativeApp 产生 Native 动态库,ohosApp 负责 ArkUI
页面和 N-API 加载。三者边界清晰,任何一层失败都能单独定位。
6.2 原生模块注册
napi_init.cpp 通过 napi_module_register 注册 entry 模块,类型声明位于:
example/ohosApp/entry/src/main/cpp/types/libentry/index.d.ts
ArkTS 使用:
import cliktNative from 'libentry.so';
6.3 Native 动态库准备
执行:
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
脚本依次执行根模块测试、示例测试、linkDebugSharedOhosArm64 和 prepareOhos。
成功后生成:
example/ohosApp/entry/libs/arm64-v8a/libclikt.so
example/ohosApp/entry/src/main/cpp/include/libclikt_api.h
还可以检查 ARM64 ELF 的强依赖:
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
6.4 构建、签名和安装
未配置签名时,可以在 DevEco Studio 构建本地调试 HAP;真机安装需要有效签名。配置
签名后执行:
python3 scripts/prepare-signing-project.py /absolute/path/clikt-signing
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
scripts/build-hap.sh /absolute/path/clikt-signing
产物位于:
entry/build/default/outputs/default/entry-default-signed.hap
安装并启动:
HDCP=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc
"$HDCP" install -r \
/absolute/path/clikt-signing/entry/build/default/outputs/default/entry-default-signed.hap
"$HDCP" shell aa start \
-a EntryAbility -b com.github.ajalt.clikt.sample -m entry
签名证书、profile、p12 和密码只保存在本机,不要提交到 AtomGit。
四、完整代码对照
4.1 整体架构
TextInput / preset button
│ command-line string
▼
CliktClient.ets
│ libentry.so
▼
C++ N-API
│ C ABI
▼
libclikt.so
│ Kotlin/Native
▼
CliktExamples.execute
│ CoreCliktCommand.parse
▼
CliktExample -> JSON -> ArkUI result card
4.2 文件清单
| 文件 | 职责 |
|---|---|
clikt/src/commonMain/... | Clikt 核心解析 API |
example/shared/.../CliktExamples.kt | 公共命令示例、JSON 前的结果模型和自检 |
example/shared/.../CliktExamplesTest.kt | JVM 解析和错误验收 |
example/nativeApp/.../NativeBridge.kt | C ABI、JSON 返回和内存释放 |
example/ohosApp/.../CliktClient.ets | N-API 调用、JSON 解析和结果兜底 |
example/ohosApp/.../Index.ets | 输入、预设、解析按钮和结果页面 |
example/ohosApp/.../napi_init.cpp | N-API 导出、参数读取和 C 字符串转换 |
scripts/build-openharmony.sh | 测试、Native 链接和产物复制 |
scripts/build-hap.sh | 已配置签名工程的 HAP 构建 |
docs/openharmony/VALIDATION.md | 自动检查和真机验收记录 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| Kotlin | CommandLineParser.tokenize | 将命令行文本拆成参数列表 |
| Kotlin | CoreCliktCommand.parse | 使用 Clikt 解析选项和 flag |
| Kotlin | CliktError | 返回错误文本和退出码 |
| Native | CliktRun | 返回一条命令的 JSON 结果 |
| Native | CliktRunChecks | 返回公共检查 JSON |
| N-API | run | 向 ArkTS 暴露解析方法 |
| ArkTS | refresh | 更新页面输入、结果和状态 |
4.4 ArkTS 与 Kotlin 的边界
ArkTS 只负责输入和界面生命周期:
const result: CliktExample = run(this.commandLine);
this.selected = result;
Kotlin 负责参数语义、默认值、类型转换、范围校验和输出:
val command = DemoCommand()
command.parse(argv)
CliktExample("command", argv.joinToString(" "), command.output, true, 0)
两者之间只传输字符串和 JSON,不传输 Kotlin 对象、ArkTS class 实例或未校验的动态
结构。
五、关键决策说明
决策 1:把 ohosArm64 加入核心构建约定
只有真正链接 ohosArm64 动态库,才能证明共享 Clikt 代码可以进入 OpenHarmony
运行时。只添加一个 ArkTS 页面不能代表 KMP 库已经完成适配。
决策 2:独立消费者必须通过构建产物消费
example 单独解析 Clikt,ohosApp 单独接收 .so 和头文件,避免根工程编译通过
却无法在 DevEco 中打包。
决策 3:JSON 作为跨语言数据契约
JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,并允许后续增加可信字段而
不暴露内部对象布局。
决策 4:桥接层只开放四个 C ABI 入口
目录、解析、自检和释放已经覆盖示例所需能力;减少 ABI 符号可以降低 Native 生命周期
和兼容风险。
决策 5:快捷预设和手动解析分开
预设按钮用于快速验证固定参数,解析按钮用于验证用户输入。两者都调用同一个
refresh,不会出现页面逻辑和库逻辑各自维护一套解析器的情况。
决策 6:把库验证和设备验证分开
JVM 测试验证 Clikt 规则,Native 链接验证 ABI,Hvigor 验证 HAP,真机验证安装和
ArkUI 页面。每一层都有明确的失败边界。
六、测试与验证
6.1 测试环境
本次真机验证使用:
- macOS;
- JDK 21;
- Kotlin Multiplatform
2.2.21-1.0.0; - DevEco Studio 及 OpenHarmony ARM64 Native SDK;
- DevEco 自带 Node.js 和 Hvigor;
- 已签名
entry-default-signed.hap; - USB 连接的 HarmonyOS ARM64 真机;
hdc设备序列号FMR0223825079397。
6.2 静态检查与单元测试
./gradlew :clikt:jvmTest :test:jvmTest
(cd example && ./gradlew :shared:jvmTest)
测试覆盖默认选项、自定义名称、重复次数、flag、整数范围错误和公共检查列表。
6.3 原生桥接和 HAP 验证
./scripts/build-openharmony.sh
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
本地 DevEco 构建命令:
PATH=/Applications/DevEco-Studio.app/Contents/tools/node/bin:$PATH \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw \
assembleApp --no-daemon
验证结果:
Hvigor BUILD SUCCESSFUL
entry-default-signed.hap generated
ARM64 native library packaged
6.4 功能验证用例
用例 1:默认页面和自检
启动应用后页面显示默认命令 --name OpenHarmony --count 1、SUCCESS 和公共检查
通过数量。初始结果由 Native run 生成。
用例 2:默认参数按钮
点击“默认参数”,输入框恢复默认命令并重新解析,结果为:
Hello, OpenHarmony!
用例 3:详细模式按钮
点击“详细模式”,结果为:
[verbose] Hello, Clikt!
这个用例同时验证 flag 参数已经由 Clikt 处理,而不是页面手工拼接前缀。
用例 4:自定义参数和解析按钮
输入:
--name OpenHarmony --count 2
点击“解析命令”后,页面显示两次问候语、实际 argv 和“解析完成 · 已刷新第 N 次”。
用例 5:无效参数
输入 --count 0,页面显示 EXIT 1 和 count must be between 1 and 5。Native
错误不会导致 ArkUI 页面崩溃。
用例 6:重复解析
连续点击“解析命令”,结果内容不变但刷新次数递增,证明按钮事件被执行且页面有明确
反馈。这个场景解决了初始默认参数与再次解析结果相同、用户误以为按钮无响应的问题。
6.5 验证结论
公共 JVM 测试、Kotlin/Native ARM64 链接、ELF 依赖检查、Hvigor HAP 构建、签名安装
和真机页面交互均已完成。真机页面可以执行默认、详细和手动参数解析,说明从 Clikt
核心到 ArkTS 展示的链路可用。
七、运行效果
7.1 真机截图

截图中可以看到:
- 页面标题为“Clikt 工作台”;
- 副标题说明这是命令行解析示例;
- 顶部标识显示
KMP NATIVE; - 输入框当前为
--name OpenHarmony --count 2; - 快捷按钮提供“默认参数”和“详细模式”;
- 主按钮为“解析命令”;
- 结果卡片显示两次
Hello, OpenHarmony!; - 状态为
SUCCESS,公共检查为7/7 公共检查通过; - 解析结果下方显示实际参与解析的参数。
7.2 命令速查
# 根工程测试和 OpenHarmony Native 准备
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
# 单独运行共享测试
(cd example && ./gradlew :shared:jvmTest)
# 设备依赖检查
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
# 在已配置签名的工程中构建 HAP
./scripts/build-hap.sh /absolute/path/clikt-signing
# 安装和启动
hdc install -r \
/absolute/path/clikt-signing/entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.github.ajalt.clikt.sample -m entry
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配:必须把共享 Clikt 代码真正编译成
ohosArm64
动态库。 - N-API 不负责业务判断:C++ 只做参数检查、字符串转换和释放,参数语义放在
Kotlin Clikt 命令中。 - Native 字符串必须及时释放:每次
CliktRun、CliktCatalog和
CliktRunChecks返回后都要配对调用CliktFree。 - 解析按钮需要可见反馈:默认参数已经在页面启动时解析,再次点击若只显示相同
文本,用户会误以为没有响应,因此页面增加了刷新次数和状态提示。 - JDK、Node 和 DevEco 工具链要分开确认:Gradle 使用 JDK 21,Hvigor 使用
DevEco 自带 Node,避免系统 Node 版本导致 Native 构建异常。 - 签名材料不能进入源码:证书、profile、p12 和密码必须放在独立的本机工程中。
8.2 已知问题
- 当前示例只构建
ohosArm64和arm64-v8a,不覆盖 OpenHarmony x86 或其他 ABI; - Clikt 是命令行解析库,ArkUI 页面是验收宿主,不提供 Compose Multiplatform UI 组件;
- Native 和 HAP 构建依赖本机 OpenHarmony SDK、DevEco 工具链和签名配置;
- 页面采用同步 Native 调用,复杂或长时间运行的命令应在业务层增加异步调度;
- 文章中的签名配置只适用于本地开发机,不能直接复制到其他环境。
8.3 未来优化方向
- 增加
Flow<CliktExample>或异步执行接口,支持更复杂的命令任务; - 为 Compose Multiplatform 宿主补充共享状态卡片示例,保持 ArkUI 与 CMP 的模型一致;
- 增加命令行引号、转义和多行输入的交互测试;
- 增加设备能力、签名状态和 Native 库版本的诊断页面;
- 在持续集成中加入
ohosArm64链接、ELF 依赖检查和 HAP 未签名构建任务; - 将 AtomGit 文档、示例截图和版本发布说明纳入同一套发布流程。
九、总结
9.1 核心难点回顾
本次适配真正需要处理的不是一个按钮,而是一条完整跨端链路:
命令行文本
→ ArkUI 输入和页面状态
→ C++ N-API
→ Kotlin/Native C ABI
→ KMP Clikt 核心解析器
→ JSON
→ ArkUI 结果页面
9.2 封装层次
- KMP 层:定义选项、默认值、类型转换、flag、范围检查和错误语义;
- Native 层:生成 ARM64 动态库,并通过 C ABI 输出有限入口;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理输入框、快捷预设、解析动作、结果状态和错误提示;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 先让共享 Clikt 模型在 JVM 和 Native 通过,再接入 ArkUI;
- 用 JSON 和少量 C ABI 代替跨语言对象传递;
- 把自动测试、HAP 构建和真机交互分别记录,避免把“编译成功”误认为“设备运行链路可用”。
9.4 适配成果
当前 Clikt OpenHarmony 适配已完成:
clikt核心模块新增ohosArm64目标;example/shared复用真实 Clikt parser、默认选项、flag 和范围校验;example/nativeApp生成 ARM64libclikt.so和 C ABI 头文件;- Kotlin/Native + C ABI + C++ N-API JSON 桥接;
- ArkUI 页面支持输入、默认参数、详细模式和手动解析;
- 解析成功、退出码、错误文本和公共检查状态可视化;
- HAP 构建、签名安装和 Mate 60 Pro 真机验证;
- AtomGit 项目文档、OpenHarmony 验收记录和真机效果图;
- 仓库中的代码托管地址、项目文档和发布链接已统一使用 AtomGit。
适配过程 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)