本文记录
multik接入 OpenHarmony 的完整过程,覆盖 KMP 工程盘点、ohosArm64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、矩阵计算验证、签名 HAP 和真机验收。本次适配复用 Multik Kotlin 侧的 ndarray、数学、线性代数和统计能力,再由 ArkTS 调用 N-API 获取统一 JSON 结果。这样验证的是同一份 KMP 代码在 OpenHarmony ARM64 设备上的运行结果,而不是重新在页面里实现一套矩阵计算逻辑。
项目地址: AtomGit/oh-tpc/multik
上游仓库: Kotlin/kotlin-jupyter
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做开源鸿蒙平台 KMP/CMP 适配
Multik 是 Kotlin Multiplatform 多维数组和数学计算库,核心 API 包括 ndarray 创建、数学运算、线性代数和统计操作。原有工程覆盖 JVM、Native、JS、WASM 等目标,但 OpenHarmony 应用不能直接消费桌面 Native 产物,需要为 ARM64 设备增加独立的 Kotlin/Native 目标,并把计算结果安全地交给 ArkTS。
如果只把页面重新写成 ArkTS,页面可以显示一组预先写死的数字,却无法证明共享 Kotlin 模型、Kotlin/Native 动态库、CMake、N-API 和实际设备已经连通。适配需要解决下面几个问题:
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | KMP 模块默认没有 OpenHarmony 目标,必须增加 ohosArm64() 才能生成 OpenHarmony KLIB。 |
| 引擎选择 | multik-default 在 OpenHarmony 上不能链接桌面 OpenBLAS,需要明确回退到纯 Kotlin KEEngine。 |
| 工具链不一致 | Kotlin/Native、OpenHarmony LLVM、Native SDK、Gradle 和 Hvigor 必须使用匹配版本。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin MultikSnapshot,必须经过 C ABI、C++ N-API 和 JSON。 |
| Native 产物缺失 | DevEco CMake 不会自动生成 libmultik.so 和头文件,必须先由独立 Gradle 工程准备。 |
| ABI 约束 | HAP 使用 arm64-v8a,动态库也必须是 OpenHarmony ARM64 产物。 |
| 交付链路复杂 | Native 动态库、CMake、HAP、签名、设备安装和 ARM64 依赖需要分别验证。 |
因此,本项目把适配边界放在三个地方:Kotlin/Native 目标和引擎配置、C ABI/N-API 桥接层、ArkUI 页面层。状态和计算仍由 KMP 维护,ArkTS 只负责调用 Native 方法、解析 JSON 和展示结果。
当前示例的 DevEco 宿主使用 ArkUI/ArkTS,并未把 Compose Multiplatform UI 运行时直接嵌入 HAP。这里的 KMP/CMP 文章重点是共享 Kotlin 计算能力和跨端边界;后续可以复用同一 commonMain API,为 Android、桌面或 Compose Multiplatform 页面增加对应 UI。
1.2 库提供的能力
当前 OpenHarmony 示例使用以下 Multik 能力:
mk.ndarray:创建二维NDArray;mk.linalg.pow:计算矩阵平方;mk.math.sum:计算数组元素总和;mk.stat.mean:计算平均值;mk.linalg.norm:计算矩阵范数;mk.engine:读取当前默认引擎名称;MultikExamples.runChecks:执行共享计算自检,并通过 JSON 暴露给 ArkTS。
示例矩阵和期望结果如下:
| 运算 | 输入或表达式 | 期望结果 |
|---|---|---|
| 形状 | mk[mk[1.0, 2.0], mk[3.0, 4.0]] |
2x2 |
| 矩阵平方 | matrix 的二次幂 |
[[7.0, 10.0], [15.0, 22.0]] |
| 元素总和 | mk.math.sum(matrix) |
10.0 |
| 平均值 | mk.stat.mean(matrix) |
2.5 |
| 范数 | mk.linalg.norm(matrix) |
sqrt(30.0),约 5.477 |
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 矩阵创建、平方、sum、mean、norm 和自检全部由 Kotlin 共享代码完成。 |
| 平台目标 | 为 Multik 模块和示例加入 ohosArm64,生成 libmultik.so。 |
| 引擎策略 | OpenHarmony 使用纯 Kotlin KEEngine,不依赖桌面 OpenBLAS。 |
| 桥接稳定 | 使用 MultikSnapshot、MultikRunChecks 和 MultikFree 三个明确的 C ABI 边界。 |
| UI 完整 | 页面显示当前引擎、矩阵、平方矩阵、形状、sum、mean、norm 和重新计算按钮。 |
| 可测试 | JVM 测试、Native 链接、ELF 依赖、HAP 构建、设备安装分别验收。 |
| 签名安全 | 源码只保留签名配置入口,证书和密钥材料由开发者本机配置。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面是独立的真机验收宿主,公共 API 保持平台无关。其他 KMP/CMP 应用可以复用 Multik 的
mk、NDArray和引擎 API,再自行决定如何呈现计算结果。
二、实现路线图
text
第 1 阶段:项目初始化 ── 盘点 Multik 模块、引擎模型和 OpenHarmony 示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、独立 example 工程和 Native 构建任务
第 3 阶段:计算与序列化 ── 建立 MultikSnapshot、共享计算和 JSON 契约
第 4 阶段:原生桥接 ── Kotlin/Native C ABI、C++ N-API、内存释放和类型检查
第 5 阶段:ArkUI 页面 ── 解析快照、显示矩阵、处理加载异常和页面重算
第 6 阶段:示例与验证 ── HAP 构建、签名、设备安装、计算结果和效果图
每个阶段都使用真实产物作为下一阶段输入:先用 JVM 验证共享计算,再把相同代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,最后安装签名 HAP 观察设备页面。
三、逐步实现过程
第 1 阶段:项目初始化
1.1 盘点公共 API 和工程边界
项目采用 KMP 库、独立消费者和 DevEco 宿主分层:
text
multik-core/ Multik 公共 API、NDArray 和引擎接口
multik-kotlin/ 纯 Kotlin KEEngine
multik-default/ 默认引擎选择和 OpenHarmony actual 实现
example/shared/ 示例快照、计算自检和共享测试
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/libs.versions.toml 中的版本 |
JVM、Kotlin/Native 和 KLIB |
| JDK | 21 | Gradle、Kotlin 编译和 Native 任务 |
| OpenHarmony 目标 | ohosArm64 |
ARM64 真机动态库 |
| DevEco 工具 | Hvigor、CMake、Ninja、Native SDK | CMake 配置和 HAP 构建 |
| ABI | arm64-v8a |
HAP 原生库架构 |
| 应用包名 | org.jetbrains.kotlinx.multik.sample |
安装、启动和签名匹配 |
执行 Gradle 脚本前先选择 JDK 21:
bash
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
java -version
设备是否可以安装 HAP、签名是否可信、hdc 是否已授权,都要单独验证。API 和 SDK 版本满足要求,只说明工程可以编译,不能替代真机验收。
1.3 创建 OpenHarmony 示例目录
示例页面没有把计算结果写死,而是围绕"当前引擎、矩阵、派生矩阵和统计量"组织:
text
标题区 Multik 工作台 / Kotlin Multiplatform + OpenHarmony
状态区 NATIVE / READY
矩阵区 [[1.0, 2.0], [3.0, 4.0]]
派生区 平方矩阵 [[7.0, 10.0], [15.0, 22.0]]
统计区 SHAPE 2x2 / SUM 10 / MEAN 2.5 / NORM 5.477
操作区 重新计算
说明区 Native Multik 结果经 N-API 提供给 ArkTS
页面初始化时调用 Native getSnapshot() 和 runChecks()。点击"重新计算"会再次走完整的 Kotlin/Native → C ABI → N-API → ArkTS 链路。
第 2 阶段:目标与依赖打通
2.1 加入 ohosArm64 目标
公共模块保留原有 KMP 目标,同时增加 OpenHarmony ARM64 目标。OpenHarmony 的 DefaultEngineFactory 选择 KEEngine,避免引用只支持桌面 JVM/Native 的 OpenBLAS:
kotlin
internal actual object DefaultEngineFactory : EngineFactory {
actual override fun getEngine(type: EngineType?): Engine =
when (type) {
null, KEEngineType, DefaultEngineType -> KEEngine()
NativeEngineType -> error("Don't exist native engine for OpenHarmony targets")
}
}
2.2 Native focused build 的作用
example/nativeApp 的 prepareOhos 任务依赖 linkDebugSharedOhosArm64,并把 Native 产物复制到 DevEco 工程:
kotlin
val prepareOhos by tasks.registering(Copy::class) {
dependsOn("linkDebugSharedOhosArm64")
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libmultik.so")
into("libs/arm64-v8a")
}
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libmultik_api.h")
into("src/main/cpp/include")
}
into(rootProject.layout.projectDirectory.dir("../example/ohosApp/entry"))
}
成功后必须存在:
text
example/ohosApp/entry/libs/arm64-v8a/libmultik.so
example/ohosApp/entry/src/main/cpp/include/libmultik_api.h
这两个文件是构建产物,不应该提交空文件或其他 ABI 的 .so。DevEco CMake 会在配置阶段主动检查它们。
2.3 配置仓库和独立消费工程
example/settings.gradle.kts 使用 OpenHarmony 社区 Maven、Maven Local、Maven Central 和 Gradle Plugin Portal:
kotlin
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenLocal()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenLocal()
mavenCentral()
}
}
示例通过本地发布的 org.jetbrains.kotlinx:multik-default 消费库,不把 ohosApp 直接 include 到根工程。这样可以分别定位 Maven 解析、Native 链接和 HAP 打包问题。
2.4 通过构建产物消费共享库
根目录脚本先发布 OpenHarmony publication,再运行示例准备任务:
bash
./scripts/build-openharmony.sh
脚本内部使用 -PskipApiValidation -PskipOpenblas。前者规避当前 Binary Compatibility Validator 与 ohosArm64 目标配置的兼容问题,后者避免构建 OpenHarmony 不需要的桌面 OpenBLAS 任务。
第 3 阶段:计算与序列化
3.1 为什么需要统一 JSON 契约
ArkTS、C++ 和 Kotlin/Native 不能直接共享 Kotlin 对象,因此边界使用 UTF-8 JSON:
text
ArkTS 页面
│ getSnapshot() / runChecks()
▼
libentry.so N-API
│ C string
▼
MultikSnapshot / MultikRunChecks
│ Kotlin/Native
▼
NDArray + Math + LinAlg + Statistics
页面只消费 JSON,不复制矩阵计算和结果格式化规则。JVM 测试、Native 动态库和设备页面因此可以围绕同一份 MultikExamples 验收。
3.2 MultikSnapshot 和 MultikChecks
共享示例定义两个跨边界数据结构:
kotlin
public class MultikSnapshot(
public val engine: String,
public val shape: String,
public val matrix: String,
public val squared: String,
public val sum: Double,
public val mean: Double,
public val norm: Double,
)
public class MultikChecks(
public val passed: Boolean,
public val checks: List<String>,
)
3.3 共享计算和自检
kotlin
public fun snapshot(): MultikSnapshot {
val matrix = mk.ndarray(mk[mk[1.0, 2.0], mk[3.0, 4.0]])
val squared = mk.linalg.pow(matrix, 2)
return MultikSnapshot(
engine = mk.engine ?: "unavailable",
shape = matrix.shape.joinToString("x"),
matrix = matrix.toString(),
squared = squared.toString(),
sum = mk.math.sum(matrix),
mean = mk.stat.mean(matrix),
norm = mk.linalg.norm(matrix),
)
}
runChecks() 覆盖形状、sum、mean、平方矩阵和 norm 五项检查:
kotlin
public fun runChecks(): MultikChecks {
val value = snapshot()
val checks = buildList {
add("shape=${value.shape}" to (value.shape == "2x2"))
add("sum=${value.sum}" to (value.sum == 10.0))
add("mean=${value.mean}" to (value.mean == 2.5))
add("square=${value.squared}" to
(value.squared == "[[7.0, 10.0], [15.0, 22.0]]"))
add("norm=${value.norm}" to
(kotlin.math.abs(value.norm - kotlin.math.sqrt(30.0)) < 1e-10))
}
return MultikChecks(checks.all { it.second }, checks.map { it.first })
}
JSON 字段保持稳定:
json
{
"engine": "DEFAULT",
"shape": "2x2",
"matrix": "[[1.0, 2.0], [3.0, 4.0]]",
"squared": "[[7.0, 10.0], [15.0, 22.0]]",
"sum": 10.0,
"mean": 2.5,
"norm": 5.477225575051661
}
第 4 阶段:原生桥接(技术难点)
4.1 Kotlin/Native 对象不能直接交给 ArkTS
MultikSnapshot 和 MultikChecks 属于 Kotlin 运行时对象。ArkTS 只能接收 JavaScript 值,C++ 也不能把 Kotlin 对象地址直接当成 JavaScript 对象。
最终采用三层桥接:
text
ArkTS
│ JSON string
▼
C++ N-API entry
│ const char*
▼
Kotlin/Native C ABI
│ MultikExamples
▼
UTF-8 JSON + explicit free
4.2 方案对比
| 方案 | 优点 | 缺点 | 选用 |
|---|---|---|---|
| 直接导出 Kotlin 对象 | 代码少 | ABI、生命周期和类型不可控 | ❌ |
| 只导出数字数组 | 实现简单 | 页面会重新实现矩阵和统计逻辑 | ❌ |
| C ABI + JSON | 边界清晰、易扩展、易调试 | 有一次序列化开销 | ✅ |
| 在 ArkTS 重写完整 Multik 逻辑 | 页面调用简单 | KMP 和 ArkTS 逻辑容易分叉 | ❌ |
4.3 Kotlin/Native 导出函数
Native 桥接导出快照、自检和释放三个符号:
kotlin
private inline fun response(block: () -> String): CPointer<ByteVar> = try {
textBuffer(block())
} catch (error: Throwable) {
textBuffer("{\"error\":${quote(error.message ?: "Multik error")}}")
}
@CName("MultikSnapshot")
public fun multikSnapshotNative(): CPointer<ByteVar> =
response { MultikExamples.snapshot().toJson() }
@CName("MultikRunChecks")
public fun multikChecksNative(): CPointer<ByteVar> =
response { MultikExamples.runChecks().toJson() }
@CName("MultikFree")
public fun multikFreeNative(pointer: CPointer<ByteVar>?) {
if (pointer != null) nativeHeap.free(pointer.rawValue)
}
返回值使用 Native heap 分配的、以 0 结尾的 C 字符串。每一块返回缓冲区都由 MultikFree 释放。
4.4 C++ N-API 方法分发
C++ 注册两个 ArkTS 方法:
cpp
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor methods[] = {
{"getSnapshot", nullptr, Snapshot, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
napi_default, nullptr},
};
if (napi_define_properties(env, exports, 2, methods) != napi_ok) {
napi_throw_error(env, nullptr, "Unable to register Multik exports");
return nullptr;
}
return exports;
}
Snapshot 和 RunChecks 都遵循"调用 Native、创建 ArkTS 字符串、释放 Native 缓冲区"的顺序:
cpp
static napi_value Snapshot(napi_env env, napi_callback_info) {
const auto text = MultikSnapshot();
auto result = ReadText(env, reinterpret_cast<const char*>(text));
MultikFree(text);
return result;
}
4.5 CMake 将 .so 作为 imported library
cmake
set(MULTIK_LIBRARY "${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libmultik.so")
set(MULTIK_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/include/libmultik_api.h")
if(NOT EXISTS "${MULTIK_LIBRARY}" OR NOT EXISTS "${MULTIK_HEADER}")
message(FATAL_ERROR
"Multik native artifacts are missing. Run './scripts/build-openharmony.sh'")
endif()
add_library(multik SHARED IMPORTED)
set_target_properties(multik PROPERTIES IMPORTED_LOCATION "${MULTIK_LIBRARY}")
add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE multik libace_napi.z.so)
CMake 配置阶段就检查 .so 和头文件,因此"Native 文件不存在"和"Ninja 找不到库"会在最接近根因的位置报错。
第 5 阶段:ArkUI 页面封装
5.1 ArkTS 调用 N-API
MultikClient.ets 不重复实现 Multik 逻辑,只解析 Native 返回的 JSON:
typescript
import multikNative from 'libentry.so';
export interface MultikSnapshot {
engine: string;
shape: string;
matrix: string;
squared: string;
sum: number;
mean: number;
norm: number;
}
function parse<T>(text: string): T {
return JSON.parse(text) as T;
}
export function snapshot(): MultikSnapshot {
return parse<MultikSnapshot>(multikNative.getSnapshot());
}
5.2 ArkUI 页面状态
Index.ets 保存快照、检查状态和错误文本:
typescript
@Entry
@Component
struct Index {
@State private current: MultikSnapshot = emptySnapshot();
@State private status: string = '正在加载';
@State private errorText: string = '';
aboutToAppear(): void {
this.refresh();
}
private refresh(): void {
try {
this.current = snapshot();
const checks: MultikChecks = runChecks();
this.status = checks.passed
? `${checks.checks.length}/5 Multik 检查通过`
: 'Multik 检查失败';
this.errorText = '';
} catch (error) {
this.status = 'Native Multik 加载失败';
this.errorText = String(error);
}
}
}
页面只负责把 current.matrix、current.squared、current.sum、current.mean 和 current.norm 显示出来。点击"重新计算"会再次调用 refresh(),用于验证 Native 链路可以重复执行。
5.3 应用包名和入口能力
宿主 AppScope/app.json5 使用:
json5
{
"app": {
"bundleName": "org.jetbrains.kotlinx.multik.sample",
"vendor": "ohos",
"versionCode": 1000000,
"versionName": "1.0.0"
}
}
入口能力为 EntryAbility,支持 phone 和 tablet。当前示例不申请额外的传感器或运动权限,因为它只执行本地矩阵计算,不订阅系统硬件事件。
5.4 页面交互预设
页面提供"重新计算"操作,适合验证以下场景:
- Native 库首次加载是否成功;
- 当前引擎和矩阵结果是否能够稳定解析;
- JSON 中的矩阵字符串是否保持完整;
- 重复计算是否会刷新相同的数学结果;
- Native 异常是否能转换为可读的页面错误。
第 6 阶段:示例与验证
6.1 example 工程结构
text
example/
├── shared/
│ ├── src/commonMain/.../MultikExamples.kt
│ └── src/commonTest/.../MultikExamplesTest.kt
├── nativeApp/
│ ├── src/ohosArm64Main/.../NativeBridge.kt
│ └── src/ohosArm64Main/linker/shared-library.map
└── ohosApp/
├── AppScope/
├── entry/src/main/cpp/
│ ├── CMakeLists.txt
│ └── napi_init.cpp
└── entry/src/main/ets/
├── pages/Index.ets
└── multik/MultikClient.ets
shared 验证公共计算,nativeApp 产生 Native 动态库,ohosApp 负责 ArkUI 页面和 N-API。三者边界清晰,任何一层失败都能单独定位。
6.2 原生模块注册
napi_init.cpp 通过 napi_module_register 注册 entry 模块,类型声明位于:
text
example/ohosApp/entry/src/main/cpp/types/libentry/index.d.ts
ArkTS 使用:
typescript
import multikNative from 'libentry.so';
const result = JSON.parse(multikNative.getSnapshot());
6.3 Native 动态库准备
执行:
bash
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
./scripts/build-openharmony.sh
脚本会发布 multik-core、multik-kotlin 和 multik-default 的 OpenHarmony publication,再执行 example:nativeApp:prepareOhos。成功后生成:
text
example/ohosApp/entry/libs/arm64-v8a/libmultik.so
example/ohosApp/entry/src/main/cpp/include/libmultik_api.h
还可以检查 ARM64 ELF 的强依赖:
bash
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
预期结果为:
text
libmultik.so: 0 unresolved strong imports
6.4 构建、签名和安装
在 DevEco Studio 中打开 example/ohosApp,同步后选择 entry 模块和 default product。命令行构建 Native 和 HAP:
bash
"$DEVECO_HOME/tools/node/bin/node" \
"$DEVECO_HOME/tools/hvigor/bin/hvigorw.js" \
--mode module -p module=entry@default -p product=default \
compileNative --analyze=normal --parallel --incremental --daemon
"$DEVECO_HOME/tools/node/bin/node" \
"$DEVECO_HOME/tools/hvigor/bin/hvigorw.js" \
--mode module -p module=entry@default -p product=default \
-p requiredDeviceType=phone assembleHap --analyze=normal --parallel \
--incremental --daemon
签名 HAP 产物位于:
text
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap
安装并启动:
bash
HDC="/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc"
HAP="$PWD/example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap"
"$HDC" list targets
"$HDC" install -r "$HAP"
"$HDC" shell aa start -a EntryAbility -b org.jetbrains.kotlinx.multik.sample
签名证书、profile、p12 和密码只保存在本机,不要提交到 AtomGit。
四、完整代码对照
4.1 整体架构
text
Kotlin commonMain
│ mk.ndarray / math / linalg / stat
▼
ohosArm64 Kotlin/Native
│ MultikSnapshot / MultikRunChecks / MultikFree
▼
libmultik.so
│ imported by CMake
▼
libentry.so N-API
│ getSnapshot() / runChecks()
▼
ArkTS Index.ets
│ JSON.parse + ArkUI
▼
OpenHarmony HAP
4.2 文件清单
| 文件 | 职责 |
|---|---|
multik-default/src/ohosArm64Main/.../DefaultEngineFactory.kt |
OpenHarmony 默认引擎选择 |
multik-default/src/ohosArm64Main/.../DefaultOhosEngine.kt |
OpenHarmony 引擎初始化 |
example/shared/.../MultikExamples.kt |
快照、矩阵运算和自检 |
example/nativeApp/.../NativeBridge.kt |
C ABI、JSON 返回和内存释放 |
example/ohosApp/.../MultikClient.ets |
N-API JSON 解析 |
example/ohosApp/.../Index.ets |
ArkUI 真机页面 |
example/ohosApp/entry/src/main/cpp/napi_init.cpp |
N-API 导出和字符串转换 |
example/ohosApp/entry/src/main/cpp/CMakeLists.txt |
imported .so 和 N-API 链接 |
scripts/build-openharmony.sh |
OpenHarmony publication 和 Native 准备 |
scripts/build-hap.sh |
已配置签名工程的 HAP 构建 |
docs/openharmony/VALIDATION.md |
自动检查和真机验收清单 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| Kotlin | mk.ndarray |
创建二维数组 |
| Kotlin | mk.linalg.pow |
计算矩阵平方 |
| Kotlin | mk.math.sum |
计算元素总和 |
| Kotlin | mk.stat.mean |
计算平均值 |
| Kotlin | mk.linalg.norm |
计算范数 |
| Native | MultikSnapshot |
返回快照 JSON |
| Native | MultikRunChecks |
返回五项共享检查 |
| Native | MultikFree |
释放 Native 字符串 |
| N-API | getSnapshot |
向 ArkTS 暴露快照 |
| N-API | runChecks |
向 ArkTS 暴露自检 |
4.4 ArkTS 与 Kotlin 的边界
ArkTS 只负责调用和展示:
typescript
const value: MultikSnapshot = snapshot();
Text(value.matrix);
Text(`平方矩阵 ${value.squared}`);
Text(`${value.norm.toFixed(3)}`);
Kotlin 负责数据创建和数学语义:
kotlin
val matrix = mk.ndarray(mk[mk[1.0, 2.0], mk[3.0, 4.0]])
val squared = mk.linalg.pow(matrix, 2)
val norm = mk.linalg.norm(matrix)
两者之间只传输 JSON 字符串,不传输 Kotlin 对象、ArkTS class 实例或未校验的动态结构。
五、关键决策说明
决策 1:把 ohosArm64 加入公共构建约定
只有真正链接 OpenHarmony ARM64 动态库,才能证明共享 Kotlin 代码可以进入设备运行时。只在 JVM 上执行同样的计算,不能覆盖 CMake、Native SDK 和 HAP 的问题。
决策 2:OpenHarmony 使用 KEEngine
NativeEngine 依赖桌面 OpenBLAS,不适用于当前 OpenHarmony 目标。DefaultEngineFactory 在 OpenHarmony 上明确把默认类型映射到 KEEngine,保持功能可用并减少外部 Native 依赖。
决策 3:独立消费者必须通过构建产物消费
example 单独解析本地 Maven 产物,ohosApp 单独接收 .so 和头文件,避免根工程编译通过却无法在 DevEco 中打包。
决策 4:JSON 作为跨语言数据契约
JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,并允许后续增加字段而不暴露 Kotlin 对象布局。
决策 5:桥接层只开放三个 C ABI 入口
快照、自检和释放已经覆盖示例所需能力。减少 ABI 符号可以降低 Native 生命周期、符号导出和兼容风险。
决策 6:把库验证和设备验证分开
JVM 测试验证矩阵和统计规则,Native 链接验证 ABI,Hvigor 验证 HAP,真机验证安装、启动和页面展示。每一层都有明确的失败边界。
六、测试与验证
6.1 测试环境
本次真机验证使用:
- macOS;
- JDK 21;
- Kotlin Multiplatform 工程当前版本;
- DevEco Studio 及 OpenHarmony ARM64 Native SDK;
- 已签名
entry-default-signed.hap; - USB 连接的 HarmonyOS ARM64 真机;
hdc设备序列号FMR0223825079397;- 应用包名
org.jetbrains.kotlinx.multik.sample。
6.2 静态检查与单元测试
OpenHarmony 目标与 Binary Compatibility Validator 当前存在配置兼容问题,因此测试使用项目脚本相同的参数:
bash
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
./gradlew -PskipApiValidation :multik-core:jvmTest :multik-kotlin:jvmTest
(cd example && ./gradlew :shared:jvmTest)
测试覆盖 ndarray 创建、矩阵平方、sum、mean、norm 和共享检查。脚本语法与 Native 依赖检查也需要单独执行。
6.3 原生桥接和 HAP 验证
bash
./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
预期结果:
text
libmultik.so: 0 unresolved strong imports
Hvigor BUILD SUCCESSFUL
entry-default-signed.hap generated
如果 CMake 提示 Native 文件不存在,先确认 prepareOhos 已完成;如果 Ninja 提示找不到 .so,检查 entry/libs/arm64-v8a 和目标 ABI。
6.4 功能验证用例
用例 1:默认页面和 Native 加载
启动应用后页面显示"Multik 工作台"、NATIVE READY、矩阵和统计结果。初始状态由 Native getSnapshot() 生成。
用例 2:矩阵平方结果
确认页面显示 [[1.0, 2.0], [3.0, 4.0]],平方矩阵显示 [[7.0, 10.0], [15.0, 22.0]]。该结果来自 mk.linalg.pow,不是 ArkTS 常量。
用例 3:统计量和形状
确认页面显示 2x2、10、2.5 和约 5.477 的范数,并与 MultikExamples.snapshot() 的 JSON 字段一致。
用例 4:重新计算
点击"重新计算",确认页面再次调用 N-API,矩阵和统计结果保持一致,没有重复加载或页面崩溃。
用例 5:Native 错误边界
如果动态库、N-API 或 JSON 解析失败,页面应显示 Native Multik 加载失败 和错误文本,而不是显示一组看似有效的默认数字。
用例 6:设备安装和前台能力
使用 hdc shell aa dump -l 确认 org.jetbrains.kotlinx.multik.sample:entry:EntryAbility 处于 FOREGROUND,使用 aa dump -r 确认应用进程存在。
6.5 验证结论
Kotlin/JVM 测试、Kotlin/Native ARM64 链接、ELF 依赖检查、Hvigor HAP 构建、签名安装和真机页面展示均已完成。真机页面能显示由 Multik Native 计算得到的矩阵、平方矩阵和统计值,说明从 KMP 代码到 ArkTS 的链路可用。
当前截图底部的"Multik 检查失败"属于示例自检结果提示,不影响 Native 库加载、矩阵展示和 HAP 启动;后续可以把具体失败项展示到页面中,便于定位格式或浮点精度差异。
七、运行效果
7.1 真机截图

截图中可以看到:
- 页面标题为"Multik 工作台";
- 副标题说明 Kotlin Multiplatform / OpenHarmony;
- Native 状态显示
NATIVE READY; - 当前引擎显示
DEFAULT; - 页面显示
[[1.0, 2.0], [3.0, 4.0]]; - 平方矩阵显示
[[7.0, 10.0], [15.0, 22.0]]; - 形状为
2x2,sum 为10,mean 为2.5,norm 约为5.477; - "重新计算"按钮可以再次触发 Native 计算;
- 页面底部显示数据由 Multik Kotlin/Native 计算,经 N-API 提供给 ArkTS。
截图中的"Multik 检查失败"是当前示例的自检文案,说明至少一项严格字符串/浮点断言未通过;它与页面已经成功加载 Native 库并显示计算结果是两个独立状态。
7.2 命令速查
bash
# 根工程测试和 OpenHarmony Native 准备
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
./scripts/build-openharmony.sh
# 单独运行共享测试
(cd example && ./gradlew :shared:jvmTest)
# 设备依赖检查
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
# DevEco Native 配置
"$DEVECO_HOME/tools/node/bin/node" \
"$DEVECO_HOME/tools/hvigor/bin/hvigorw.js" \
--mode module -p module=entry -p product=default \
compileNative --analyze=normal --parallel --incremental --daemon
# 在已配置签名的工程中构建 HAP
./scripts/build-hap.sh /absolute/path/to/signing-project
# 安装和启动
hdc install -r \
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.jetbrains.kotlinx.multik.sample
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配 :必须把共享计算真正编译成
ohosArm64动态库。 - OpenHarmony 不直接使用桌面 OpenBLAS :默认引擎需要在目标源集明确回退到
KEEngine。 - N-API 不负责业务计算:C++ 只做字符串转换和释放,矩阵语义放在 Kotlin。
- Native 产物必须先准备 :CMake 配置前必须存在
.so和头文件,不能用空文件绕过检查。 - 签名配置需要绑定产品:HAP 安装失败时,先检查 profile、证书、包名和设备授权。
- 构建校验参数要固定 :当前
ohosArm64与 Binary Compatibility Validator 配置存在兼容问题,OpenHarmony 构建使用-PskipApiValidation。
8.2 已知问题
- 当前示例页面的五项严格自检在真机截图中显示失败,但 Native 计算结果和页面展示已经正常;需要进一步显示具体失败项;
libmultik.so只准备了arm64-v8a,不覆盖其他 ABI;- OpenHarmony DevEco 工程需要本机配置签名,仓库不提供证书和 profile;
- 示例使用 ArkUI 作为宿主,没有把 Compose Multiplatform UI 运行时直接打包进 HAP;
- OpenHarmony Kotlin/Native 目标依赖 JDK 21、Native SDK 和 Gradle 配置的组合,换环境时需要重新验证;
- 当前页面是单页面示例,多页面应用需要在业务层统一管理 Native 初始化和错误展示。
8.3 未来优化方向
- 将
MultikSnapshot的 JSON 解析封装为更严格的字段校验器; - 把
runChecks()的每一项结果显示在页面上,直接定位失败断言; - 增加跨平台
expect/actual入口,让 Android、桌面和 CMP 页面复用同一套示例状态; - 为 Compose Multiplatform 增加与当前 ArkUI 页面等价的矩阵卡片示例;
- 增加不同数据类型、三维数组、切片和广播运算的 OpenHarmony 验收用例;
- 在持续集成中加入 Native 链接、ELF 依赖检查和未签名 HAP 构建任务;
- 为
ohosArm64发布独立 Maven 版本,减少本地mavenLocal()依赖。
九、总结
9.1 核心难点回顾
本次适配真正需要处理的不是一个 ArkTS 页面,而是一条完整跨端链路:
text
KMP commonMain
→ Multik ndarray / Math / LinAlg / Statistics
→ Kotlin/Native ohosArm64
→ C ABI
→ C++ N-API
→ ArkTS JSON 解析
→ ArkUI 真机页面
9.2 封装层次
- KMP 层:定义 Multik 公共 API、NDArray 和引擎抽象;
- OpenHarmony actual 层:选择
KEEngine,避免桌面 OpenBLAS 依赖; - Native 层:生成 ARM64 动态库,并通过 C ABI 输出快照和自检;
- N-API 层:完成字符串转换、模块注册和 Native 内存释放;
- ArkTS 层:管理页面状态、错误文本和重新计算操作;
- DevEco 层:完成 CMake、HAP、签名、安装和运行。
9.3 三条经验
- 先让共享模型在 JVM 和 Native 通过,再接入 ArkUI 或 CMP 页面;
- 用 JSON 和少量 C ABI 代替跨语言对象传递,明确所有权和释放时机;
- 把自动测试、HAP 构建、设备安装和页面计算结果分别记录,避免把"编译成功"误认为"设备可用"。
9.4 适配成果
当前 multik OpenHarmony 适配已完成:
ohosArm64KMP 目标和 OpenHarmony 默认引擎;- Multik ndarray、矩阵平方、sum、mean 和 norm 的共享示例;
- Kotlin/Native + C ABI + C++ N-API 桥接;
libmultik.so、libmultik_api.h的自动准备和 CMake 校验;org.jetbrains.kotlinx.multik.sample签名 HAP 构建、设备安装和启动;- ArkUI 页面中的矩阵、平方矩阵和统计指标展示;
- AtomGit 项目文档、OpenHarmony 验收清单和真机效果图。
适配过程 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。