本文记录 Atrium 接入 OpenHarmony 的完整过程,覆盖 KMP/CMP 工程盘点、ohosArm64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 交互页面、HAP 构建、签名入口和验证流程。

本次适配复用 Atrium 的断言语义、失败报告和 Kotlin 多平台实现,再由 ArkTS 调用 Kotlin/Native 中的真实断言函数。示例页面不是单纯的固定检查页面,而是允许用户输入实际值和期望值、切换断言类型、执行断言并查看最近结果的交互式工作台。

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

上游仓库: robstoll/atrium

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做开源鸿蒙平台 KMP/CMP 适配

Atrium 是一个 Kotlin Multiplatform 断言库,提供 fluent 和 infix 两套 API,支持相等、不相等、集合、字符序列包含、类型和结构等断言,并能生成结构化失败报告。原项目的核心代码主要在 commonMain 中运行,原生目标通过各平台实现补齐运行时差异。

如果只把示例页面重新写成 ArkTS,再在页面中使用 === 或 String.includes,只能证明 ArkUI 可以显示结果,不能证明 Atrium 的公共 API、Kotlin/Native 目标、失败报告和跨语言桥接已经连通。因此本次适配需要同时解决以下问题:

障碍具体问题
目标缺失KMP 模块原本没有 OpenHarmony ohosArm64 目标,必须让核心、logic、API 和 bundle 共同生成 KLIB。
Native 兼容Kotlin/Native 没有 JVM 的部分标准库和反射行为,需要补充 nativeMain actual 实现。
依赖变体Atrium 的部分构建依赖没有现成的 OpenHarmony variant,需要在本地多平台模块中提供兼容实现。
语言边界不同ArkTS 不能直接持有 Kotlin data class 或 Expect 对象,必须经过 C ABI、C++ N-API 和 JSON。
报告生命周期Kotlin/Native 返回的字符串由 Native heap 分配,C++ 复制到 ArkTS 后必须显式释放。
示例真实性固定显示“检查通过”无法体现断言库功能,页面必须把用户输入传给 Atrium 并展示真实诊断消息。
交付链路复杂Native 动态库、CMake、ArkTS、HAP、签名、设备安装和 ARM64 依赖需要分开验收。

因此,本项目把适配边界放在四个地方:Kotlin Multiplatform 目标配置、Native 兼容实现、C ABI/N-API 桥接层和 ArkUI 交互层。断言语义与失败报告仍由 Atrium 维护,ArkTS 只负责输入、模式选择、页面状态和结果展示。

1.2 Atrium 提供的能力

Atrium 公共模块提供以下能力:

  • expect(actual).toEqual(expected):使用 Any.equals 判断相等;
  • expect(actual).notToEqual(expected):判断实际值与期望值不相等;
  • expect(text).toContain.atLeast(1).value(expected):判断字符序列至少包含一次期望文本;
  • fluent 和 infix 两种调用风格,满足不同测试代码的表达习惯;
  • 统一的 AssertionError 失败报告,包含主题、期望条件和实际值;
  • 多语言翻译模块和报告格式化模块;
  • 公共测试工厂、verbs 和 bundle 模块,供库内部测试和业务项目复用。

本次交互示例把最容易理解的三种文本断言映射为页面模式:

页面模式modeAtrium 调用示例
相等0expect(actual).toEqual(expected)atrium 与 atrium 相等
不相等1expect(actual).notToEqual(expected)left 与 right 不相等
包含文本2expect(actual).toContain.atLeast(1).value(expected)OpenHarmony 包含 Harmony

未知 mode 在共享门面中回退到相等分支,ArkTS 页面只提供上述三个合法选项。

1.3 实现适配

维度要求
代码复用断言调用、失败捕获、结果模型和自检逻辑由 Kotlin 共享。
平台目标为 Atrium 核心模块和示例加入 ohosArm64,生成 libatrium.so。
桥接稳定只使用少量 C ABI 函数和 UTF-8 JSON,不把 Kotlin 对象地址交给 ArkTS。
UI 完整页面支持输入、模式切换、成功/失败示例、执行历史和错误反馈。
可测试JVM 测试、Native 编译、CMake/N-API、ArkTS 编译、HAP 和设备安装分别验收。
签名安全源码只保留签名配置入口,证书、Profile、keystore 和密码由开发者本机配置。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面是独立的 OpenHarmony 验收宿主。其他 KMP/CMP 应用可以直接复用 Atrium 的公共断言 API,也可以只复用 example/shared 中的门面模式,按业务需要设计自己的 UI。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点 Atrium 模块、公共 API 和 OpenHarmony 示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、Native actual 和独立 example 工程
第 3 阶段:断言与序列化   ── 建立三种断言模式、结果模型和 JSON 契约
第 4 阶段:原生桥接       ── Kotlin/Native C ABI、C++ N-API、参数检查和内存释放
第 5 阶段:交互页面       ── ArkUI 输入控件、模式切换、结果卡片和执行历史
第 6 阶段:构建与验证     ── CMake、ArkTS、HAP 构建、签名入口和效果图

每个阶段都使用真实产物作为下一阶段输入:先用 JVM 验证共享断言,再把相同代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,最后由 ArkTS 页面发起真实断言调用。

三、逐步实现过程

第 1 阶段:项目初始化

1.1 盘点公共 API 和工程边界

项目采用与参考 UUID CMP 工程一致的分层:

atrium-core/                         核心断言、报告和公共类型
logic/atrium-logic/                  断言逻辑、搜索器和 Native actual
apis/fluent/atrium-api-fluent/       fluent 断言 API
apis/infix/atrium-api-infix/         infix 断言 API
bundles/                             可消费的 Atrium bundle
misc/                                specs、test-factory、verbs 和支撑模块
example/shared/                      示例门面和 JVM 验收测试
example/nativeApp/                   ohosArm64 Kotlin/Native 动态库
example/ohosApp/                     DevEco Stage 工程和 ArkUI 页面
scripts/                             Native 构建和 HAP 签名入口脚本
docs/openharmony/                    验收记录和效果图

example 是独立 Gradle 工程,不把 DevEco 工程当作根工程的 Kotlin 子模块。这样可以分别执行 Gradle 和 Hvigor,也可以把 ohosApp 复制到另一个目录后由开发者在 DevEco Studio 中配置签名。

1.2 固定工具链和版本矩阵

本项目使用以下工具链约定:

项目配置用途
Kotlin Multiplatform以 gradle/buildLibs.versions.toml 为准JVM、Kotlin/Native 和 KLIB
JDK21Gradle、Kotlin 编译和 Native 任务
OpenHarmony 目标ohosArm64ARM64 真机动态库
DevEco product6.0.0(20)工程兼容和目标 SDK
DevEco Studio6.0.0 ReleaseIDE 版本,负责工程同步、HAP 构建与签名
ABIarm64-v8aHAP 原生库架构
N-API 模块libentry.soArkTS 到 Kotlin/Native 的桥接

执行 Gradle 脚本前先选择 JDK 21:

export JAVA_HOME="/path/to/jdk-21"
java -version

本次验证使用了本机 JDK 21。Kotlin/Native 任务不要使用 JDK 25,否则可能出现 class-file 版本不兼容。

1.3 创建 OpenHarmony 示例目录

示例页面围绕“输入、选择、执行、反馈、历史”组织,而不是把共享检查结果直接平铺成验证清单:

标题区          Atrium 三方库 / OpenHarmony 交互式断言工作台
输入区          实际值 / 期望值
模式区          相等 / 不相等 / 包含文本
操作区          运行断言 / 载入成功示例 / 载入失败示例
结果区          通过或未通过、断言类型、Atrium 诊断消息
历史区          最近五次执行记录

页面默认使用“包含文本”示例,用户可以直接修改任意输入后再次执行。失败结果保留原始 Atrium 消息,便于观察断言库的报告能力。

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

2.1 加入 ohosArm64 目标

示例共享模块同时保留 JVM 测试和 OpenHarmony Native 目标:

plugins {
    kotlin("multiplatform")
}

kotlin {
    explicitApi()
    jvm()
    ohosArm64()

    sourceSets {
        commonTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

根工程中的 Atrium 核心、logic、fluent、infix、translations、verbs、test-factory 和 bundle 模块也加入对应的 ohosArm64 配置。JVM 目标让断言行为可以快速测试,ohosArm64 则把同一份 commonMain 代码编译为 OpenHarmony KLIB。

2.2 Native focused build 的作用

example/nativeApp 构建一个受 linker map 约束的 shared library:

kotlin {
    ohosArm64 {
        binaries.sharedLib {
            baseName = "atrium"
            linkerOpts(
                "--entry=0",
                "--version-script=${project.file("src/ohosArm64Main/linker/shared-library.map")}",
            )
            linkerOpts("-lace_napi.z", "-luv", "-lhilog_ndk.z")
        }
    }
}

链接器只导出三个 C ABI 符号:

AtriumRunChecks
AtriumEvaluate
AtriumFree

这样 ArkTS 只能通过明确的边界获取检查或断言结果,Atrium 内部的 Expect、报告对象和实现细节不会变成不受控的 ABI。

2.3 配置仓库和独立消费工程

示例工程的 settings.gradle.kts 保留 Maven Central、Gradle Plugin Portal 和项目使用的公共 Maven 仓库:

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

dependencyResolutionManagement {
    repositories {
        mavenLocal()
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        mavenCentral()
    }
}

当前 example/shared 通过项目依赖消费根工程的 Atrium bundle:

sourceSets {
    commonMain.dependencies {
        api("ch.tutteli.atrium:atrium-fluent:1.3.0-SNAPSHOT")
    }
}

这让页面示例始终使用当前工作树中的真实 Atrium API,而不是在 ArkTS 中复制一套行为相似的判断函数。

2.4 通过构建产物消费共享库

prepareOhos 在 Native 链接成功后复制动态库和 Kotlin/Native 头文件:

val prepareOhos by tasks.registering(Copy::class) {
    dependsOn("linkDebugSharedOhosArm64")
    from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
        include("libatrium.so")
        into("libs/arm64-v8a")
    }
    from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
        include("libatrium_api.h")
        into("src/main/cpp/include")
    }
    into(rootProject.layout.projectDirectory.dir("../example/ohosApp/entry"))
}

生成的 .so 和头文件只作为本机 DevEco 构建输入,按照参考工程的规则写入 .gitignore。这样可以避免把与本机 SDK、ABI 和签名环境绑定的二进制产物提交到 AtomGit。

第 3 阶段:断言与序列化

3.1 为什么需要统一 JSON 契约

ArkTS、C++ 和 Kotlin/Native 不能直接共享 Kotlin data class,因此边界使用稳定的 UTF-8 JSON:

ArkTS 输入 actual / expected / mode
        ↓
N-API evaluate()
        ↓
C ABI AtriumEvaluate()
        ↓
AtriumExamples.evaluate()
        ↓
AssertionResult.toJson()
        ↓
ArkUI 结果卡片和历史记录

页面只消费 JSON,不重新实现 Atrium 的相等和包含语义。这样 JVM 测试、Kotlin/Native 和 ArkTS 页面使用同一套断言规则。

3.2 AtriumAssertionResult

公共示例门面定义不可变结果模型:

public data class AtriumAssertionResult(
    val type: String,
    val passed: Boolean,
    val message: String,
)

三种模式统一由 AtriumExamples.evaluate 分发:

public fun evaluate(actual: String, expected: String, mode: Int): AtriumAssertionResult {
    val type = when (mode) {
        NOT_EQUAL -> "不相等"
        CONTAINS -> "包含文本"
        else -> "相等"
    }
    return try {
        when (mode) {
            NOT_EQUAL -> expect(actual).notToEqual(expected)
            CONTAINS -> expect(actual).toContain.atLeast(1).value(expected)
            else -> expect(actual).toEqual(expected)
        }
        AtriumAssertionResult(type, true, "断言通过")
    } catch (error: AssertionError) {
        AtriumAssertionResult(type, false, error.message ?: "断言失败")
    }
}

关键点是 try/catch 只包住 Atrium 断言调用。通过时返回简洁状态,失败时保留 Atrium 生成的详细 AssertionError.message,页面无需猜测失败原因。

3.3 JSON 结构和字符串转义

Native bridge 返回的成功结果如下:

{
  "type": "包含文本",
  "passed": true,
  "message": "断言通过"
}

失败结果保留诊断消息:

{
  "type": "相等",
  "passed": false,
  "message": "expected: ... actual: ..."
}

Kotlin/Native 侧统一使用 quote 对反斜杠、引号、换行和制表符进行转义。任何异常都会转换成:

{"error":"Native Atrium error"}

这样 ArkTS 能显示错误文本,C ABI 仍然只有字符串和指针两种边界类型。

3.4 自检和错误边界

AtriumExamples.runChecks() 保留四项共享冒烟检查:

  1. 整数相等断言通过;
  2. 字符串相等断言通过;
  3. 集合相等断言通过;
  4. 故意失败的断言能产生包含 to equal 的报告。

这些检查用于确认 Atrium 核心和 Native 加载成功,但示例首页的主要功能仍然是用户驱动的 evaluate 工作流。

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

4.1 Kotlin/Native 对象不能直接交给 ArkTS

Kotlin/Native 的 AtriumAssertionResult 和 Atrium 内部 Expect 属于 Kotlin 运行时对象。ArkTS 只能接收 JavaScript 值,C++ 也不能把 Kotlin 对象地址直接当成 JavaScript 对象。

最终采用三层桥接:

ArkTS
  │ JSON string
  ▼
C++ N-API entry
  │ char* / int32
  ▼
Kotlin/Native C ABI
  │ AtriumExamples.evaluate
  ▼
UTF-8 JSON + explicit free
4.2 方案对比
方案优点缺点选用
直接导出 Kotlin 对象代码少ABI、生命周期和类型不可控❌
只导出布尔值实现简单页面看不到 Atrium 失败报告❌
C ABI + JSON边界清晰、易扩展、易调试有一次序列化开销✅
在 ArkTS 重写断言逻辑页面调用简单KMP 和 ArkTS 逻辑会分叉❌
4.3 Kotlin/Native 导出函数

Native bridge 的结果缓冲区由 nativeHeap 分配:

@CName("AtriumEvaluate")
public fun evaluateNative(
    actual: CPointer<ByteVar>?,
    expected: CPointer<ByteVar>?,
    mode: Int,
): CPointer<ByteVar> = response {
    val result = AtriumExamples.evaluate(
        actual?.toKString().orEmpty(),
        expected?.toKString().orEmpty(),
        mode,
    )
    "{\"type\":${quote(result.type)},\"passed\":${result.passed},\"message\":${quote(result.message)}}"
}

@CName("AtriumFree")
public fun freeNative(pointer: CPointer<ByteVar>?) {
    if (pointer != null) nativeHeap.free(pointer.rawValue)
}

返回指针不跨越一次调用生命周期。C++ 复制字符串后立即调用 AtriumFree,避免 Native heap 泄漏。

4.4 C++ N-API 方法分发

C++ 注册两个 ArkTS 方法:

napi_property_descriptor methods[] = {
    {"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
        napi_default, nullptr},
    {"evaluate", nullptr, Evaluate, nullptr, nullptr, nullptr,
        napi_default, nullptr},
};

evaluate 会检查参数数量、两个字符串类型和整数 mode,再调用 AtriumEvaluate。字符串参数先查询 UTF-8 长度,再分配 std::vector<char>,避免固定缓冲区截断中文或长文本。

CMake 将 Kotlin/Native 动态库作为 imported library:

add_library(atrium SHARED IMPORTED)
set_target_properties(atrium PROPERTIES
  IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libatrium.so")

add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE atrium libace_napi.z.so)
4.5 N-API 生命周期
ArkTS evaluate(actual, expected, mode)
        │
        ▼
Read UTF-8 strings + int32 mode
        │
        ▼
AtriumEvaluate(actual, expected, mode)
        │
        ▼
napi_create_string_utf8(...)
        │
        ▼
AtriumFree(nativeBuffer)
        │
        ▼
return JS string

C++ 负责参数检查、跨语言字符串复制和 Native 释放,ArkTS 页面不需要知道 Kotlin/Native 的堆实现。

第 5 阶段:交互页面封装

5.1 ArkTS 调用 Native Atrium

ArkTS 客户端只做类型声明和 JSON 解析:

import atriumNative from 'libentry.so';

export interface AtriumAssertionResult {
  type: string;
  passed: boolean;
  message: string;
}

export function evaluate(actual: string, expected: string, mode: number): AtriumAssertionResult {
  return JSON.parse(atriumNative.evaluate(actual, expected, mode)) as AtriumAssertionResult;
}

页面不在客户端中实现 includes、=== 或失败文案,所有断言判断都通过 Native bridge 进入 Atrium。

5.2 输入和模式状态

Index.ets 使用 ArkUI @State 保存用户输入、模式、最新结果和历史数组:

@State private actual: string = 'OpenHarmony 让跨平台断言更易用';
@State private expected: string = '跨平台断言';
@State private mode: number = 2;
@State private latest: AssertionHistory | null = null;
@State private history: AssertionHistory[] = [];

三个按钮只修改 mode,不执行本地判断。点击“运行断言”时,当前输入和模式一次性传入 evaluate,返回结果再写入页面状态。

5.3 结果卡片和历史记录

每次执行生成一条记录:

const record: AssertionHistory = {
  id: Date.now(),
  type: result.type,
  actual: this.actual,
  expected: this.expected,
  passed: result.passed,
  message: result.message
};
this.latest = record;
this.history = [record, ...this.history].slice(0, 5);

成功结果显示“通过”和“断言通过”;失败结果额外显示实际值和期望值。最近执行区域用 PASS / FAIL 标记,帮助用户比较多次输入的差异。

5.4 页面交互预设

页面提供两个数据预设:

  • 载入成功示例:加载中文包含文本场景,默认选择“包含文本”;
  • 载入失败示例:加载不同字符串的相等场景,默认选择“相等”。

预设按钮只写入输入框和模式,不直接伪造结果。用户仍需点击“运行断言”,页面才会调用 Kotlin/Native。

第 6 阶段:示例与验证

6.1 example 工程结构
example/
├── shared/
│   ├── src/commonMain/.../AtriumExamples.kt
│   └── src/commonTest/.../AtriumExamplesTest.kt
├── nativeApp/
│   ├── src/ohosArm64Main/.../NativeBridge.kt
│   └── src/ohosArm64Main/linker/shared-library.map
└── ohosApp/
    ├── AppScope/
    ├── entry/src/main/cpp/
    │   ├── CMakeLists.txt
    │   ├── napi_init.cpp
    │   └── types/libentry/
    └── entry/src/main/ets/
        ├── atrium/AtriumClient.ets
        └── pages/Index.ets

shared 验证公共断言,nativeApp 产生 Native 动态库,ohosApp 负责 ArkUI 页面。三者的边界清晰,任何一层失败都能单独定位。

6.2 原生模块注册

napi_init.cpp 通过 napi_module_register 注册 entry 模块,类型声明位于:

example/ohosApp/entry/src/main/cpp/types/libentry/index.d.ts

ArkTS 使用:

import atriumNative from 'libentry.so';

模块导出 runChecks 和 evaluate 两个方法,方法返回值均为 JSON 字符串,再由 ArkTS 客户端解析为类型化对象。

6.3 Native 动态库准备

执行:

export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh

脚本依次编译 Atrium 的核心 ohosArm64 目标、运行示例 JVM 测试、链接 nativeApp 并执行 prepareOhos。成功后生成:

example/ohosApp/entry/libs/arm64-v8a/libatrium.so
example/ohosApp/entry/src/main/cpp/include/libatrium_api.h

这些文件属于本地构建输入,默认被 .gitignore 排除。重新构建时必须同时更新 .so 和头文件,避免 C ABI 不一致。

6.4 构建、签名和安装

可以直接使用 DevEco Hvigor 编译 ArkTS、C++ 和 HAP:

cd example/ohosApp
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw \
  --mode module -p module=entry@default -p product=default \
  -p buildMode=debug clean assembleHap --no-daemon

仓库脚本在签名阶段增加明确停点:

./scripts/build-hap.sh /absolute/path/to/atrium/example/ohosApp

未设置 OHOS_SIGNING_CONFIG 时,脚本会提示用户配置签名路径并退出,不会索取或保存密码、私钥:

签名需要用户配置 OHOS_SIGNING_CONFIG

配置本机签名工程后再重新执行脚本。源码工程不应提交 .cer、.p7b、.p12、Profile、密码或私钥。

四、完整代码对照

4.1 整体架构

ArkUI 输入框和模式按钮
    │ actual / expected / mode
    ▼
AtriumClient.ets -> libentry.so
    │ N-API
    ▼
AtriumEvaluate / AtriumRunChecks
    │ C ABI
    ▼
libatrium.so
    │ Kotlin/Native
    ▼
AtriumExamples -> Atrium API -> JSON

4.2 文件清单

文件职责
atrium-core/src/nativeMain/Atrium 核心 Native actual 和兼容实现
logic/atrium-logic/src/nativeMain/断言 logic 的 OpenHarmony Native 实现
example/shared/.../AtriumExamples.kt三种断言门面、自检和结果模型
example/nativeApp/.../NativeBridge.ktC ABI、JSON 返回和 Native 内存释放
example/nativeApp/.../shared-library.mapC ABI 导出符号白名单
example/ohosApp/.../napi_init.cppN-API 导出、参数读取和字符串转换
example/ohosApp/.../AtriumClient.etsArkTS 类型定义和 JSON 解析
example/ohosApp/.../Index.ets交互式断言工作台页面
scripts/build-openharmony.sh模块测试、Native 链接和产物复制
scripts/build-hap.sh已配置签名工程的 HAP 构建入口
docs/openharmony/VALIDATION.md自动构建门和真机验证记录
docs/openharmony/atrium-workbench.jpg交互页面效果图

4.3 关键 API 对照

层次API作用
Atriumexpect(actual).toEqual(expected)相等断言
Atriumexpect(actual).notToEqual(expected)不相等断言
Atriumexpect(text).toContain.atLeast(1).value(expected)包含文本断言
SharedAtriumExamples.evaluate统一模式分发和失败捕获
NativeAtriumEvaluateC ABI 入口,返回结果 JSON
N-APIevaluate向 ArkTS 暴露 Native 断言
ArkTSrunAssertion读取页面状态、调用 N-API、记录历史

4.4 ArkTS 与 Kotlin 的边界

ArkTS 只负责收集输入和呈现状态:

const result: AtriumAssertionResult = evaluate(
  this.actual,
  this.expected,
  this.mode
);

Kotlin 负责断言含义和失败报告:

when (mode) {
    NOT_EQUAL -> expect(actual).notToEqual(expected)
    CONTAINS -> expect(actual).toContain.atLeast(1).value(expected)
    else -> expect(actual).toEqual(expected)
}

两者之间只传输字符串、整数和 JSON,不传输 Kotlin 对象、ArkTS class 实例或未校验的动态结构。

五、关键决策说明

决策 1:把 ohosArm64 加入公共构建约定

只有真正链接 ohosArm64 动态库,才能证明 Atrium 的共享断言和报告逻辑可以进入 OpenHarmony 运行时。只编译 JVM 示例不足以证明 Native 兼容。

决策 2:独立消费者必须通过构建产物消费

example 单独解析 Atrium,ohosApp 单独接收 .so 和头文件,避免根工程编译通过却无法在 DevEco 中打包。

决策 3:JSON 作为跨语言数据契约

JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,并能携带 Atrium 失败报告。新增字段时也不需要暴露 Kotlin 对象布局。

决策 4:桥接层只开放三个 C ABI 入口

执行检查、执行交互断言和释放字符串已经覆盖示例所需能力。减少 ABI 符号可以降低 Native 生命周期和兼容风险。

决策 5:页面展示原始 Atrium 诊断

如果页面只显示“成功/失败”,用户无法理解断言库的价值。失败时保留 AssertionError.message,才能展示 Atrium 对主题、期望和实际值的结构化描述。

决策 6:把自动构建和设备验证分开

JVM 测试验证断言规则,Native 编译验证目标,CMake 验证 N-API,Hvigor 验证 HAP,真机验证安装和运行。每一层都有明确的失败边界。

六、测试与验证

6.1 测试环境

本次工程验证使用:

  • macOS;
  • JDK 21;
  • Kotlin Multiplatform 版本以仓库版本目录为准;
  • DevEco Studio 及 OpenHarmony ARM64 Native SDK;
  • OpenHarmony Stage 工程;
  • hdc 设备工具用于可选的真机安装。

当前验证环境没有授权的 hdc 设备,因此本文不把设备安装结果写成已完成的真机验收。效果图用于展示交互页面设计和页面信息层级,Native、ArkTS 和 HAP 构建结果仍按命令独立验证。

6.2 静态检查与单元测试

export JAVA_HOME="/path/to/jdk-21"
./gradlew -p example :shared:jvmTest :nativeApp:prepareOhos --no-daemon

测试覆盖:

  1. 相等断言成功;
  2. 不相等断言成功;
  3. 包含文本断言成功;
  4. 相等断言失败时返回非空 Atrium 消息;
  5. 原有四项共享冒烟检查全部通过。

6.3 原生桥接和 HAP 验证

JAVA_HOME=/path/to/jdk-21 ./scripts/build-openharmony.sh

脚本已验证以下任务可以完成:

:atrium-core:compileKotlinOhosArm64
:atrium-fluent:compileKotlinOhosArm64
:atrium-infix:compileKotlinOhosArm64
:shared:jvmTest
:nativeApp:prepareOhos

CMake/Ninja 还可以单独检查 napi_init.cpp:

cmake -S example/ohosApp/entry/src/main/cpp \
  -B /tmp/atrium-ohos-cmake \
  -G Ninja \
  -DCMAKE_TOOLCHAIN_FILE=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build/cmake/ohos.toolchain.cmake \
  -DOHOS_ARCH=arm64-v8a \
  -DCMAKE_BUILD_TYPE=Debug
cmake --build /tmp/atrium-ohos-cmake -v

DevEco Hvigor 已验证能够完成 CMake、ArkTS、HAP 打包流程。仓库签名脚本在没有 OHOS_SIGNING_CONFIG 时会按设计停在签名提示处。

6.4 功能验证用例

用例 1:默认页面和包含文本断言

打开页面后,实际值为 OpenHarmony 让跨平台断言更易用,期望值为 跨平台断言,默认模式为“包含文本”。点击“运行断言”应进入 Kotlin/Native 并显示“断言通过”。

用例 2:相等断言

选择“相等”,输入两个相同字符串,执行后结果类型显示“相等”,状态为通过。输入不同字符串后再次执行,页面显示失败并展示 Atrium 诊断。

用例 3:不相等断言

选择“不相等”,输入 left 和 right,执行后应通过;把期望值改成 left,再次执行应失败。

用例 4:包含文本断言

选择“包含文本”,输入长文本和子文本,执行后验证字符序列搜索实际来自 Atrium,而不是 ArkTS includes。

用例 5:成功/失败预设

点击“载入成功示例”或“载入失败示例”,确认输入框和模式变化;点击“运行断言”后,页面结果才改变,预设按钮不会伪造执行结果。

用例 6:连续执行和历史记录

连续执行至少六次,确认“最近执行”最多保留五条,最新一条位于列表顶部,PASS/FAIL 与执行结果一致。

6.5 验证结论

共享 JVM 测试、Atrium ohosArm64 编译、Kotlin/Native 动态库链接、CMake/N-API 编译和 DevEco ArkTS/HAP 构建均可独立完成。真机安装与设备交互需要在连接授权的 ARM64 OpenHarmony 设备后继续执行,文章不以无设备状态代替真机结论。

七、运行效果

7.1 交互页面效果图

在这里插入图片描述

效果图展示了当前 ArkUI 页面:

  • 页面标题为“Atrium 三方库”;
  • 副标题为“OpenHarmony 交互式断言工作台”;
  • 顶部显示 NATIVE,说明结果通过 Native bridge 产生;
  • 输入区同时展示实际值和期望值;
  • 三个模式按钮中“包含文本”处于选中状态;
  • “运行断言”是实际执行入口;
  • “载入成功示例”和“载入失败示例”用于快速切换输入;
  • 结果区显示“断言通过 · 包含文本”和 Atrium 返回的成功消息;
  • 最近执行区域保留实际运行过的输入组合。

图片文件位于 docs/openharmony/atrium-workbench.jpg,文章与效果图均随项目源码使用 AtomGit 管理。

7.2 命令速查

# 根工程测试和 OpenHarmony Native 准备
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh

# 单独运行共享测试
./gradlew -p example :shared:jvmTest --no-daemon

# CMake/N-API 编译
cmake -S example/ohosApp/entry/src/main/cpp \
  -B /tmp/atrium-ohos-cmake -G Ninja \
  -DCMAKE_TOOLCHAIN_FILE=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build/cmake/ohos.toolchain.cmake \
  -DOHOS_ARCH=arm64-v8a
cmake --build /tmp/atrium-ohos-cmake

# 在已配置签名的工程中构建 HAP
./scripts/build-hap.sh /absolute/path/to/atrium/example/ohosApp

# 查看设备并安装签名 HAP
hdc list targets
hdc -t <设备序列号> install -r /absolute/path/to/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
  -a EntryAbility -b ch.tutteli.atrium.example

7.3 签名说明

签名是 HAP 交付的最后一步。执行 scripts/build-hap.sh 时,如果没有配置 OHOS_SIGNING_CONFIG,脚本会输出:

签名需要用户配置 OHOS_SIGNING_CONFIG(仅接受签名配置文件路径,不要在终端输入密码或私钥)。

配置应由开发者在本机完成。证书、Profile、p12、别名和密码不进入 AtomGit,也不写入文章或示例源码。

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配:必须把 Atrium 共享断言真正编译为 ohosArm64 动态库。
  2. N-API 不负责业务判断:C++ 只做参数检查、字符串转换和释放,断言语义必须放在 Kotlin。
  3. 返回值不能只保留布尔值:如果丢弃 AssertionError.message,示例无法体现 Atrium 的报告能力。
  4. C ABI 必须配 linker map:头文件生成成功不代表符号会被导出,新增函数后必须同步更新版本脚本。
  5. 生成文件不应进入仓库:.so、生成头文件、.cxx 和 .hvigor 应在本地准备,源码仓库只保留可复现的构建入口。
  6. 签名与编译分开处理:构建成功不等于 HAP 已签名,签名材料必须由本地 DevEco 工程提供。

8.2 已知问题

  • 当前 Native 示例只提供 ARM64(arm64-v8a),没有 32 位或 x86 模拟器产物;
  • 当前验证环境没有授权真机,设备安装和 hdc 启动需要后续补做;
  • DevEco 对 libentry.so 的 N-API 类型声明可能输出 SDK 版本相关的提示,项目已提供 index.d.ts;
  • 当前页面只提供三种文本断言,Atrium 其他集合、类型和结构断言仍需按业务增加交互控件;
  • HAP 签名依赖开发者本机配置,仓库脚本只负责在缺少配置时提示并停止。

8.3 未来优化方向

  • 增加集合断言、正则断言和自定义消息的交互示例;
  • 为 ArkTS 客户端增加更严格的 JSON schema 校验和错误分类;
  • 发布带 OpenHarmony variant 的 Atrium bundle,减少示例对本地构建的依赖;
  • 增加 x86_64 模拟器或更多 ARM64 设备的兼容性验证;
  • 在持续集成中加入 compileKotlinOhosArm64、CMake 和未签名 HAP 构建任务;
  • 为 CMP/Compose Multiplatform 宿主提供与当前 ArkUI 工作台等价的交互示例。

九、总结

9.1 核心难点回顾

本次适配真正需要处理的不是一个页面按钮,而是一条完整跨端链路:

ArkUI 输入
    → N-API 参数检查
    → Kotlin/Native C ABI
    → Atrium 断言 API
    → AssertionError / 成功结果
    → JSON
    → ArkUI 结果卡片和执行历史

9.2 封装层次

  • Atrium 层:定义稳定的 expectation、报告和翻译能力;
  • Shared 层:定义三种页面模式、结果模型和共享自检;
  • Native 层:生成 ARM64 动态库,并通过 C ABI 输出有限入口;
  • N-API 层:完成参数检查、字符串转换和内存释放;
  • ArkTS 层:管理输入控件、模式状态、结果展示和执行历史;
  • DevEco 层:完成 CMake、ArkTS、HAP、签名和设备安装。

9.3 三条经验

  1. 先让共享断言在 JVM 和 Native 通过,再接入 ArkUI;
  2. 用 JSON 和少量 C ABI 代替跨语言对象传递,同时保留失败报告;
  3. 把自动测试、HAP 构建、签名和真机验收分别记录,避免把“编译成功”误认为“设备可用”。

9.4 适配成果

当前 Atrium OpenHarmony 适配已完成:

  • 核心模块和 API 的 ohosArm64 Kotlin/Native 目标;
  • Native actual、KMP 依赖变体和本地 kbox 兼容模块;
  • JVM 与 OpenHarmony 共用的相等、不相等、包含文本断言;
  • Kotlin/Native + C ABI + C++ N-API JSON 桥接;
  • ArkUI 交互式断言工作台和最近执行记录;
  • CMake、Hvigor、HAP 构建和签名提示脚本;
  • AtomGit 项目文档、中文/英文 OpenHarmony README 和效果图;
  • 可复现的 JVM、Native、CMake 和 ArkTS 验证命令。

适配过程 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

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

更多推荐