本文记录 assertk 接入 OpenHarmony 的完整过程,覆盖 KMP 工程盘点、ohosArm64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 交互页面、HAP 构建、签名隔离和真机验收准备。

assertk 的核心是 Kotlin Multiplatform 断言库。本文沿用 KMP/CMP 适配文章的组织方式,把公共断言逻辑编译到 OpenHarmony ARM64 Native,再由 ArkTS 页面提供真实的“实际值 / 期望值 / 断言关系”交互。页面显示的结果来自 Kotlin/Native 中真正执行的 assertk API,不是在 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 调用示例
等于0isEqualTo42 = 42
大于1isGreaterThan18 > 10
小于2isLessThan3 < 8
大于等于3isGreaterThanOrEqualTo10 >= 10
小于等于4isLessThanOrEqualTo3 <= 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
JDK21Gradle、Kotlin 编译和 Native 任务
OpenHarmony 目标ohosArm64ARM64 真机动态库
DevEco 工程Stage + API 20 SDKHAP 编译和打包
ABIarm64-v8aentry 模块 Native 库目录
页面桥接C++ N-APIArkTS 加载 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 验收:

  1. 标量相等断言通过;
  2. 有序集合 containsExactly 通过;
  3. 集合 hasSize 通过;
  4. 布尔值 isTrue 通过;
  5. 文本 startsWith 通过;
  6. 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.ktJVM 共享验收测试
example/nativeApp/.../NativeBridge.ktC ABI、JSON 编码和 Native 释放
example/nativeApp/.../shared-library.map控制导出的 C 符号
example/ohosApp/.../napi_init.cppN-API 导出和参数检查
example/ohosApp/.../CMakeLists.txt导入 .so 并链接 entry 模块
example/ohosApp/.../AssertkClient.etsJSON 解析和 ArkTS 类型封装
example/ohosApp/.../Index.ets交互式断言页面
scripts/build-openharmony.shJVM、Native 测试和产物准备
scripts/build-hap.shDevEco HAP 构建
scripts/prepare-signing-project.py创建仓库外签名工程
docs/openharmony/VALIDATION.md自动检查和验收记录
docs/openharmony/assertk-workbench.jpg示例页面效果图

4.3 关键 API 对照

层次API作用
KotlinassertThat(actual).isEqualTo(expected)执行等于断言
KotlinAssertkExamples.evaluate把操作编号映射为公共断言 API
NativeAssertkEvaluate返回一个 JSON 结果
N-APIevaluate校验参数并把结果交给 ArkTS
ArkTSparse<EvaluationResult>解析 JSON 并处理 Native 错误
ArkTSrunAssertion管理输入、执行状态和结果卡片

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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配:必须把公共 assertk 调用真正编译进 ohosArm64 动态库。
  2. N-API 不负责断言语义:C++ 只做类型检查、字符串转换和释放,比较规则放在 Kotlin 共享层。
  3. 失败结果不能被当成异常页面:断言失败是演示库能力的重要场景,应以结构化 EvaluationResult 返回。
  4. C ABI 必须维护 linker map:新增 AssertkEvaluate 后,必须同步导出符号,否则 C++ 会在链接阶段找不到函数。
  5. 签名配置不能进入源码:未签名 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 三条经验

  1. 先让共享断言门面在 JVM 和 Native 通过,再接入 ArkUI 页面;
  2. 用 JSON 和少量 C ABI 代替跨语言对象传递,把释放责任写进桥接流程;
  3. 把自动测试、动态库依赖、HAP 构建和真机签名分别记录,避免把“编译成功”误认为“设备交付完成”。

9.4 适配成果

当前 assertk OpenHarmony 适配已经完成:

  • ohosArm64 Kotlin/Native ARM64 动态库;
  • AssertkCatalog、AssertkGet、AssertkRunChecks、AssertkEvaluate 和 AssertkFree C 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。

参考文档

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐