本文记录 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,在一个结果分支中处理所有情况:

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 动态库。

二、实现路线图

第 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 工程一致的分层方式:

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 Multiplatform2.2.21-1.0.0JVM、Kotlin/Native 和 KLIB
JDK21Gradle、Kotlin 编译和 Native 任务
OpenHarmony 目标ohosArm64ARM64 真机动态库
kotlinx-coroutines1.10.2-1.0.0挂起函数、重试和测试
Okio3.16.4-1.0.0Native 可用的 IO 异常类型
ABIarm64-v8aHAP 原生库架构
适配版本2.1.0-ohos.1示例和本地发布版本

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

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 示例目录

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

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

目录点击只读取确定性的共享场景,不访问网络。重试按钮运行共享的挂起重试逻辑,
因此页面可以在没有后端服务的情况下验证 EitherNet 的结果分支和重试规则。

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

2.1 加入 ohosArm64 目标

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

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 {
  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 符号:

ApiCatalog
ApiGet
ApiRetry
ApiRunChecks
ApiFree

限制导出符号可以避免 Kotlin/Native 内部实现变成不受控 ABI,也让 C++ 桥接层的
职责和内存所有权更容易审查。

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

根工程通过版本目录统一 Kotlin、coroutines 和 Okio 版本,example 通过项目
依赖消费共享模块:

sourceSets {
  commonMain.dependencies {
    implementation(project(":shared"))
  }
}

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

eithernetVersion=2.1.0-ohos.1
coroutinesVersion=1.10.2-1.0.0

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

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

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

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"))
}

产物路径固定为:

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:

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 中定义五个确定性场景:

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"):

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

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

{
  "attempts": 3,
  "type": "Success",
  "summary": "attempts=3, Success(value=Po)"
}

场景 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 对象。

最终采用三层桥接:

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 只导出目录、读取、重试、检查和释放函数:

@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 方法:

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:

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 生命周期
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 边界完成
类型解析:

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 保存当前场景、目录、重试结果、自检文本和错误文本:

@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 工程结构
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 类型
声明位于:

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

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

import eithernetNative from 'libentry.so';
6.3 Native 动态库准备

执行:

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

脚本依次执行根模块 JVM 检查、本地发布、示例共享测试、
linkDebugSharedOhosArm64 和 prepareOhos。成功后生成:

example/ohosApp/entry/libs/arm64-v8a/libeithernet.so
example/ohosApp/entry/src/main/cpp/include/libeithernet_api.h

还可以检查 ARM64 ELF 的强依赖:

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,或使用:

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

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

python3 scripts/prepare-signing-project.py /absolute/path/eithernet-signing

构建脚本会在签名边界提示确认配置。证书、profile、p12、别名和密码只保存在
外部目录,不提交到 AtomGit。

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

example/ohosApp/entry/build/default/outputs/default/entry-default-unsigned.hap

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

四、完整代码对照

4.1 整体架构

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/commonMainEitherNet 公共 API、结果类型和扩展函数。
eithernet/build.gradle.ktsohosArm64 目标和 Native source set。
example/shared/.../EitherNetExamples.kt五类结果、重试和十项公共检查。
example/shared/.../EitherNetExamplesTest.ktJVM 共享行为测试。
example/nativeApp/.../NativeBridge.ktC ABI、JSON 返回和内存释放。
example/nativeApp/.../shared-library.map限制 Native 导出符号。
example/ohosApp/.../napi_init.cppN-API 方法注册、参数检查和释放。
example/ohosApp/.../EitherNetClient.etsJSON 解析和 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作用
KotlinApiResult.success创建成功结果。
KotlinApiResult.httpFailure创建带状态码的 HTTP 失败。
KotlinApiResult.networkFailure创建网络失败。
KotlinretryWithExponentialBackoff按指数退避执行重试。
NativeApiCatalog返回五类场景目录 JSON。
NativeApiGet返回指定场景 JSON。
NativeApiRetry返回重试摘要 JSON。
NativeApiRunChecks返回公共检查结果 JSON。
NativeApiFree释放 Native 返回缓冲区。
N-APIgetCatalog / getApi向 ArkTS 暴露目录和读取方法。
ArkTSretry / runChecks触发重试和共享检查。

4.4 ArkTS 与 Kotlin 的边界

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

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

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 静态检查与单元测试

./gradlew :eithernet:jvmTest
./gradlew :eithernet:compileKotlinOhosArm64
./gradlew :eithernet:compileTestKotlinOhosArm64
(cd example && ./gradlew :shared:jvmTest)

测试覆盖五类结果、HTTP 状态码、错误体、Okio 异常、未知异常、ApiException、
第三次重试、回退值和非法下标。示例共享检查返回十项结果,页面显示 10/10。

6.3 原生桥接和 HAP 验证

(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

已验证的关键结果是:

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 命令速查

# 根工程测试、发布本地库和 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 页面,而是一条完整跨端链路:

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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐