本文记录 TestMints 接入 OpenHarmony 的完整过程,覆盖 KMP/CMP 工程盘点、ohosArm64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 交互示例、DevEco Studio 构建和验证。

本次适配保留 TestMints 的核心 testTemplate 语义:由 setup 准备场景,exercise 执行行为,verifyAnd 校验结果,teardown 记录收尾,并把同一份 Kotlin 代码编译到 OpenHarmony ARM64。ArkUI 页面不是只显示一组静态校验按钮,而是让用户选择一个业务场景、输入执行次数、运行场景并查看结构化生命周期日志。

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

上游仓库: robertfmurdock/testmints

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

TestMints 是一个面向 Kotlin 测试代码可读性的轻量库。它把测试中的准备、执行、断言和收尾组织成可读的模板,既提供标准同步模板,也提供异步模块和日志模块。原项目面向多个 Kotlin 平台,OpenHarmony 适配的重点是让共享 Kotlin 逻辑可以在 ohosArm64 上编译,并由一个真实的 ArkUI 页面调用。

如果只把页面重新写成 ArkTS,页面虽然可以显示一个“通过”文本,却无法证明 TestMints 的 testTemplate、exercise、verifyAnd 和 teardown 已经在 OpenHarmony Native 侧执行。完整适配需要解决下面几个问题:

障碍具体问题
目标缺失现有 KMP 工程默认覆盖 JVM、JS、Wasm、Apple、Linux 和 Windows 等目标,需要增加 ohosArm64()。
依赖兼容OpenHarmony 目标使用兼容构件,Coroutines、Serialization 和 Kotlin Logging 需要定向替换版本。
代码消费边界DevEco 工程不能直接把 Kotlin 源码当作 ArkTS 模块,需要先生成 libtestmints.so 和 C 头文件。
语言边界不同ArkTS 不能直接持有 Kotlin data class,必须通过 C ABI、C++ N-API 和 JSON 传递结果。
交互验证不足单纯调用 runChecks 只能验证固定断言,不能体现用户配置场景和完整生命周期。
构建链路较长Gradle、Kotlin/Native、CMake、Hvigor、HAP 和设备安装需要分别定位问题。
签名材料隔离证书、Profile、p12 和密码属于本地交付配置,不应进入源码或文章。

因此,本项目把适配边界分成三层:KMP 共享层负责调用 TestMints API,Kotlin/Native 层负责输出稳定 JSON,C++ N-API 层负责给 ArkTS 注册方法,ArkUI 页面负责收集输入并呈现结果。

1.2 库提供的能力

TestMints 的标准模块提供 testTemplate 及其 DSL 操作符:

  • testTemplate(sharedSetup = ..., sharedTeardown = ...):创建带共享上下文的测试模板;
  • exercise { ... }:执行被测行为并返回结果;
  • verify { ... }:消费执行结果并完成断言;
  • verifyAnd { ... }:在示例中串联结果校验;
  • teardown { ... }:记录单场景收尾;
  • sharedTeardown:模板级收尾,在场景结束后执行;
  • async 模块:为协程场景提供对应的异步模板;
  • mint-logs 模块:将测试事件输出到结构化日志。

本次 OpenHarmony 示例选择标准同步模板,因为它能在一个短流程中展示完整生命周期。示例页面使用以下场景模型:

字段含义
title用户输入或预设的业务场景名称,空值会规范化为“未命名场景”
quantity1 到 99 的演示输入,页面称为“执行次数”
output示例行为计算出的结果,当前实现为 quantity * 2
eventssetup、exercise、verify、teardown 和 template teardown 的有序日志

这里的 quantity 是演示场景输入,exercise 当前计算一次 quantity * 2,并没有在 Kotlin 侧循环执行 N 次真实业务操作。文章和页面都把它作为可重复的行为输入展示,避免误解为已经接入某个真实订单服务。

1.3 实现适配

维度要求
代码复用TestMints 的模板执行、结果校验和 teardown 由 Kotlin 共享代码完成。
平台目标为库模块和示例加入 ohosArm64,生成 ARM64 KLIB 和 libtestmints.so。
依赖兼容仅对 ohosArm64 configuration 替换 OpenHarmony 兼容构件,不影响已有平台。
桥接稳定通过少量 C ABI 入口返回 UTF-8 JSON,避免跨语言传递 Kotlin 对象。
UI 完整页面支持预设场景、文本输入、加减数量、执行按钮、结果卡和生命周期日志。
可测试JVM 测试、Native 链接、ELF 依赖检查和 DevEco 构建分别验收。
签名安全文章只给出签名入口和占位路径,证书和密码由开发者本机配置。
仓库规范项目、文章和示例链接统一使用 AtomGit。

example/ohosApp 是一个真实的 OpenHarmony 宿主工程。页面点击执行后,ArkTS 调用 N-API,N-API 再调用 Kotlin/Native 中的 TestMints 代码。这样验证的是共享库功能在 OpenHarmony 上的运行链路,而不是在页面中复制一套相似的判断逻辑。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点 TestMints 模块、API 和示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、版本替换和独立 example 工程
第 3 阶段:共享场景模型   ── 用 testTemplate 封装场景结果和生命周期事件
第 4 阶段:原生桥接       ── Kotlin/Native C ABI、C++ N-API、JSON 和内存释放
第 5 阶段:ArkUI 交互页面 ── 预设、输入、数量步进、执行结果和错误状态
第 6 阶段:构建与验证     ── Native 动态库、依赖检查、DevEco 构建和效果图

每个阶段都使用真实产物作为下一阶段输入:先在 JVM 中验证 TestMintsExamples,再把共享代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,最后在 ArkUI 页面中执行一个用户配置的行为场景。

三、逐步实现过程

第 1 阶段:项目初始化

1.1 盘点公共 API 和工程边界

仓库保留原有 libraries 组织,并新增 OpenHarmony 示例:

testmints/
├── libraries/
│   ├── standard/                 标准同步 testTemplate
│   ├── async/                    协程场景模板
│   ├── report/                   公共报告类型
│   └── mint-logs/                结构化日志能力
├── convention-plugins/           KMP 构建约定和 OpenHarmony 依赖替换
├── example/
│   ├── shared/                   共享场景门面和 JVM 测试
│   ├── nativeApp/                ohosArm64 Kotlin/Native 动态库
│   └── ohosApp/                  DevEco Stage 工程和 ArkUI 页面
├── scripts/                      构建、依赖检查和签名工程辅助脚本
└── docs/openharmony/             验证记录和适配文章

example 是独立 Gradle 工程,example/ohosApp 是独立 DevEco 工程。两者通过 prepareOhos 任务交换动态库和头文件,不把 DevEco 工程强行纳入 Kotlin settings。

1.2 固定工具链和版本矩阵
项目配置用途
Kotlin Multiplatform2.2.21-1.0.0编译 JVM、Kotlin/Native 和 KLIB
JDK21Gradle、Kotlin 编译和 Native 任务
OpenHarmony 目标ohosArm64ARM64 真机 Native 库
KotlinPoet KSP2.2.0项目现有代码生成工具链
Coroutines OHOS1.10.2-1.0.0OpenHarmony 目标兼容构件
Serialization OHOS1.9.1-1.0.0OpenHarmony 目标兼容构件
Kotlin Logging OHOS8.0.4-1.0.0-02OpenHarmony 目标兼容构件
HAP ABIarm64-v8aCMake 导入 Kotlin/Native 动态库

构建前选择 JDK 21:

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

OpenHarmony 兼容构件只在 ohosArm64 configuration 中替换,JVM、JS 和其他原有目标继续使用项目默认依赖版本。

1.3 创建 OpenHarmony 示例目录

页面围绕一个可操作的“行为场景工作台”组织,而不是只显示验证文本:

顶部        TestMints 三方库 / Kotlin Multiplatform / OpenHarmony
配置区      快速选择、场景名称、执行次数、执行按钮
结果区      PASSED、场景标题、INPUT、OUTPUT、BRIDGE
日志区      setup、exercise、verify、teardown、template teardown
状态区      已完成场景和错误信息

默认场景为“订单回访”,默认输入为 3,点击执行后输出 6。页面还提供“消息同步”和“资料校验”两个预设,方便在设备上切换场景并重复执行。

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

2.1 加入 ohosArm64 目标

库的 OpenHarmony 目标由 convention-plugins/src/main/kotlin/com/zegreatrob/testmints/plugins/platforms.gradle.kts 统一配置。为了兼容旧版插件功能测试,约定插件通过反射检查 ohosArm64 方法是否存在;正式 KMP 模块则直接调用目标:

kotlin {
    jvm()
    ohosArm64()

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

example/shared 同时保留 jvm() 和 ohosArm64()。它通过 kotlin.srcDirs 引入 libraries/report 与 libraries/standard 的 commonMain,从而让示例直接执行仓库当前的 TestMints 实现。

2.2 OpenHarmony 依赖定向替换

不同目标的构件发布节奏不完全一致,因此不能把 OHOS 版本写成全局强制版本。当前约定只处理名称以 ohosArm64 开头的 configuration:

configurations.configureEach {
    if (name.startsWith("ohosArm64")) {
        resolutionStrategy.dependencySubstitution {
            substitute(module("org.jetbrains.kotlinx:kotlinx-coroutines-core"))
                .using(module("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2-1.0.0"))
            substitute(module("org.jetbrains.kotlinx:kotlinx-serialization-json"))
                .using(module("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.1-1.0.0"))
            substitute(module("io.github.oshai:kotlin-logging"))
                .using(module("io.github.oshai:kotlin-logging:8.0.4-1.0.0-02"))
        }
    }
}

实际配置还覆盖这些库的目标后缀构件,例如 *-ohosarm64、Coroutines Test 和 Serialization Core。这样 OpenHarmony Native 编译能解析兼容构件,同时不改变原项目已有平台的依赖解析。

2.3 配置独立消费工程

example/settings.gradle.kts 使用 OpenHarmony 社区 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()
    }
}

共享示例没有复制 TestMints 源码,而是通过 example/shared/build.gradle.kts 引入公共源码目录:

sourceSets {
    commonMain {
        kotlin.srcDirs(
            "../../libraries/report/src/commonMain/kotlin",
            "../../libraries/standard/src/commonMain/kotlin",
        )
    }
}
2.4 通过构建产物消费共享库

example/nativeApp 将共享示例链接成动态库,并由 prepareOhos 复制两个产物:

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

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

动态库和生成头文件属于构建产物。DevEco 工程只消费本机根据 SDK 和源码重新生成的文件,不把二进制复制流程隐藏在页面代码中。

第 3 阶段:共享场景模型

3.1 为什么需要共享场景门面

TestMints 本身是测试 DSL,ArkUI 页面需要一个稳定的、适合跨语言传输的业务结果。因此在 example/shared/src/commonMain 增加 TestMintsExamples 门面:

ArkTS 输入 title + quantity
        ↓
TestMintsRunScenario
        ↓
TestMintsExamples.runScenario
        ↓
testTemplate(sharedSetup, sharedTeardown)
        ↓
exercise → verifyAnd → teardown
        ↓
ScenarioResult(title, quantity, output, events)

页面只知道场景结果字段,不需要了解 TestMints 内部模板对象的泛型和生命周期实现。

3.2 ScenarioResult 和输入规范化
public object TestMintsExamples {
    public data class ScenarioResult(
        val title: String,
        val quantity: Int,
        val output: Int,
        val events: List<String>,
    )

    public fun normalizeScenarioInput(rawTitle: String, rawQuantity: Int): Pair<String, Int> {
        val title = rawTitle.trim().ifEmpty { "未命名场景" }
        val quantity = rawQuantity.coerceIn(1, 99)
        return title to quantity
    }
}

标题先 trim,空标题使用默认文案;数量限制在 1..99,防止页面输入异常值进入 Native 层。页面在调用前也做一次限制,但共享层仍然保留最终边界,确保其他调用方也得到相同结果。

3.3 用 TestMints 组织完整生命周期

核心场景实现如下:

public fun runScenario(rawTitle: String, rawQuantity: Int): ScenarioResult {
    val title = rawTitle.trim().ifEmpty { "未命名场景" }
    val quantity = rawQuantity.coerceIn(1, 99)
    val events = mutableListOf<String>()
    val template = testTemplate(
        sharedSetup = {
            events += "setup · 准备「$title」"
            title
        },
        sharedTeardown = {
            events += "template teardown · 场景已收尾"
        },
    )

    var output = 0
    template() exercise {
        events += "exercise · 执行 $quantity 次"
        output = quantity * 2
        output
    } verifyAnd { result ->
        events += "verify · 检查结果 $result"
        check(result == quantity * 2) { "场景结果不符合预期" }
    } teardown {
        events += "teardown · 记录执行摘要"
    }

    return ScenarioResult(title, quantity, output, events)
}

默认输入 订单回访 和 3 的结果是:

title    = 订单回访
quantity = 3
output   = 6
events   = 5 条有序生命周期日志

需要特别说明的是,日志文本中的“执行 3 次”是场景输入的业务表达;当前 exercise 代码只执行一次模板动作并计算 3 * 2。如接入真实业务,后续可以把这里替换为循环调用、批处理请求或仓储操作。

3.4 固定检查与错误边界

runChecks() 使用同一个 testTemplate 验证共享库可以在 OpenHarmony Native 侧执行:

public fun runChecks(): List<String> {
    var observed = false
    val template = testTemplate(sharedSetup = { Unit })
    template() exercise { 42 } verify { result ->
        check(result == 42) { "testmints exercise returned $result" }
        observed = true
    }
    check(observed) { "testmints verify callback was not invoked" }
    return listOf(
        "standard test template executes on OpenHarmony",
        "verify callback observes the exercise result",
    )
}

Native 包装层捕获异常并返回 {"error":"..."}。这样 ArkTS 可以把错误显示在页面底部,而不是让一个 Kotlin/Native 异常直接穿过 C ABI。

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

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

ScenarioResult 是 Kotlin data class,events 还是 Kotlin 集合。ArkTS 只能安全接收 JavaScript 值或字符串,C++ 也不能把 Kotlin 对象地址直接包装成 JS 对象。因此本项目选择 UTF-8 JSON 作为唯一跨语言结果格式:

ArkTS runScenario(title, quantity)
        │
        ▼
C++ N-API 参数检查
        │
        ▼
Kotlin/Native TestMintsRunScenario
        │
        ▼
Native heap UTF-8 JSON
        │
        ▼
napi_create_string_utf8 + TestMintsFree
        │
        ▼
ArkTS JSON.parse
4.2 方案对比
方案优点缺点选用
直接导出 Kotlin 对象代码少ABI、生命周期和类型不可控❌
只导出整数结果实现简单页面无法展示场景标题和有序日志❌
C ABI + JSON边界清晰、易调试、可扩展有一次序列化开销✅
在 ArkTS 重写 TestMints 生命周期页面调用简单共享库与页面逻辑容易分叉❌
4.3 Kotlin/Native C ABI

NativeBridge.kt 只开放三个符号:

@CName("TestMintsRunChecks")
public fun runChecksNative(): CPointer<ByteVar> = response {
    val checkJson = TestMintsExamples.runChecks()
        .joinToString(separator = ",") { quote(it) }
    "{\"passed\":true,\"checks\":[$checkJson]}"
}

@CName("TestMintsRunScenario")
public fun runScenarioNative(
    title: CPointer<ByteVar>?,
    quantity: Int,
): CPointer<ByteVar> = response {
    val result = TestMintsExamples.runScenario(title?.toKString().orEmpty(), quantity)
    val eventJson = result.events.joinToString(separator = ",") { quote(it) }
    "{\"passed\":true,\"title\":${quote(result.title)}," +
        "\"quantity\":${result.quantity},\"output\":${result.output}," +
        "\"events\":[$eventJson]}"
}

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

返回字符串由 Native heap 分配,并以 0 结尾。每次 N-API 创建 JavaScript 字符串后,必须调用 TestMintsFree 释放对应缓冲区。

4.4 linker map 限制导出符号

example/nativeApp/src/ohosArm64Main/linker/shared-library.map 显式导出上述三个符号。这样动态库的公共边界不会随着 Kotlin/Native 内部实现变化而扩大:

{
  global:
    TestMintsRunChecks;
    TestMintsRunScenario;
    TestMintsFree;
  local: *;
};

这是一次容易遗漏的配置。没有 linker map 导出时,Native 库可能能够生成,但 CMake 或 N-API 在加载时找不到函数;修改导出后需要重新执行 linkDebugSharedOhosArm64。

4.5 C++ N-API 方法分发

napi_init.cpp 注册 runChecks 和 runScenario 两个 ArkTS 方法:

static napi_value Init(napi_env env, napi_value exports) {
  napi_property_descriptor methods[] = {
      {"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
          napi_default, nullptr},
      {"runScenario", nullptr, RunScenario, nullptr, nullptr, nullptr,
          napi_default, nullptr},
  };
  if (napi_define_properties(env, exports, 2, methods) != napi_ok) {
    napi_throw_error(env, nullptr, "Unable to register TestMints exports");
    return nullptr;
  }
  return exports;
}

RunScenario 检查参数数量,读取不超过 255 字节的标题和一个 int32 数量,再调用 TestMintsRunScenario。成功创建 JS 字符串后立即释放 Native 缓冲区;创建失败时也先释放再抛错。

4.6 CMake 导入动态库

DevEco 工程把 Kotlin/Native 产物作为 imported library,并与 N-API 系统库链接:

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

add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE testmints libace_napi.z.so)

页面侧类型声明位于 entry/src/main/cpp/types/libentry/index.d.ts,调用方式为:

import { runScenario } from '../testmints/TestMintsClient';
const json = runScenario('订单回访', 3);
const result = JSON.parse(json);

第 5 阶段:ArkUI 交互页面

5.1 页面状态和调用封装

TestMintsClient.ets 将 Native 字符串解析成页面使用的结果类型:

import { runScenario as nativeRunScenario } from 'libentry.so';

export interface ScenarioResult {
  passed: boolean;
  title: string;
  quantity: number;
  output: number;
  events: string[];
}

export function runScenario(title: string, quantity: number): ScenarioResult {
  return JSON.parse(nativeRunScenario(title, quantity)) as ScenarioResult;
}

Index.ets 保存场景名称、数量文本、执行状态、错误文本和最后一次结果。页面输入先在 ArkTS 中规范化,Native 共享层再次规范化,保证入口和核心逻辑都具备边界保护。

5.2 页面交互预设

页面提供三个预设:

  • 订单回访:适合展示默认效果,输入 3 时输出 6;
  • 消息同步:用于模拟同步类场景;
  • 资料校验:用于模拟校验类场景。

点击预设只改变场景名称和状态文案;点击“执行 TestMints 场景”才会真正经过 N-API 调用。数量可以通过输入框或 -、+ 按钮调整,并始终限制在 1..99。

5.3 结果卡和生命周期日志

结果卡显示四类信息:

PASSED       TestMints 场景执行成功
INPUT        3 次
OUTPUT       6
BRIDGE       N-API

日志区域按共享 Kotlin 代码写入的顺序展示:

setup · 准备「订单回访」
exercise · 执行 3 次
verify · 检查结果 6
teardown · 记录执行摘要
template teardown · 场景已收尾

其中 setup 和 template teardown 来自 testTemplate 的共享生命周期,exercise、verify 和 teardown 来自单个场景。日志展示让用户可以观察 TestMints 的执行顺序,而不是只看到一个布尔值。

5.4 错误状态

Native 返回错误 JSON 或 N-API 抛出异常时,页面把状态改为“场景执行失败”,并显示错误文本;旧结果会被清空。空标题、非数字输入和越界数量会在执行前被规范化,不会直接导致页面崩溃。

第 6 阶段:示例与验证

6.1 example 工程结构
example/
├── shared/
│   ├── src/commonMain/kotlin/com/zegreatrob/testmints/example/
│   │   └── TestMintsExamples.kt
│   └── src/commonTest/kotlin/com/zegreatrob/testmints/example/
│       └── TestMintsExamplesTest.kt
├── nativeApp/
│   ├── src/ohosArm64Main/kotlin/.../NativeBridge.kt
│   ├── src/ohosArm64Main/linker/shared-library.map
│   └── build.gradle.kts
└── ohosApp/
    └── entry/
        ├── src/main/cpp/CMakeLists.txt
        ├── src/main/cpp/napi_init.cpp
        └── src/main/ets/
            ├── pages/Index.ets
            └── testmints/TestMintsClient.ets

shared 验证 TestMints 语义,nativeApp 生成 OpenHarmony ARM64 库,ohosApp 负责 ArkUI 页面和动态库加载。每一层都可以单独定位故障。

6.2 原生模块注册

napi_init.cpp 通过 napi_module_register 注册 entry 模块。此前最容易出现的两个问题是:

  1. napi_define_properties 仍然传 1,导致第二个 runScenario 方法没有注册;
  2. Kotlin linker map 没有导出 TestMintsRunScenario 或 TestMintsFree,导致链接或加载失败。

当前实现传入方法数量 2,并显式导出三个 C ABI 符号。ArkTS 通过 libentry.so 的类型声明调用它们。

6.3 Native 动态库准备

执行根工程脚本:

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

脚本依次运行 libraries 的 standard:jvmTest、async:jvmTest,再运行 example 的 shared:jvmTest 和 nativeApp:prepareOhos。成功后应生成:

example/ohosApp/entry/libs/arm64-v8a/libtestmints.so
example/ohosApp/entry/src/main/cpp/include/libtestmints_api.h
6.4 ARM64 依赖检查

使用 OpenHarmony Native SDK 的 LLVM 工具检查动态库:

python3 scripts/check-native-deps.py \
  /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
  example/ohosApp/entry/libs/arm64-v8a

期望看到:

libtestmints.so: 0 unresolved strong imports

这个检查只验证 ELF 架构、系统导出和强未解析符号,不等同于签名或设备功能验收。

6.5 HAP 构建与签名入口

打开 example/ohosApp 后可以在 DevEco Studio 中同步并构建未签名 HAP。若要生成签名工程,使用仓库脚本创建隔离副本:

python3 scripts/prepare-signing-project.py /absolute/path/testmints-signing
./scripts/build-hap.sh /absolute/path/testmints-signing

签名证书、Profile、p12 和密码应由开发者在本机 DevEco 工程配置。本文不包含任何真实签名材料,也不把本地 build-profile.json5 当作可复制配置。没有配置证书时,只能确认源码、Native 库和 DevEco 工程可以构建,不能据此声称完成签名安装。

四、完整代码对照

4.1 整体架构

用户输入场景名称和数量
          │
          ▼
ArkUI Index.ets
          │ runScenario(title, quantity)
          ▼
TestMintsClient.ets / libentry.so
          │ C++ N-API
          ▼
NativeBridge.kt
          │ TestMintsExamples.runScenario
          ▼
libraries/standard testTemplate
          │ ScenarioResult → JSON
          ▼
ArkTS JSON.parse → 结果卡和生命周期日志

4.2 文件清单

文件作用
convention-plugins/src/main/kotlin/.../platforms.gradle.kts声明 ohosArm64 和定向依赖替换
example/shared/build.gradle.kts共享示例的 JVM/Native 目标和 TestMints 源码目录
example/shared/src/commonMain/.../TestMintsExamples.kt场景模型和 testTemplate 生命周期
example/nativeApp/.../NativeBridge.ktC ABI JSON 输出和 Native 内存释放
example/nativeApp/.../shared-library.map动态库导出边界
example/nativeApp/build.gradle.ktsARM64 动态库和 prepareOhos 任务
example/ohosApp/entry/src/main/cpp/napi_init.cppN-API 方法注册和参数检查
example/ohosApp/entry/src/main/cpp/CMakeLists.txt导入 libtestmints.so 并链接 N-API
example/ohosApp/entry/src/main/ets/pages/Index.ets交互页面、结果卡和日志列表
example/ohosApp/entry/src/main/ets/testmints/TestMintsClient.etsArkTS 结果类型和 JSON 解析
scripts/build-openharmony.shJVM 测试和 Native 产物准备
scripts/check-native-deps.pyARM64 ELF 依赖检查

4.3 关键 API 对照

Kotlin/Native 符号ArkTS 方法作用
TestMintsRunChecksrunChecks执行固定的共享模板自检
TestMintsRunScenariorunScenario执行用户配置的场景并返回 JSON
TestMintsFree内部调用释放 Native heap 字符串

示例结果 JSON:

{
  "passed": true,
  "title": "订单回访",
  "quantity": 3,
  "output": 6,
  "events": [
    "setup · 准备「订单回访」",
    "exercise · 执行 3 次",
    "verify · 检查结果 6",
    "teardown · 记录执行摘要",
    "template teardown · 场景已收尾"
  ]
}

4.4 ArkTS 与 Kotlin 的边界

ArkTS 负责输入收集、按钮状态、结果展示和错误文案;Kotlin 负责 TestMints 模板和结果语义;C++ 只负责类型检查、函数分发、字符串转换和内存释放。这个边界避免在 ArkTS 中重复实现 verify 规则,也让 JVM 测试与设备页面使用同一份场景代码。

五、关键决策说明

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

OpenHarmony 适配必须让共享 Kotlin 代码真正生成 Native 目标。只增加一个 DevEco 页面不能证明三方库可用,因此目标配置放在构建约定中,并让库模块和示例共同复用。

决策 2:只对 OHOS configuration 替换依赖

OpenHarmony 兼容构件与 JVM/JS 构件的发布坐标不同。按 configuration 替换可以缩小影响范围,避免为了一个新目标改变其他平台的解析结果。

决策 3:用场景门面承接 TestMints DSL

测试 DSL 的泛型模板对象不适合直接跨语言。TestMintsExamples 把它收敛成 ScenarioResult,同时保留 setup、exercise、verify 和 teardown 的执行证据。

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

场景结果包含字符串、整数和数组。JSON 可读、易调试,ArkTS 解析简单;C ABI 只需要管理一块明确释放的 UTF-8 缓冲区。

决策 5:交互页面采用真实业务语境

“订单回访”“消息同步”“资料校验”比单纯的“运行测试”更能体现库的使用方式。用户可以修改场景名称和数量,观察同一套模板如何组织业务行为与结果。

决策 6:把库验证、Native 验证和签名验证分开

JVM 通过说明共享逻辑正确,ELF 检查说明 ARM64 动态库依赖完整,DevEco 构建说明工程可打包。签名和真机安装还需要本地证书、Profile、设备和授权条件,不能用前面的结果替代。

六、测试与验证

6.1 测试环境

建议使用:

环境要求
macOS能运行 Gradle、DevEco Studio 和 OpenHarmony Native SDK
JDK21
DevEco Studio与本机 OpenHarmony SDK 匹配的版本
Native SDK包含 llvm-nm、llvm-readelf 和 AArch64 sysroot
设备ARM64 OpenHarmony 设备;是否支持完整功能由设备能力决定

6.2 静态检查与单元测试

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

该命令覆盖:

  1. libraries:standard:jvmTest;
  2. libraries:async:jvmTest;
  3. example:shared:jvmTest;
  4. example:nativeApp:prepareOhos。

TestMintsExamplesTest 至少验证两件事:固定模板自检返回两条检查,以及“消息同步”输入 4 时输出 8、结果通过且包含生命周期事件。

6.3 原生桥接和 HAP 验证

在 Native 产物生成后运行 check-native-deps.py,确认 libtestmints.so 是 AArch64 ELF 且无未解析强符号。随后在 DevEco Studio 打开 example/ohosApp,同步 CMake 和 ArkTS,构建 HAP。

N-API 侧重点包括:

  • runChecks 和 runScenario 都已注册;
  • napi_define_properties 的数量为 2;
  • TestMintsRunScenario、TestMintsRunChecks、TestMintsFree 都在 linker map 中;
  • JS 字符串创建成功和失败路径都会释放 Native 缓冲区;
  • 标题和数量参数类型不正确时返回可读异常。

6.4 功能验证用例

用例 1:默认场景执行

进入页面,默认显示“订单回访”和 3。点击“执行 TestMints 场景”,确认状态变为已完成,结果卡显示 PASSED、INPUT 3 次、OUTPUT 6,并出现五条生命周期日志。

用例 2:切换预设

依次点击“消息同步”和“资料校验”,确认场景标题更新,执行后日志中的 setup 文案使用新的标题。

用例 3:调整数量

使用 -、+ 或文本输入把数量改为 4,执行后确认输出为 8。输入空值、负数或超过 99 时,页面将数量规范化到有效范围。

用例 4:空标题

清空场景名称并执行,确认结果标题为“未命名场景”,而不是出现空卡片或 Native 异常。

用例 5:重复执行

连续点击执行按钮,确认每次返回新的结果和完整日志,不会把上一次的事件继续追加到下一次结果中。

用例 6:Native 错误显示

当 Native 返回错误 JSON 或 N-API 抛出异常时,确认页面显示“场景执行失败”和错误文本,并清空旧结果。

6.5 验证结论

根据项目中的构建脚本和验证记录,JVM 测试、Native 动态库准备及 ELF 依赖检查具备明确命令入口,示例页面具备场景执行和结构化结果展示能力。本文附带的效果图来自用户提供的运行截图,展示了 订单回访、输入 3、输出 6、PASSED 和生命周期日志。

本文不把未在当前工作流中执行的签名 HAP、设备安装或现场操作结果写成已完成事实。需要真机交付时,应在配置好本地签名材料后按第 6 阶段命令继续验证。

七、运行效果

7.1 效果图

在这里插入图片描述

图中页面展示了一个完整的人机交互场景:

  • 顶部显示 TestMints 三方库 和 Kotlin Multiplatform / OpenHarmony 说明;
  • 预设场景选择为“订单回访”;
  • 用户输入执行次数 3 并点击执行;
  • 结果卡显示标题、INPUT 3 次、OUTPUT 6、BRIDGE N-API;
  • 状态标签显示 PASSED;
  • 日志区域按顺序显示 setup、exercise、verify、teardown 和 template teardown。

这里的 6 来自示例 exercise 的 quantity * 2 计算,重点是展示 TestMints 生命周期从 Kotlin/Native 传到 ArkUI 的完整路径。

7.2 命令速查

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

# 单独运行共享示例测试
(cd example && ./gradlew :shared:jvmTest)

# 检查 ARM64 动态库依赖
python3 scripts/check-native-deps.py \
  /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
  example/ohosApp/entry/libs/arm64-v8a

# 生成独立签名工程,按需配置证书后构建 HAP
python3 scripts/prepare-signing-project.py /absolute/path/testmints-signing
./scripts/build-hap.sh /absolute/path/testmints-signing

# 配置设备和签名后安装,设备序列号与 HAP 路径按本机实际情况填写
hdc -t <设备序列号> install -r <signed-hap-path>

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配:共享 TestMints 代码必须实际编译成 ohosArm64 动态库。
  2. OpenHarmony 依赖不能全局替换:只对 ohosArm64 configuration 做替换,才能保持原平台构建稳定。
  3. N-API 注册数量必须准确:同时增加 runChecks 和 runScenario 时,napi_define_properties 必须传 2。
  4. 导出符号必须和头文件一致:linker map 缺少任一 C ABI 符号时,CMake 链接或运行时加载会失败。
  5. Native 字符串必须成对释放:创建 JS 字符串后立即调用 TestMintsFree,错误路径也不能遗漏。
  6. 输入语义要和实现一致:示例中的“执行次数”目前参与 quantity * 2 演示计算,不代表真实循环执行业务 N 次。
  7. 签名和构建是两个阶段:没有证书、Profile 和密码时,不能把 unsigned HAP 当作可安装交付物。

8.2 已知问题

  • 当前示例使用同步 testTemplate,尚未在 ArkUI 页面中展示 async 模块的协程交互;
  • runScenario 的结果序列化使用轻量手写 JSON 转义,字段增加时需要同步维护转义逻辑;
  • Native 动态库和生成头文件需要每台开发机重新准备,不能直接依赖其他机器的构建产物;
  • 签名材料和设备安装依赖本地 DevEco 配置,仓库不提供通用证书;
  • 示例页面是单页面工作台,多页面产品需要在业务层集中管理 Native 调用和状态缓存。

8.3 未来优化方向

  • 增加 async 场景预设,展示协程 TestMints 的执行与错误处理;
  • 将 ScenarioResult 改为正式的跨平台序列化模型,减少手写 JSON 维护成本;
  • 为页面增加可折叠日志、耗时统计和失败步骤定位;
  • 增加真实业务适配示例,让 exercise 调用仓储、网络或本地数据操作;
  • 在持续集成中加入 ohosArm64 编译、Native 依赖检查和未签名 HAP 构建;
  • 在配置好授权的设备流水线中补充签名、安装和启动验收。

九、总结

9.1 核心难点回顾

TestMints 的 OpenHarmony 适配不是把一个测试按钮搬到 ArkUI,而是一条完整跨端链路:

ArkUI 场景输入
    → C++ N-API 参数检查
    → Kotlin/Native C ABI
    → TestMints testTemplate
    → ScenarioResult 和 JSON
    → ArkTS 结果卡与生命周期日志

9.2 封装层次

  • KMP 层:复用 TestMints 标准模块的 testTemplate、exercise、verifyAnd 和 teardown;
  • 示例共享层:把模板执行收敛为 ScenarioResult,提供输入规范化和事件日志;
  • Native 层:生成 ARM64 动态库,通过三个 C ABI 函数输出 JSON;
  • N-API 层:完成参数读取、字符串转换、方法注册和内存释放;
  • ArkTS 层:管理用户输入、按钮交互、JSON 解析、结果展示和错误状态;
  • DevEco 层:完成 CMake、Hvigor、HAP 和后续签名安装。

9.3 三条经验

  1. 先在 JVM 验证共享 TestMints 语义,再接入 Kotlin/Native 和 ArkUI;
  2. 用稳定 JSON 和少量 C ABI 代替跨语言对象传递;
  3. 把共享逻辑、Native 依赖、HAP 构建、签名和设备安装分别记录,避免把“编译通过”误认为“完成真机交付”。

9.4 适配成果

当前 TestMints OpenHarmony 示例已经具备:

  • ohosArm64 KMP 目标和 OHOS 兼容依赖替换;
  • TestMintsExamples 共享场景门面;
  • setup、exercise、verifyAnd、teardown 和模板级 teardown 的可视化日志;
  • Kotlin/Native + C ABI + C++ N-API 桥接;
  • runChecks 和 runScenario 双方法注册;
  • ARM64 动态库、CMake 导入和 Native 依赖检查;
  • 包含预设、输入、数量步进、执行、结果和日志的人机交互页面;
  • 本地效果图和 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。

参考文档

Logo

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

更多推荐