开源鸿蒙平台 KMP_CMP 三方库「assertk」适配全流程
本文记录
assertk接入 OpenHarmony 的完整过程,覆盖 KMP 工程盘点、ohosArm64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 交互页面、HAP 构建、签名隔离和真机验收准备。
assertk的核心是 Kotlin Multiplatform 断言库。本文沿用 KMP/CMP 适配文章的组织方式,把公共断言逻辑编译到 OpenHarmony ARM64 Native,再由 ArkTS 页面提供真实的“实际值 / 期望值 / 断言关系”交互。页面显示的结果来自 Kotlin/Native 中真正执行的assertkAPI,不是在 ArkTS 里重新实现一套判断。
项目地址: AtomGit/oh-tpc/assertk
上游仓库: jzbrooks/assertk-lint
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做开源鸿蒙平台 KMP/CMP 适配
assertk 是面向 Kotlin 的 fluent assertion library,公共实现位于 KMP 的 commonMain,可以在测试代码中使用 assertThat、isEqualTo、containsExactly、hasSize 和 isTrue 等断言。原项目的主要交付对象是 Kotlin/JVM 和其他 KMP 平台,OpenHarmony 应用不能直接把 Kotlin 对象交给 ArkTS。
如果只重新写一个 ArkTS 页面并在页面里比较两个数字,无法证明 assertk 核心库已经进入 OpenHarmony 运行时。一次完整适配需要同时验证共享 Kotlin 逻辑、ARM64 Native 动态库、C ABI、C++ N-API 和页面交互。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | KMP 模块默认没有 OpenHarmony Native 目标,必须加入 ohosArm64() 才能生成 KLIB。 |
| 构建边界 | DevEco Stage 工程和 Gradle/Kotlin 工程工具链不同,需要用独立 example 工程连接两侧。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin data class,需要通过 C ABI、C++ N-API 和 JSON 传输结果。 |
| 内存生命周期 | Kotlin/Native 返回的字符串由 Native heap 分配,C++ 必须复制后调用释放函数。 |
| 示例可演示性 | 固定的 6/6 检查只能验证加载,不能体现用户输入和断言失败信息。 |
| 参数安全 | ArkTS 数字、C++ double 和 Kotlin Int 必须统一校验整数、范围和操作编号。 |
| 交付链路复杂 | Native 库、CMake、HAP、签名、设备 ABI 和 hdc 安装需要分别验收。 |
因此,本项目把适配边界放在三个地方:KMP 的 ohosArm64 目标、Kotlin/Native C ABI 与 C++ N-API 桥接、ArkTS 交互页面。断言语义仍由 assertk 提供,ArkTS 只负责输入、操作选择、结果展示和错误提示。
1.2 库提供的能力
assertk 公共模块本身提供丰富的 fluent assertion API。本次 OpenHarmony 示例选择一组容易在触屏页面中演示的能力,并保留公共检查作为桥接验收:
assertThat(actual).isEqualTo(expected):等值断言;assertThat(actual).isGreaterThan(expected):大于断言;assertThat(actual).isLessThan(expected):小于断言;assertThat(actual).isGreaterThanOrEqualTo(expected):大于等于断言;assertThat(actual).isLessThanOrEqualTo(expected):小于等于断言;containsExactly、hasSize、isTrue、startsWith:公共 API 自检;assertFailure和hasMessage:验证失败信息可以被捕获和检查。
页面的五种关系和 Native 调用如下:
| 页面操作 | 操作编号 | assertk 调用 | 示例 |
|---|---|---|---|
| 等于 | 0 | isEqualTo | 42 = 42 |
| 大于 | 1 | isGreaterThan | 18 > 10 |
| 小于 | 2 | isLessThan | 3 < 8 |
| 大于等于 | 3 | isGreaterThanOrEqualTo | 10 >= 10 |
| 小于等于 | 4 | isLessThanOrEqualTo | 3 <= 8 |
失败结果保留 assertk 的比较信息,例如:
expected to be greater than:<10> but was:<3>
这样示例页展示的是断言库的真实错误文本,而不是页面自行拼接的提示。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 断言语义、公共 API 检查、失败消息和 JSON 结果由 Kotlin 共享。 |
| 平台目标 | 为示例 Native 模块加入 ohosArm64,生成 libassertk.so。 |
| 桥接稳定 | 只导出少量 C ABI 函数,跨边界传输不可变 UTF-8 JSON。 |
| UI 完整 | 页面支持输入、关系选择、预设、执行、通过和失败展示。 |
| 可测试 | JVM 测试、Native 链接、ELF 依赖、ArkTS 编译和 HAP 构建分别验证。 |
| 签名安全 | 源码保持未签名,证书和密码只配置在仓库外的独立工程。 |
| 仓库规范 | 项目、文章和文档链接统一使用 AtomGit。 |
页面是 OpenHarmony 的交互式验收宿主,
assertk公共 API 仍保持平台无关。其他 KMP/CMP 应用可以直接复用断言库,再根据自己的 UI 技术栈决定结果展示方式。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 assertk API、测试模块和 OpenHarmony 示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、独立 example 工程和 Native 构建任务
第 3 阶段:断言与序列化 ── 建立示例门面、交互结果模型和稳定 JSON 契约
第 4 阶段:原生桥接 ── Kotlin/Native C ABI、C++ N-API、参数检查和内存释放
第 5 阶段:交互页面 ── ArkTS 输入、关系选择、预设、执行和结果状态
第 6 阶段:示例与验证 ── Native 依赖、HAP 构建、效果图、签名隔离和验收清单
每个阶段都使用真实构建产物作为下一阶段输入:先用 JVM 验证共享断言门面,再把同一份 Kotlin 代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,最后通过页面输入验证 Native 断言结果。
三、逐步实现过程
第 1 阶段:项目初始化
1.1 盘点公共 API 和工程边界
项目采用与参考 KMP/CMP 工程一致的边界,核心库、示例门面、Native 动态库和 DevEco 宿主分别放在不同目录:
assertk/ KMP 断言核心和平台实现
assertk-coroutines/ 协程断言扩展
example/shared/ 公共示例门面和 JVM 验收测试
example/nativeApp/ ohosArm64 Kotlin/Native 动态库
example/ohosApp/ DevEco Stage 工程、CMake、N-API、ArkTS UI
scripts/ Native、HAP、依赖检查和签名工程脚本
docs/openharmony/ 验收记录和效果图
example 是独立 Gradle 工程,不把 DevEco 工程当作 Kotlin 子模块。这样可以分别执行 Gradle 和 Hvigor,也可以把 ohosApp 复制到另一个目录后配置签名。
1.2 固定工具链和版本矩阵
本项目使用以下工具链约定:
| 项目 | 配置 | 用途 |
|---|---|---|
| Kotlin Multiplatform | 以 gradle/libs.versions.toml 为准 | JVM、Kotlin/Native 和 KLIB |
| JDK | 21 | Gradle、Kotlin 编译和 Native 任务 |
| OpenHarmony 目标 | ohosArm64 | ARM64 真机动态库 |
| DevEco 工程 | Stage + API 20 SDK | HAP 编译和打包 |
| ABI | arm64-v8a | entry 模块 Native 库目录 |
| 页面桥接 | C++ N-API | ArkTS 加载 libentry.so |
执行 Gradle 脚本前先选择 JDK 21:
export JAVA_HOME="/path/to/jdk-21"
java -version
JDK 版本、Kotlin/Native 编译器和 DevEco Native SDK 必须保持可兼容。页面可以在没有真机时完成编译,但最终设备运行还需要 ARM64 ABI 和匹配的签名 HAP。
1.3 创建 OpenHarmony 示例目录
页面围绕一次真实断言组织,而不是只显示固定测试数量:
标题区 Assertk 工作台 / Kotlin/Native / NATIVE READY
输入区 实际值、期望值
关系区 =、>、<、>=、<=
执行区 运行断言
预设区 42 = 42、18 > 10、3 < 8
结果区 PASS 或 FAIL,以及 assertk 返回的 message
用户可以先点击预设快速看到成功场景,再修改输入值触发失败场景。输入校验在调用 Native 前完成,失败消息则来自 Kotlin/Native 的 assertk 异常文本。
第 2 阶段:目标与依赖打通
2.1 加入 ohosArm64 目标
example/nativeApp 只需要链接公共示例门面和 OpenHarmony ARM64 目标:
plugins {
kotlin("multiplatform")
}
kotlin {
ohosArm64 {
binaries.sharedLib {
baseName = "assertk"
linkerOpts(
"--entry=0",
"--version-script=${project.file("src/ohosArm64Main/linker/shared-library.map")}",
)
linkerOpts("-lace_napi.z", "-luv", "-lhilog_ndk.z")
}
}
}
公共模块保留 JVM 测试,Native 模块通过 commonMain.dependencies 使用 project(":shared")。本次 OpenHarmony 依赖组合没有 JS/Wasm 变体,因此适配构建不额外配置这些目标。
2.2 Native focused build 的作用
nativeApp 使用 linker map 限制导出符号,只把 N-API 需要的函数放入动态库:
AssertkCatalog
AssertkGet
AssertkRunChecks
AssertkEvaluate
AssertkFree
这样 ArkTS 只能通过明确的 ABI 入口获取目录、公共检查和交互结果,AssertkExamples 的 Kotlin 对象和内部实现不会成为不受控的 ABI。
2.3 配置仓库和独立消费工程
example/settings.gradle.kts 使用本地 Maven、社区 Maven、Maven Central 和 Gradle Plugin Portal:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
mavenLocal()
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
}
}
示例共享层通过项目依赖消费核心库:
sourceSets {
commonMain.dependencies {
implementation(project(":assertk"))
}
}
如果应用使用发布产物,业务 KMP 模块可以在 commonMain 中依赖对应坐标:
commonMain.dependencies {
implementation("com.willowtreeapps.assertk:assertk:<version>")
}
OpenHarmony 示例的 Native 产物仍然由当前仓库构建,以确保头文件和 .so 来自同一次链接。
2.4 通过构建产物消费共享库
prepareOhos 在 Native 链接成功后复制动态库和生成头文件:
val prepareOhos by tasks.registering(Copy::class) {
dependsOn("linkDebugSharedOhosArm64")
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libassertk.so")
into("libs/arm64-v8a")
}
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libassertk_api.h")
into("src/main/cpp/include")
}
into(rootProject.layout.projectDirectory.dir("../example/ohosApp/entry"))
}
生成物会进入:
example/ohosApp/entry/libs/arm64-v8a/libassertk.so
example/ohosApp/entry/src/main/cpp/include/libassertk_api.h
Native 动态库和头文件属于可重复生成的构建产物,签名材料和本地 HAP 不应提交到 AtomGit。
第 3 阶段:断言与序列化
3.1 为什么需要统一 JSON 契约
ArkTS、C++ 和 Kotlin/Native 不能直接共享 Kotlin data class。本项目使用稳定的 UTF-8 JSON 作为边界:
ArkTS 输入 operation / actual / expected
↓ N-API evaluate()
C++ 参数检查和类型转换
↓ C ABI AssertkEvaluate()
Kotlin AssertkExamples.evaluate()
↓ EvaluationResult.toJson()
Kotlin/Native JSON + explicit free
↓ napi_create_string_utf8()
ArkUI 结果卡片
页面只解析结果,不复制 assertk 的比较语义。这样 JVM 测试、Native 示例和 ArkTS 页面使用的是同一套 Kotlin 断言实现。
3.2 AssertionExample 和 EvaluationResult
公共示例门面定义了三种跨边界数据记录:
public data class AssertionExample(
val id: String,
val expression: String,
val expected: String,
)
public data class CheckReport(
val passed: Boolean,
val checks: List<String>,
val failure: String? = null,
)
public data class EvaluationResult(
val passed: Boolean,
val operation: String,
val actual: Int,
val expected: Int,
val message: String,
)
EvaluationResult 只包含基本类型和字符串,适合在 C ABI 中编码成 JSON;它不把 Assert<T>、异常对象或 Kotlin 集合引用暴露给 ArkTS。
3.3 evaluate 和 JSON
共享门面把页面操作编号映射为真实的 assertk API:
public fun evaluate(operation: Int, actual: Int, expected: Int): EvaluationResult {
val operationName = when (operation) {
0 -> "等于"
1 -> "大于"
2 -> "小于"
3 -> "大于等于"
4 -> "小于等于"
else -> error("unsupported assertion operation: $operation")
}
return try {
when (operation) {
0 -> assertThat(actual, name = "实际值").isEqualTo(expected)
1 -> assertThat(actual, name = "实际值").isGreaterThan(expected)
2 -> assertThat(actual, name = "实际值").isLessThan(expected)
3 -> assertThat(actual, name = "实际值").isGreaterThanOrEqualTo(expected)
4 -> assertThat(actual, name = "实际值").isLessThanOrEqualTo(expected)
else -> error("unsupported assertion operation: $operation")
}
EvaluationResult(true, operationName, actual, expected, "断言通过")
} catch (error: Throwable) {
EvaluationResult(false, operationName, actual, expected, error.message ?: "断言失败")
}
}
返回 JSON 的字段固定为 passed、operation、actual、expected 和 message:
{
"passed": false,
"operation": "大于",
"actual": 3,
"expected": 10,
"message": "expected to be greater than:<10> but was:<3>"
}
3.4 自检和错误边界
AssertkExamples.runChecks() 保留六项公共 API 验收:
- 标量相等断言通过;
- 有序集合
containsExactly通过; - 集合
hasSize通过; - 布尔值
isTrue通过; - 文本
startsWith通过; assertFailure捕获失败并检查hasMessage。
交互测试另外覆盖等于成功和大于失败两个场景。Native 异常会包装成 {"error":"..."},C++ 创建 ArkTS 字符串后立即调用 AssertkFree,页面再把错误显示到输入错误区域。
第 4 阶段:原生桥接(技术难点)
4.1 Kotlin/Native 对象不能直接交给 ArkTS
Kotlin/Native 的 EvaluationResult 属于 Kotlin 运行时对象。ArkTS 只能安全接收 JavaScript 值,C++ 也不能把 Kotlin 对象地址直接当成 JavaScript 对象。
最终采用三层桥接:
ArkTS
│ operation + actual + expected
▼
C++ N-API entry
│ int32_t
▼
Kotlin/Native C ABI
│ AssertkExamples.evaluate
▼
UTF-8 JSON + explicit free
4.2 方案对比
| 方案 | 优点 | 缺点 | 选用 |
|---|---|---|---|
| 直接导出 Kotlin 对象 | 页面调用代码少 | ABI、生命周期和类型不可控 | ❌ |
| 只导出布尔值 | 实现简单 | 页面看不到 assertk 失败原因 | ❌ |
| C ABI + JSON | 边界清晰、易调试、易扩展 | 有一次序列化开销 | ✅ |
| 在 ArkTS 重写比较逻辑 | 页面调用简单 | KMP 和 ArkTS 逻辑容易分叉 | ❌ |
4.3 Kotlin/Native 导出函数
Native 侧使用 @CName 导出函数,所有字符串都由统一的 response 包装:
@CName("AssertkEvaluate")
public fun evaluateNative(
operation: Int,
actual: Int,
expected: Int,
): CPointer<ByteVar> = response {
AssertkExamples.evaluate(operation, actual, expected).toJson()
}
@CName("AssertkFree")
public fun freeNative(pointer: CPointer<ByteVar>?) {
if (pointer != null) nativeHeap.free(pointer.rawValue)
}
response 会把异常变成 JSON,textBuffer 使用 nativeHeap 分配 NUL 结尾的 UTF-8 缓冲区。这样 C++ 侧的每条调用路径都可以执行相同的复制和释放顺序。
4.4 C++ N-API 方法分发
C++ 为 getCatalog、getExample、runChecks 和 evaluate 注册四个 ArkTS 方法:
napi_property_descriptor methods[] = {
{"getCatalog", nullptr, Catalog, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"getExample", nullptr, GetExample, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"evaluate", nullptr, Evaluate, nullptr, nullptr, nullptr,
napi_default, nullptr},
};
Evaluate 要求三个参数,并校验操作编号在 0..4、实际值和期望值是有穷整数且适合 int32_t:
if (!ReadInteger(env, args[0], operation) || operation < 0 || operation > 4 ||
!ReadInteger(env, args[1], actual) || !ReadInteger(env, args[2], expected)) {
napi_throw_type_error(env, nullptr,
"evaluate arguments must be supported integers");
return nullptr;
}
C++ 调用 Native 后使用 napi_create_string_utf8 创建 ArkTS 字符串,再立即释放返回指针:
const auto text = AssertkEvaluate(operation, actual, expected);
auto result = ReadText(env, reinterpret_cast<const char*>(text));
AssertkFree(text);
return result;
CMake 把 Kotlin/Native 动态库作为 imported library:
add_library(assertk SHARED IMPORTED)
set_target_properties(assertk PROPERTIES
IMPORTED_LOCATION
"${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libassertk.so")
add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE assertk libace_napi.z.so)
4.5 N-API 生命周期
ArkTS evaluate(operation, actual, expected)
│
▼
ReadInteger + range check
│
▼
AssertkEvaluate(operation, actual, expected)
│
▼
napi_create_string_utf8(...)
│
▼
AssertkFree(nativeBuffer)
│
▼
return JSON string
C++ 负责跨语言字符串转换和 Native 释放,ArkTS 客户端只负责 JSON 类型解析和界面状态,不需要知道 Kotlin/Native 的堆实现。
第 5 阶段:交互页面
5.1 ArkTS 调用 evaluate
ArkTS 客户端先解析 JSON,并把 Native 错误转换为异常:
export function evaluate(
operation: number,
actual: number,
expected: number,
): EvaluationResult {
return parse<EvaluationResult>(
assertkNative.evaluate(operation, actual, expected),
);
}
页面不在 Index.ets 中实现等于、大于或小于的业务规则,只把用户选择的操作编号传给共享 Kotlin 门面。
5.2 输入校验
页面接受有符号 32 位整数:
private parseInteger(text: string): number | null {
const normalized = text.trim();
if (normalized.length === 0 || !/^-?\d+$/.test(normalized)) {
return null;
}
const value = Number.parseInt(normalized, 10);
return value < -2147483648 || value > 2147483647 ? null : value;
}
空文本、小数和溢出数字会在跨越 N-API 前显示“请输入有效的 32 位整数”。C++ 层仍然会再次做类型检查,避免其他调用方绕过页面校验。
5.3 ArkUI 页面状态
Index.ets 保存用户输入、当前操作、执行状态、错误文本和结果对象:
@State private selectedOperation: number = 0;
@State private actualText: string = '42';
@State private expectedText: string = '42';
@State private result: EvaluationResult | null = null;
@State private errorText: string = '';
@State private running: boolean = false;
点击“运行断言”后,页面清空旧结果,校验输入,调用 evaluate,最后根据 result.passed 显示绿色 PASS 或红色 FAIL。
5.4 页面交互预设
页面提供三个预设,分别用于快速验证等于、大于和小于:
42 = 42:成功的等于断言;18 > 10:成功的大于断言;3 < 8:成功的小于断言。
修改任意输入值即可看到失败结果。例如把 18 > 10 的实际值改成 3,Native 会返回失败消息,页面结果卡片显示 FAIL 和 expected to be greater than。
第 6 阶段:示例与验证
6.1 example 工程结构
example/
├── shared/
│ ├── src/commonMain/kotlin/org/assertk/example/AssertkExamples.kt
│ └── src/commonTest/kotlin/org/assertk/example/AssertkExamplesTest.kt
├── nativeApp/
│ ├── src/ohosArm64Main/kotlin/org/assertk/example/NativeBridge.kt
│ └── src/ohosArm64Main/linker/shared-library.map
└── ohosApp/
├── AppScope/
├── entry/src/main/cpp/
│ ├── CMakeLists.txt
│ └── napi_init.cpp
└── entry/src/main/ets/
├── assertk/AssertkClient.ets
└── pages/Index.ets
shared 验证公共断言模型,nativeApp 产生 ARM64 动态库,ohosApp 负责 N-API 注册、ArkUI 页面和 HAP 打包。三者的边界清晰,失败可以定位到具体层次。
6.2 原生模块注册
napi_init.cpp 通过 napi_module_register 注册 entry 模块,ArkTS 类型声明位于:
example/ohosApp/entry/src/main/cpp/types/libentry/index.d.ts
页面客户端使用:
import assertkNative from 'libentry.so';
index.d.ts 明确声明四个返回字符串的方法,后续可在 SDK 加强 N-API 校验时继续复用这份类型契约。
6.3 Native 动态库准备
执行一键脚本:
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
脚本依次执行根模块 JVM 测试、publishToMavenLocal、示例共享测试和 nativeApp:prepareOhos。成功后生成:
example/ohosApp/entry/libs/arm64-v8a/libassertk.so
example/ohosApp/entry/src/main/cpp/include/libassertk_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 构建、签名和安装
未配置签名时,可以构建未签名 HAP:
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
scripts/build-hap.sh "$PWD/example/ohosApp"
产物位于:
example/ohosApp/entry/build/default/outputs/default/entry-default-unsigned.hap
需要签名时,先把工程复制到仓库外:
python3 scripts/prepare-signing-project.py /absolute/path/assertk-signing
在复制出的工程中配置证书、profile、p12、别名和密码,再运行:
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
scripts/build-hap.sh /absolute/path/assertk-signing
签名证书和密码只保存在本机,不要提交到 AtomGit。当前源码工程保持未签名,签名只在需要真机安装或交付 HAP 时执行。
四、完整代码对照
4.1 整体架构
ArkTS TextInput + operation buttons
│ operation / actual / expected
▼
AssertkClient.ets -> libentry.so
│ N-API
▼
AssertkEvaluate / AssertkRunChecks
│ C ABI
▼
libassertk.so
│ Kotlin/Native
▼
AssertkExamples -> assertk.assertThat -> JSON
│
▼
PASS / FAIL result card
4.2 文件清单
| 文件 | 职责 |
|---|---|
assertk/src/commonMain | 公共断言 API 和实现 |
example/shared/.../AssertkExamples.kt | 公共示例、检查和交互门面 |
example/shared/.../AssertkExamplesTest.kt | JVM 共享验收测试 |
example/nativeApp/.../NativeBridge.kt | C ABI、JSON 编码和 Native 释放 |
example/nativeApp/.../shared-library.map | 控制导出的 C 符号 |
example/ohosApp/.../napi_init.cpp | N-API 导出和参数检查 |
example/ohosApp/.../CMakeLists.txt | 导入 .so 并链接 entry 模块 |
example/ohosApp/.../AssertkClient.ets | JSON 解析和 ArkTS 类型封装 |
example/ohosApp/.../Index.ets | 交互式断言页面 |
scripts/build-openharmony.sh | JVM、Native 测试和产物准备 |
scripts/build-hap.sh | DevEco HAP 构建 |
scripts/prepare-signing-project.py | 创建仓库外签名工程 |
docs/openharmony/VALIDATION.md | 自动检查和验收记录 |
docs/openharmony/assertk-workbench.jpg | 示例页面效果图 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| Kotlin | assertThat(actual).isEqualTo(expected) | 执行等于断言 |
| Kotlin | AssertkExamples.evaluate | 把操作编号映射为公共断言 API |
| Native | AssertkEvaluate | 返回一个 JSON 结果 |
| N-API | evaluate | 校验参数并把结果交给 ArkTS |
| ArkTS | parse<EvaluationResult> | 解析 JSON 并处理 Native 错误 |
| ArkTS | runAssertion | 管理输入、执行状态和结果卡片 |
4.4 ArkTS 与 Kotlin 的边界
ArkTS 只负责读取输入和调用 Native:
const actual = this.parseInteger(this.actualText);
const expected = this.parseInteger(this.expectedText);
this.result = evaluate(this.selectedOperation, actual, expected);
Kotlin 负责断言语义和失败文本:
assertThat(actual, name = "实际值").isGreaterThan(expected)
两者之间只传输整数和 JSON,不传递 Kotlin 对象、异常实例或未校验的动态结构。
五、关键决策说明
决策 1:Native 示例单独使用 ohosArm64
OpenHarmony 真机需要 ARM64 Native 库。把目标放在独立 nativeApp 中,可以让根工程继续发布原有 KMP 产物,也能明确记录动态库的导出边界。
决策 2:独立消费者必须通过构建产物消费
example 单独解析共享层,ohosApp 单独接收 .so 和头文件,避免根工程 JVM 编译通过却无法在 DevEco 中打包的问题。
决策 3:JSON 作为跨语言数据契约
JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,同时允许后续增加结果字段,不暴露 Kotlin 内部对象布局。
决策 4:桥接层开放五个 C ABI 入口
目录、单条示例、公共检查、交互评估和释放函数已经覆盖示例所需能力;减少无关符号可以降低 Native 生命周期和兼容风险。
决策 5:失败断言返回结果而不是抛出页面错误
断言失败正是 assertk 的核心用途。evaluate 捕获断言异常并返回 passed: false 和 message,页面才能同时演示成功和失败两种交互。
决策 6:页面校验和 C++ 校验双层保留
页面校验能给用户及时反馈,C++ 校验能保护 Native ABI 不被其他调用方传入小数、无穷值或越界操作编号。两层校验使用同一套 Int32 范围。
六、测试与验证
6.1 测试环境
本次自动验证使用:
- macOS;
- Homebrew JDK 21;
- Kotlin Multiplatform 项目配置版本;
- DevEco Studio 自带 OpenHarmony Native SDK;
- ARM64 Native 目标
ohosArm64; - Hvigor Stage HAP 构建工具。
本次完成的是 JVM、Native、ELF 依赖和 HAP 构建验证;签名证书和真机安装需要在具备对应设备与签名材料后继续执行。
6.2 静态检查与单元测试
./gradlew :assertk:jvmTest :assertk-coroutines:jvmTest
(cd example && ./gradlew :shared:jvmTest)
测试覆盖公共断言目录、六项 API 检查、交互等于成功和大于失败。git diff --check 也用于检查文章、代码和文档没有尾随空格或补丁格式问题。
6.3 原生桥接和 HAP 验证
(cd example && ./gradlew :shared:jvmTest :nativeApp:prepareOhos)
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
scripts/build-hap.sh "$PWD/example/ohosApp"
验证结果:
libassertk.so: 0 unresolved strong imports
Hvigor BUILD SUCCESSFUL
entry-default-unsigned.hap generated
源码工程在 Hvigor 中会提示 No signingConfig found for product default,这是未配置签名材料的预期提示,不影响 C++、ArkTS 和 HAP 编译。
6.4 功能验证用例
用例 1:默认页面
启动页面后显示 Assertk 工作台、NATIVE READY、默认值 42 和 42,默认操作为等于。点击运行后,Native 返回 passed: true,页面显示绿色 PASS。
用例 2:快速预设
依次点击 42 = 42、18 > 10 和 3 < 8,确认输入值和选中关系同步更新,运行后分别进入等于、大于和小于断言。
用例 3:失败断言
输入实际值 3、期望值 10,选择 > 并运行。页面应显示红色 FAIL,并展示 expected to be greater than:<10> but was:<3> 等 Native 消息。
用例 4:边界输入
输入空文本、小数、超过 int32_t 范围的数字,页面应在调用 Native 前显示输入错误,不应生成错误的断言结果。
用例 5:五种操作
分别验证 =、>、<、>=、<= 的成功和失败组合,确认操作编号从 ArkTS 经过 C++ 传到 Kotlin 后没有偏移。
用例 6:Native 结果释放
连续点击运行断言,检查页面结果稳定更新。C++ 每次调用后都执行 AssertkFree,不在 ArkTS 侧保留 Kotlin/Native 缓冲区指针。
6.5 验证结论
公共 JVM 测试、Kotlin/Native ARM64 链接、动态库依赖检查、ArkTS 编译和未签名 HAP 构建均已完成。效果图展示了已编译页面的交互布局。签名安装和设备侧触屏操作需要在配置签名材料和 ARM64 真机后执行,不能由未签名构建替代。
七、运行效果
7.1 示例页面效果图

图中可以看到:
- 页面标题为“Assertk 工作台”;
- 顶部显示
NATIVE READY,说明页面定位为 Native 桥接示例; - 中间卡片有实际值、期望值和断言关系选择;
=、>、<、>=、<=五个操作以可点击按钮呈现;- “运行断言”按钮触发真正的 Kotlin/Native
assertk调用; - 下方提供三组快速预设,便于演示交互;
- 页面底部说明结果通过 N-API 从
libassertk.so返回。
图片文件保存在仓库内的 docs/openharmony/assertk-workbench.jpg,发布文章时不依赖临时目录或外部图片链接。
7.2 命令速查
# 根工程测试
export JAVA_HOME="/path/to/jdk-21"
./gradlew :assertk:jvmTest :assertk-coroutines:jvmTest
# 示例测试和 Native 动态库准备
(cd example && ./gradlew :shared:jvmTest :nativeApp:prepareOhos)
# 一键准备 OpenHarmony Native 产物
./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
# 构建未签名 HAP
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
scripts/build-hap.sh "$PWD/example/ohosApp"
# 创建仓库外签名工程
python3 scripts/prepare-signing-project.py /absolute/path/assertk-signing
# 在已配置签名的工程中构建 HAP
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
scripts/build-hap.sh /absolute/path/assertk-signing
# 设备安装和启动(需要 signed HAP)
hdc list targets
hdc -t <设备序列号> install -r \
/absolute/path/assertk-signing/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.assertk.example
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配:必须把公共
assertk调用真正编译进ohosArm64动态库。 - N-API 不负责断言语义:C++ 只做类型检查、字符串转换和释放,比较规则放在 Kotlin 共享层。
- 失败结果不能被当成异常页面:断言失败是演示库能力的重要场景,应以结构化
EvaluationResult返回。 - C ABI 必须维护 linker map:新增
AssertkEvaluate后,必须同步导出符号,否则 C++ 会在链接阶段找不到函数。 - 签名配置不能进入源码:未签名 HAP 可以用于构建验收,真机安装再使用仓库外的独立签名工程。
8.2 已知问题
- 当前交互示例只演示整数比较;
assertk的集合、字符串和文件断言仍通过共享runChecks验证,没有全部做成页面输入控件; - Native 产物当前只提供
ohosArm64/arm64-v8a,没有 32 位和 x86 模拟器库; - 示例工程没有内置证书、profile、p12 或密码,因此源码构建输出默认是未签名 HAP;
- 真机安装和签名结果取决于本机 DevEco 配置、设备 ABI 和签名 profile。
8.3 未来优化方向
- 增加字符串、集合、布尔值和异常捕获等可交互断言场景;
- 将断言结果封装为更丰富的共享
Flow<EvaluationResult>,供 Compose Multiplatform 或 ArkUI 页面复用; - 增加设备侧安装脚本和截图验收流程;
- 在持续集成中加入
ohosArm64链接、ELF 依赖检查和未签名 HAP 构建; - 为 AtomGit 文档增加版本化 API 表和兼容性矩阵。
九、总结
9.1 核心难点回顾
本次适配真正需要处理的不是一个页面按钮,而是一条完整跨端链路:
ArkUI TextInput / operation buttons
→ ArkTS 参数校验
→ C++ N-API
→ Kotlin/Native C ABI
→ assertk 公共断言 API
→ JSON EvaluationResult
→ ArkUI PASS / FAIL 结果卡片
9.2 封装层次
- KMP 层:提供稳定的
assertk断言语义和公共测试; - Shared 层:把页面需要的操作编号、输入和结果组织成平台无关门面;
- Native 层:生成 ARM64 动态库,通过 C ABI 输出有限入口;
- N-API 层:完成参数检查、JSON 转换和内存释放;
- ArkTS 层:管理输入、预设、执行状态、结果颜色和错误提示;
- DevEco 层:完成 CMake、HAP、签名、安装和运行。
9.3 三条经验
- 先让共享断言门面在 JVM 和 Native 通过,再接入 ArkUI 页面;
- 用 JSON 和少量 C ABI 代替跨语言对象传递,把释放责任写进桥接流程;
- 把自动测试、动态库依赖、HAP 构建和真机签名分别记录,避免把“编译成功”误认为“设备交付完成”。
9.4 适配成果
当前 assertk OpenHarmony 适配已经完成:
ohosArm64Kotlin/Native ARM64 动态库;AssertkCatalog、AssertkGet、AssertkRunChecks、AssertkEvaluate和AssertkFreeC ABI;- C++ N-API 参数检查、JSON 转换和内存释放;
- ArkTS 真实交互断言工作台;
- 五种整数比较断言和三组快速预设;
- JVM、Native、ELF 依赖和 ArkTS/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)