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

本文记录 EitherNet 接入 OpenHarmony 的完整过程,覆盖 KMP/CMP 工程盘点、

ohosArm64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkTS 示例页面、

结果类型验证、HAP 构建、签名边界和效果验证。

EitherNet 的核心职责是把网络请求结果建模为 ApiResult 密封类型。适配后的

示例没有在 ArkTS 中重新实现一套结果判断,而是让 JVM 测试和 OpenHarmony 页面

共同消费同一份 Kotlin 共享逻辑,再通过 JSON 把稳定结果交给 ArkTS。

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

上游仓库: square/retrofit

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

EitherNet 原本是 Kotlin Multiplatform 网络结果建模库,公共 API 位于

commonMain,Retrofit 集成主要面向 JVM。要让同一份结果模型进入 OpenHarmony,

不能只把示例页面重写成 ArkTS,还需要让 Kotlin 代码真正编译为 OpenHarmony

ARM64 Native 产物,并建立可维护的跨语言边界。

如果只在页面里写几个成功和失败文本,无法证明 ApiResult 的 fold、错误类型、

回退值和重试逻辑仍然由共享代码负责。本次适配需要解决下面几个问题:

障碍 具体问题
目标缺失 KMP 模块默认只有 JVM 和 Apple Native,需要增加 ohosArm64()。
工具链不一致 Kotlin/Native、OpenHarmony Native SDK、Gradle 和 JDK 必须匹配。
依赖边界 kotlinx-coroutines 和 Okio 的 OpenHarmony 产物必须能被 Native 目标解析。
语言边界不同 ArkTS 不能直接持有 Kotlin ApiResult 对象,需要 C ABI、C++ N-API 和 JSON。
内存生命周期 Kotlin/Native 返回的字符串必须由明确的 ApiFree 释放。
异步语义 重试 API 是挂起函数,Native 桥接需要在边界内安全调用 runBlocking。
交付链路复杂 Native 动态库、CMake、HAP、签名和 ARM64 依赖需要分别验证。

因此,本项目把适配边界放在三个地方:Kotlin/Native 目标配置、C ABI/N-API

桥接层和 ArkTS 页面层。ApiResult 的状态语义仍由 KMP 维护,ArkTS 只负责解析

JSON、展示场景和触发示例操作。

1.2 库提供的能力

EitherNet 的公共类型是 ApiResult<out T, out E>,它用密封类型表达一次请求的

完整结果:

  • ApiResult.Success<T>:请求成功,response 保存业务数据;
  • ApiResult.Failure.NetworkFailure:网络连接、超时等传输层异常;
  • ApiResult.Failure.HttpFailure:HTTP 状态码失败,并保留状态码和错误值;
  • ApiResult.Failure.ApiFailure:业务转换器解码出的接口错误;
  • ApiResult.Failure.UnknownFailure:无法归类的原始异常。

调用方可以使用穷举 when,在一个结果分支中处理所有情况:

kotlin 复制代码
when (val result = service.load()) {
  is ApiResult.Success -> render(result.response)
  is ApiResult.Failure.NetworkFailure -> showOffline(result.error)
  is ApiResult.Failure.HttpFailure -> showHttpError(result.code)
  is ApiResult.Failure.ApiFailure -> showApiError(result.error)
  is ApiResult.Failure.UnknownFailure -> showUnknown(result.error)
}

本次 OpenHarmony 示例覆盖以下实际 API:

EitherNet 能力 示例行为
ApiResult.success 构造 Success(value=Po) 场景。
ApiResult.httpFailure 构造 HTTP 503 场景并展示状态码。
ApiResult.apiFailure 保留 quota exceeded 业务错误。
ApiResult.networkFailure 使用 Okio IOException 表示连接超时。
ApiResult.unknownFailure 保留 IllegalStateException 未知错误。
fold / successOrElse 统一生成摘要和失败回退值。
retryWithExponentialBackoff 前两次失败,第三次恢复成功。

1.3 实现适配

维度 要求
代码复用 ApiResult、fold、失败分类、回退和重试由 Kotlin 共享。
平台目标 为核心库和示例加入 ohosArm64,生成 libeithernet.so。
桥接稳定 使用少量 C ABI 函数和 UTF-8 JSON,不暴露 Kotlin 对象地址。
UI 完整 ArkTS 页面支持目录读取、场景切换、重试和公共检查。
可测试 JVM 测试、Native 编译、ELF 依赖检查和 HAP 构建分别验收。
签名安全 源码只保留未签名工程,证书和密钥材料由开发者在外部目录配置。
仓库规范 项目说明、文章、效果图和源码链接统一使用 AtomGit。

example/ohosApp 是验证 KMP 共享结果模型的 DevEco Stage 宿主。它没有把

EitherNet 的业务判断复制到 ArkTS,页面展示的五类结果、重试摘要和 10/10

检查结果都来自 Kotlin/Native 动态库。

二、实现路线图

text 复制代码
第 1 阶段:项目初始化     ── 盘点 ApiResult 公共 API、版本和示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、独立 example 和 Native 构建任务
第 3 阶段:结果与序列化   ── 建立五类结果场景、摘要、回退和 JSON 契约
第 4 阶段:原生桥接       ── Kotlin/Native C ABI、C++ N-API、类型检查和释放
第 5 阶段:ArkTS 宿主封装 ── JSON 解析、页面状态、重试按钮和错误展示
第 6 阶段:示例与验证     ── Native 依赖检查、HAP 构建、签名交接和效果图

每个阶段都使用真实产物作为下一阶段输入:先用 JVM 验证共享结果模型,再把相同

代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,最后构建

未签名 HAP 并保留外置签名入口。

三、逐步实现过程

第 1 阶段:项目初始化

1.1 盘点公共 API 和工程边界

项目采用与参考 uuid 工程一致的分层方式:

text 复制代码
eithernet/                   KMP 核心库、ApiResult 和 ohosArm64 目标
example/shared/              平台无关的 EitherNet 场景和 JVM 验收测试
example/nativeApp/           ohosArm64 Kotlin/Native 动态库
example/ohosApp/             DevEco Stage 工程、C++ N-API 和 ArkTS 页面
scripts/                     Native 构建、依赖检查和签名交接脚本
docs/openharmony/            验收清单和效果图

example 是独立 Gradle 工程,不把 DevEco 工程当作 Kotlin 子模块。这样可以

分别执行 Gradle 和 Hvigor,也可以把 ohosApp 复制到其他目录后在 DevEco Studio

中配置签名,而不污染源码工程。

1.2 固定工具链和版本矩阵

本项目使用以下版本约定:

项目 配置 用途
Kotlin Multiplatform 2.2.21-1.0.0 JVM、Kotlin/Native 和 KLIB
JDK 21 Gradle、Kotlin 编译和 Native 任务
OpenHarmony 目标 ohosArm64 ARM64 真机动态库
kotlinx-coroutines 1.10.2-1.0.0 挂起函数、重试和测试
Okio 3.16.4-1.0.0 Native 可用的 IO 异常类型
ABI arm64-v8a HAP 原生库架构
适配版本 2.1.0-ohos.1 示例和本地发布版本

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

bash 复制代码
export JAVA_HOME="/path/to/jdk-21"
export PATH="$JAVA_HOME/bin:$PATH"
java -version

即使 DevEco Studio 使用较新的 JBR,也要让 Gradle 使用 JDK 21。JBR Java 25

可能让 Kotlin 编译器在解析版本时报告 IllegalArgumentException: 25.0.2。

1.3 创建 OpenHarmony 示例目录

示例页面围绕五类结果、当前选中项、重试结果和共享检查组织:

text 复制代码
标题区          EitherNet 工作台 / Kotlin Multiplatform + OpenHarmony
结果区          当前 API 结果、请求、摘要和 HTTP 状态码
目录区          Success、HttpFailure、ApiFailure、NetworkFailure、UnknownFailure
操作区          重新读取 / 运行重试
自检区          10/10 公共检查通过

目录点击只读取确定性的共享场景,不访问网络。重试按钮运行共享的挂起重试逻辑,

因此页面可以在没有后端服务的情况下验证 EitherNet 的结果分支和重试规则。

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

2.1 加入 ohosArm64 目标

核心模块保留 JVM 和 Apple Native 目标,并增加 OpenHarmony ARM64 目标:

kotlin 复制代码
kotlin {
  jvm()
  iosX64()
  iosArm64()
  iosSimulatorArm64()
  ohosArm64()

  sourceSets {
    val nativeMain = create("nativeMain") { dependsOn(getByName("commonMain")) }
    val nativeTest = create("nativeTest") { dependsOn(getByName("commonTest")) }
    getByName("ohosArm64Main") { dependsOn(nativeMain) }
    getByName("ohosArm64Test") { dependsOn(nativeTest) }
  }
}

显式 nativeMain 层级让 OpenHarmony 复用与 Apple Native 相同的实现,同时避免

把只适用于 JVM 的 Retrofit 测试依赖带入 Native 编译。

2.2 Native focused build 的作用

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

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

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

text 复制代码
ApiCatalog
ApiGet
ApiRetry
ApiRunChecks
ApiFree

限制导出符号可以避免 Kotlin/Native 内部实现变成不受控 ABI,也让 C++ 桥接层的

职责和内存所有权更容易审查。

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

根工程通过版本目录统一 Kotlin、coroutines 和 Okio 版本,example 通过项目

依赖消费共享模块:

kotlin 复制代码
sourceSets {
  commonMain.dependencies {
    implementation(project(":shared"))
  }
}

示例本身仍然是独立 Gradle 工程,并将版本写在 example/gradle.properties:

properties 复制代码
eithernetVersion=2.1.0-ohos.1
coroutinesVersion=1.10.2-1.0.0

这样可以单独运行 example 测试,也可以把同一套共享代码作为独立消费者进行

验证。

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

prepareOhos 在 Native 链接成功后复制动态库和生成的 C 头文件:

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

产物路径固定为:

text 复制代码
example/ohosApp/entry/libs/arm64-v8a/libeithernet.so
example/ohosApp/entry/src/main/cpp/include/libeithernet_api.h

动态库和生成头文件属于本地构建产物,源码仓库通过 .gitignore 排除它们,

每台开发机都可以根据自己的 SDK 重新生成。

第 3 阶段:结果与序列化

3.1 为什么需要统一 JSON 契约

ArkTS、C++ 和 Kotlin/Native 不能直接共享 Kotlin 对象,因此边界使用 UTF-8 JSON:

text 复制代码
EitherNetExamples
        ↓ ApiResult<String, String>
NativeBridge.kt
        ↓ NUL 结尾 UTF-8 JSON
N-API napi_create_string_utf8
        ↓ JavaScript string
EitherNetClient.ets
        ↓ JSON.parse
ArkUI 页面状态

页面只消费 JSON,不重新判断 ApiResult 的 Kotlin 类型。JVM 测试和设备页面因而

使用同一套结果场景、摘要和回退文本。

3.2 五类结果场景

共享示例在 EitherNetExamples 中定义五个确定性场景:

kotlin 复制代码
private val scenarios = listOf(
  Scenario("success", "GET /pandas", ApiResult.success("Po")),
  Scenario("http-failure", "GET /pandas/503", ApiResult.httpFailure(503, "maintenance")),
  Scenario("api-failure", "GET /pandas/quotas", ApiResult.apiFailure("quota exceeded")),
  Scenario(
    "network-failure",
    "GET /pandas/offline",
    ApiResult.networkFailure(IOException("connect timeout")),
  ),
  Scenario(
    "unknown-failure",
    "GET /pandas/broken",
    ApiResult.unknownFailure(IllegalStateException("unexpected json")),
  ),
)

每个场景被渲染为 ApiScenario,包含 id、请求路径、结果类型、HTTP 状态码、

摘要和 successOrElse 生成的回退文本。这样页面能展示完整结果,而不是只显示

一个成功或失败布尔值。

3.3 重试和 JSON 字段

重试示例使用 retryWithExponentialBackoff,前两次返回网络失败,第三次返回

Success("Po"):

kotlin 复制代码
val result = retryWithExponentialBackoff<String, String>(
  maxAttempts = 5,
  initialDelay = 1.milliseconds,
  delayFactor = 2.0,
  maxDelay = 4.milliseconds,
  jitterFactor = 0.0,
) {
  transport.call()
}

返回给 ArkTS 的重试 JSON 形状为:

json 复制代码
{
  "attempts": 3,
  "type": "Success",
  "summary": "attempts=3, Success(value=Po)"
}

场景 JSON 形状为:

json 复制代码
{
  "id": "success",
  "request": "GET /pandas",
  "type": "Success",
  "code": 0,
  "summary": "Success(value=Po)",
  "recovered": "Po"
}

Native 桥接中的字符串会进行 JSON 转义,避免错误信息中的引号、反斜杠或换行

破坏跨语言边界。

3.4 自检和错误边界

EitherNetExamples.runChecks() 覆盖十项检查:

  1. 五种确定性场景都存在;
  2. Success 可以通过 fold 得到值;
  3. HTTP 失败暴露 503 状态码;
  4. API 失败保留解码后的错误体;
  5. 网络失败保留 Okio IOException;
  6. 未知失败保留原始 IllegalStateException;
  7. ApiException 携带业务错误;
  8. 重试在第三次尝试恢复;
  9. successOrElse 返回网络失败的回退值;
  10. 非法场景下标会抛出 IllegalArgumentException。

Native 异常会转换为 {"error":"..."}。C++ 创建 ArkTS 字符串后立即调用

ApiFree,页面能够显示可读错误,同时不会泄漏 Kotlin/Native 堆内存。

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

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

Kotlin/Native 的 ApiResult、ApiScenario 和 RetryRun 都属于 Kotlin 运行时

对象。ArkTS 只能接收 JavaScript 值,C++ 也不能把 Kotlin 对象地址直接当成

JavaScript 对象。

最终采用三层桥接:

text 复制代码
ArkTS
  │ JSON string
  ▼
C++ N-API entry
  │ const char*
  ▼
Kotlin/Native C ABI
  │ EitherNetExamples
  ▼
UTF-8 JSON + explicit free
4.2 方案对比
方案 优点 缺点 选用
直接导出 Kotlin 对象 代码少 ABI、生命周期和类型不可控 ❌
只导出结果整数 实现简单 页面会重新实现结果映射 ❌
C ABI + JSON 边界清晰、易扩展、易调试 有一次序列化开销 ✅
在 ArkTS 重写完整模型 页面调用简单 KMP 和 ArkTS 逻辑容易分叉 ❌
4.3 Kotlin/Native 导出函数

NativeBridge.kt 只导出目录、读取、重试、检查和释放函数:

kotlin 复制代码
@CName("ApiCatalog")
public fun catalogNative(): CPointer<ByteVar> = response {
  EitherNetExamples.catalog().joinToString(prefix = "[", postfix = "]") { it.toJson() }
}

@CName("ApiGet")
public fun apiNative(index: Int): CPointer<ByteVar> =
  response { EitherNetExamples.get(index).toJson() }

@CName("ApiRetry")
public fun retryNative(): CPointer<ByteVar> = response {
  runBlocking { EitherNetExamples.retry() }.toJson()
}

@CName("ApiRunChecks")
public fun checksNative(): CPointer<ByteVar> = response {
  val checks = runBlocking { EitherNetExamples.runChecks() }
  "{\"passed\":true,\"checks\":[${checks.joinToString { quote(it) }}]}"
}

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

返回值使用 Native heap 分配的、以 0 结尾的 C 字符串。每一块返回缓冲区都由

调用方在复制成 ArkTS 字符串后调用 ApiFree 释放。

4.4 C++ N-API 方法分发

C++ 注册四个 ArkTS 方法:

cpp 复制代码
napi_property_descriptor methods[] = {
    {"getCatalog", nullptr, Catalog, nullptr, nullptr, nullptr,
        napi_default, nullptr},
    {"getApi", nullptr, GetApi, nullptr, nullptr, nullptr,
        napi_default, nullptr},
    {"retry", nullptr, Retry, nullptr, nullptr, nullptr,
        napi_default, nullptr},
    {"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
        napi_default, nullptr},
};

getApi 会检查参数数量、有限数值、整数性和非负范围,再调用 ApiGet。所有

方法都遵循"调用 Native、创建 ArkTS 字符串、释放 Native 缓冲区"的顺序。

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

cmake 复制代码
add_library(eithernet SHARED IMPORTED)
set_target_properties(eithernet PROPERTIES
  IMPORTED_LOCATION
  "${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libeithernet.so")

add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE eithernet libace_napi.z.so)
4.5 N-API 生命周期
text 复制代码
ArkTS getApi(index)
        │
        ▼
ReadIndex + argument check
        │
        ▼
ApiGet(index)
        │
        ▼
napi_create_string_utf8(...)
        │
        ▼
ApiFree(nativeBuffer)
        │
        ▼
return JS string

C++ 负责完成跨语言字符串转换和 Native 释放,ArkTS 页面不需要知道

Kotlin/Native 的堆实现。

第 5 阶段:ArkTS 宿主封装

5.1 ArkTS 调用 N-API

EitherNetClient.ets 从 libentry.so 导入 Native 模块,并在 JSON 边界完成

类型解析:

typescript 复制代码
import eithernetNative from 'libentry.so';

export function catalog(): ApiScenario[] {
  return JSON.parse(eithernetNative.getCatalog()) as ApiScenario[];
}

export function getApi(index: number): ApiScenario {
  return JSON.parse(eithernetNative.getApi(index)) as ApiScenario;
}

export function retry(): RetryRun {
  return JSON.parse(eithernetNative.retry()) as RetryRun;
}

ArkTS 接收到的是普通 JavaScript 对象,页面无需处理 C 指针或 Native 释放。

5.2 ArkUI 页面状态

Index.ets 保存当前场景、目录、重试结果、自检文本和错误文本:

typescript 复制代码
@State private selected: ApiScenario = emptyScenario();
@State private scenarios: ApiScenario[] = [];
@State private status: string = '正在加载';
@State private errorText: string = '';
@State private retryText: string = '尚未执行重试';
@State private checkText: string = '尚未运行公共检查';

页面出现时调用 catalog() 和 runChecks(),目录行点击 getApi(index),重试

按钮调用 retry()。这些操作都在同一个 Native 结果链路中完成。

5.3 页面交互预设

页面提供三类操作:

  • 重新读取:重新获取五类场景并选择第一项;
  • 目录点击 :查看 Success、HttpFailure、ApiFailure、
    NetworkFailure 或 UnknownFailure;
  • 运行重试:执行挂起的指数退避示例,预期第三次尝试成功。

这里没有额外的系统权限和传感器订阅,页面功能完全用于验证 EitherNet 的共享

结果模型、Native ABI 和 N-API 传输。

第 6 阶段:示例与验证

6.1 example 工程结构
text 复制代码
example/
├── shared/
│   ├── src/commonMain/.../EitherNetExamples.kt
│   └── src/commonTest/.../EitherNetExamplesTest.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
        └── eithernet/EitherNetClient.ets

shared 验证公共结果模型,nativeApp 产生 Native 动态库,ohosApp 负责

N-API 注册和 ArkUI 页面。三者的边界清晰,任何一层失败都能单独定位。

6.2 原生模块注册

napi_init.cpp 通过 napi_module_register 注册 entry 模块,TypeScript 类型

声明位于:

text 复制代码
example/ohosApp/entry/src/main/cpp/types/libentry/index.d.ts

ArkTS 通过下面的模块名访问 C++ 导出的函数:

typescript 复制代码
import eithernetNative from 'libentry.so';
6.3 Native 动态库准备

执行:

bash 复制代码
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh

脚本依次执行根模块 JVM 检查、本地发布、示例共享测试、

linkDebugSharedOhosArm64 和 prepareOhos。成功后生成:

text 复制代码
example/ohosApp/entry/libs/arm64-v8a/libeithernet.so
example/ohosApp/entry/src/main/cpp/include/libeithernet_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
6.4 构建、签名和安装

未配置签名时,可以在 DevEco Studio 构建未签名 HAP,或使用:

bash 复制代码
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
  scripts/build-hap.sh /absolute/path/to/signing-project

需要签名时,先在源码目录之外准备工程:

bash 复制代码
python3 scripts/prepare-signing-project.py /absolute/path/eithernet-signing

构建脚本会在签名边界提示确认配置。证书、profile、p12、别名和密码只保存在

外部目录,不提交到 AtomGit。

当前源码工程已验证未签名 HAP 产物:

text 复制代码
example/ohosApp/entry/build/default/outputs/default/entry-default-unsigned.hap

设备安装需要针对设备配置签名。本文不把未签名构建写成已完成的签名真机验收。

四、完整代码对照

4.1 整体架构

text 复制代码
EitherNet ApiResult
    │ commonMain / nativeMain
    ▼
EitherNetExamples
    │ Kotlin/Native
    ▼
ApiCatalog / ApiGet / ApiRetry / ApiRunChecks
    │ C ABI + UTF-8 JSON
    ▼
libeithernet.so
    │ C++ N-API
    ▼
libentry.so -> ArkTS -> ArkUI 页面

4.2 文件清单

文件 职责
eithernet/src/commonMain EitherNet 公共 API、结果类型和扩展函数。
eithernet/build.gradle.kts ohosArm64 目标和 Native source set。
example/shared/.../EitherNetExamples.kt 五类结果、重试和十项公共检查。
example/shared/.../EitherNetExamplesTest.kt JVM 共享行为测试。
example/nativeApp/.../NativeBridge.kt C ABI、JSON 返回和内存释放。
example/nativeApp/.../shared-library.map 限制 Native 导出符号。
example/ohosApp/.../napi_init.cpp N-API 方法注册、参数检查和释放。
example/ohosApp/.../EitherNetClient.ets JSON 解析和 ArkTS 类型门面。
example/ohosApp/.../Index.ets 结果目录、重试和检查页面。
scripts/build-openharmony.sh 测试、Native 链接和产物复制。
scripts/check-native-deps.py 检查 ARM64 ELF 强依赖。
scripts/build-hap.sh 外部签名工程的 HAP 构建。
docs/openharmony/VALIDATION.md 自动检查和签名边界说明。

4.3 关键 API 对照

层次 API 作用
Kotlin ApiResult.success 创建成功结果。
Kotlin ApiResult.httpFailure 创建带状态码的 HTTP 失败。
Kotlin ApiResult.networkFailure 创建网络失败。
Kotlin retryWithExponentialBackoff 按指数退避执行重试。
Native ApiCatalog 返回五类场景目录 JSON。
Native ApiGet 返回指定场景 JSON。
Native ApiRetry 返回重试摘要 JSON。
Native ApiRunChecks 返回公共检查结果 JSON。
Native ApiFree 释放 Native 返回缓冲区。
N-API getCatalog / getApi 向 ArkTS 暴露目录和读取方法。
ArkTS retry / runChecks 触发重试和共享检查。

4.4 ArkTS 与 Kotlin 的边界

ArkTS 只负责调用入口和界面状态:

typescript 复制代码
this.selected = getApi(index);
const result: RetryRun = retry();
this.retryText = `${result.type}: ${result.summary}`;

Kotlin 负责结果含义和失败处理:

kotlin 复制代码
private fun summary(result: ApiResult<String, String>): String =
  result.fold(
    onSuccess = { "Success(value=$it)" },
    onNetworkFailure = { "NetworkFailure(${it.error.message})" },
    onUnknownFailure = { "UnknownFailure(${it.error.message})" },
    onHttpFailure = { "HttpFailure(code=${it.code}, error=${it.error})" },
    onApiFailure = { "ApiFailure(${it.error})" },
  )

两者之间只传输整数下标、UTF-8 JSON 和 JavaScript 基础值,不传输 Kotlin 对象、

ArkTS class 实例或未校验的动态结构。

五、关键决策说明

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

只有真正链接 ohosArm64 动态库,才能证明 EitherNet 的共享 Kotlin 代码可以进入

OpenHarmony 运行时。页面单独运行无法替代这个验证。

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

example 单独解析共享库,ohosApp 单独接收 .so 和头文件,避免根工程编译

通过却无法在 DevEco 中打包。

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

JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,并允许后续增加可信字段,

而不暴露 Kotlin 对象布局或 Native 内存细节。

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

目录、读取、重试、检查和释放已经覆盖示例所需能力;减少 ABI 符号可以降低

Native 生命周期和兼容风险。

决策 5:结果场景使用确定性数据

示例不依赖外部网络服务,HTTP、业务、网络和未知失败都能稳定复现,方便在没有

后端环境或设备网络不稳定时完成 KMP 和 N-API 验收。

决策 6:把库验证和设备验证分开

JVM 测试验证结果规则,Native 链接验证 ABI,ELF 检查验证依赖,Hvigor 验证 HAP。

签名和设备安装属于外部环境步骤,不能用未签名构建替代。

六、测试与验证

6.1 测试环境

本次适配使用以下工具链约定:

  • macOS;
  • JDK 21;
  • Kotlin Multiplatform 2.2.21-1.0.0;
  • OpenHarmony ohosArm64 Native 工具链;
  • DevEco Studio OpenHarmony SDK;
  • ARM64 arm64-v8a HAP 配置。

签名证书、profile、p12 和真实设备序列号不属于源码仓库,也不写入文章。

6.2 静态检查与单元测试

bash 复制代码
./gradlew :eithernet:jvmTest
./gradlew :eithernet:compileKotlinOhosArm64
./gradlew :eithernet:compileTestKotlinOhosArm64
(cd example && ./gradlew :shared:jvmTest)

测试覆盖五类结果、HTTP 状态码、错误体、Okio 异常、未知异常、ApiException、

第三次重试、回退值和非法下标。示例共享检查返回十项结果,页面显示 10/10。

6.3 原生桥接和 HAP 验证

bash 复制代码
(cd example && ./gradlew :nativeApp:prepareOhos)
python3 scripts/check-native-deps.py \
  /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
  example/ohosApp/entry/libs/arm64-v8a

已验证的关键结果是:

text 复制代码
libeithernet.so: 0 unresolved strong imports
entry-default-unsigned.hap generated

DevEco 在没有 signingConfig 时会提示 No signingConfig found。这是当前源码

工程保持未签名的预期提示,不代表 Native 或 HAP 构建失败。

6.4 功能验证用例

用例 1:默认页面和公共检查

启动页面后加载五个 API 结果场景,并显示 10/10 公共检查通过。初始结果由

Native ApiCatalog() 和 ApiGet(0) 生成。

用例 2:五种结果目录

点击 Success、HttpFailure、ApiFailure、NetworkFailure 和 UnknownFailure,页面

分别显示请求路径、类型、摘要、HTTP 状态码和回退信息。

用例 3:运行重试

点击"运行重试",Native 层执行挂起的指数退避逻辑,预期显示

attempts=3, Success(value=Po)。

用例 4:非法下标

通过共享测试验证负数和超出范围的下标会抛出 IllegalArgumentException;C++ 层

同时拒绝非整数、负数和超出 int32 范围的输入。

用例 5:重复刷新和释放

多次点击"重新读取"和目录项,确认每次 Native 返回值都在 C++ 创建 ArkTS 字符串

后调用 ApiFree,页面不会保存裸指针。

6.5 验证结论

当前已完成 JVM 测试、ohosArm64 编译、Native 动态库准备、ARM64 依赖检查和

未签名 HAP 构建。效果图展示了五类结果目录、重试成功和 10/10 共享检查。

签名 HAP 的设备安装需要使用开发者自己的证书和 profile,属于外部签名流程;

源码工程没有把这一步伪装成已完成结果。

七、运行效果

7.1 效果图

效果图中可以看到:

  • 页面标题为"EitherNet 工作台";
  • 副标题说明 Kotlin Multiplatform 和 OpenHarmony;
  • 当前结果为 Success,请求为 GET /pandas;
  • 目录包含 Success、HttpFailure、ApiFailure、NetworkFailure 和 UnknownFailure;
  • 页面显示"已加载 5 个 API 结果场景";
  • 公共检查显示 10/10;
  • 重试摘要显示第三次尝试恢复成功;
  • 页面底部说明数据由 Kotlin/Native 生成,经 N-API 提供给 ArkTS。

7.2 命令速查

bash 复制代码
# 根工程测试、发布本地库和 OpenHarmony Native 准备
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh

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

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

# 复制到外部签名工程
python3 scripts/prepare-signing-project.py /absolute/path/eithernet-signing

# 在已配置签名的工程中构建 HAP
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
  scripts/build-hap.sh /absolute/path/eithernet-signing

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配 :必须把共享代码真正编译成 ohosArm64 动态库。
  2. N-API 不负责业务判断:C++ 只做类型检查、字符串转换和释放,结果语义放在 Kotlin。
  3. 挂起函数不能直接作为 C ABI :ApiRetry 在 Native 边界内使用 runBlocking,再返回同步 JSON。
  4. JSON 字符串必须明确释放 :C++ 复制完成后立即调用 ApiFree,不能把 Native 指针交给 ArkTS。
  5. 签名配置和源码工程分离:没有 signingConfig 时构建未签名 HAP 是预期行为。

8.2 已知问题

  • 当前示例只支持 OpenHarmony ARM64,未生成 x86、x86_64 或 32 位 ARM 产物;
  • ./gradlew :eithernet:check 在缺少本机 iOS Simulator SDK 时会失败,这不影响
    已验证的 JVM、OpenHarmony Native 和 HAP 步骤;
  • 当前源码工程只保留未签名 HAP,设备安装需要外部签名工程;
  • ApiGet 的示例数据是确定性场景,不代表真实后端接口或网络质量;
  • js 和 wasmJs 目标没有纳入本次 OpenHarmony 适配,相关消费者应使用上游实现。

8.3 未来优化方向

  • 增加真实 HTTP 客户端的 OpenHarmony Native 集成示例;
  • 将 JSON 手写编码替换为受控的跨平台序列化模块;
  • 为 Compose Multiplatform 页面补充与 ArkTS 宿主相同的结果卡片示例;
  • 增加 Native 错误码和更细粒度的异常分类;
  • 在持续集成中加入 ohosArm64 链接、ELF 依赖检查和未签名 HAP 构建任务;
  • 在具备外部签名和 ARM64 设备的环境中补充安装、启动和交互验收记录。

九、总结

9.1 核心难点回顾

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

text 复制代码
EitherNet ApiResult
    → KMP common/native 共享代码
    → Kotlin/Native ARM64 动态库
    → C ABI JSON
    → C++ N-API
    → ArkTS JSON 门面
    → ArkUI 结果页面

9.2 封装层次

  • KMP 层:定义 ApiResult、失败分类、fold、回退和重试;
  • Native 层:生成 ARM64 动态库,并通过 C ABI 输出有限入口;
  • N-API 层:完成参数检查、字符串转换和内存释放;
  • ArkTS 层:管理页面状态、目录点击、重试和错误展示;
  • DevEco 层:完成 CMake、HAP、外部签名、安装和运行。

9.3 三条经验

  1. 先让共享结果模型在 JVM 和 Native 通过,再接入 ArkUI;
  2. 用 JSON 和少量 C ABI 代替跨语言对象传递;
  3. 把自动测试、Native 依赖检查、HAP 构建和签名设备验证分别记录,避免把"编译成功"
    误认为"签名安装成功"。

9.4 适配成果

当前 EitherNet OpenHarmony 适配已完成:

  • ApiResult 五类结果在共享代码中的确定性展示;
  • fold、successOrElse 和指数退避重试示例;
  • JVM 与 OpenHarmony ARM64 共用的十项检查;
  • Kotlin/Native + C ABI + C++ N-API 桥接;
  • ApiCatalog、ApiGet、ApiRetry、ApiRunChecks、ApiFree 五个导出入口;
  • DevEco Stage ArkTS 结果工作台和效果图;
  • Native ELF 依赖检查和未签名 HAP 构建;
  • 外部签名工程复制脚本和交互式签名边界;
  • AtomGit 项目文档、中文 README 和 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。

参考文档

相关推荐
十方空烬2 小时前
开源雷达 · 2026-10-26:什么时候该多开一个 Agent,有人写成了阈值
开源·ai编程
小李同学_LHY2 小时前
白话大模型第五章:开源大模型和闭源大模型
开源
程序猿追2 小时前
返回 null 不一定入队成功:鸿蒙 KMP上的并发优先队列观察笔记
笔记·华为·harmonyos·kmp
2501_944525542 小时前
开源鸿蒙平台 KMP_CMP 三方库「kotlinx.immutable」适配全流程
华为·开源·harmonyos
apple_692658853 小时前
MOSS Image 生图全攻略:工作流、特调模型与参数详解
开源·生图
分布式存储与RustFS4 小时前
把 LSM 引擎直接架在对象存储上:slatedb 能不能跑在 RustFS 上?
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
wflynn4 小时前
GitHub 日榜趋势速报 | 2026-10-25
开源·github
文慧的科技江湖4 小时前
2026年10月25日微电网EMS行业早报:标准从舱管到模块,存量电站要自己补课
开源·储能·微电网·能源管理系统·源网荷储·电力安全·新型储能
2501_944525765 小时前
开源鸿蒙平台 KMP_CMP 三方库「Kaml」适配全流程
开源·kotlin·harmonyos