开源鸿蒙平台 KMP_CMP 三方库「Multik」适配全流程

本文记录 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配 :必须把共享计算真正编译成 ohosArm64 动态库。
  2. OpenHarmony 不直接使用桌面 OpenBLAS :默认引擎需要在目标源集明确回退到 KEEngine。
  3. N-API 不负责业务计算:C++ 只做字符串转换和释放,矩阵语义放在 Kotlin。
  4. Native 产物必须先准备 :CMake 配置前必须存在 .so 和头文件,不能用空文件绕过检查。
  5. 签名配置需要绑定产品:HAP 安装失败时,先检查 profile、证书、包名和设备授权。
  6. 构建校验参数要固定 :当前 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 三条经验

  1. 先让共享模型在 JVM 和 Native 通过,再接入 ArkUI 或 CMP 页面;
  2. 用 JSON 和少量 C ABI 代替跨语言对象传递,明确所有权和释放时机;
  3. 把自动测试、HAP 构建、设备安装和页面计算结果分别记录,避免把"编译成功"误认为"设备可用"。

9.4 适配成果

当前 multik OpenHarmony 适配已完成:

  • ohosArm64 KMP 目标和 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。

参考文档

相关推荐
miofly1 小时前
GitHub 日榜趋势速报 | 2026-10-26
开源·github
冬奇Lab11 小时前
一天一个开源项目(第247篇):wxa-runner —— 让小程序在普通浏览器里用微信官方基础库跑起来,直接接 Playwright 做无头 E2E
微信小程序·开源
supabc12312 小时前
欧盟电子发票(XRechnung/Peppol/ZUGFeRD)踩坑记:用 Python 不装 Java 跑官方校验规则
xml·python·开源·电子发票·1024程序员节
行者全栈架构师14 小时前
【鸿蒙心迹】让老照片变清晰:HarmonyOS 7.0 图像超分相册工程化落地与 5 个踩坑解法
架构·前端框架·开源
特立独行的猫A15 小时前
10 MB 的桌面 AI 客户端:用仓颉 + 系统 WebView 替代 Electron
前端·harmonyos
miofly16 小时前
Kling 4.0 上线 API 调用,支持 30 秒原生音频视频生成
开源·github
DisonTangor16 小时前
FLUX 3 Image Klein 中文全解:5.6B 开源文生图与图像编辑模型实战指南
人工智能·ai作画·开源·aigc·1024程序员节
miofly16 小时前
山东大学研究:代码智能体的复杂 harness 主要提升 token 消耗而非性能
开源·github
2601_9656196917 小时前
没时间给开源项目做宣传?AI自动化生成整套物料
人工智能·开源·自动化