本文记录 Napier 接入 OpenHarmony 的完整过程,覆盖 KMP/CMP 工程盘点、ohosArm64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建、签名和验收。

本次适配复用 Napier 原有的日志级别、tag、异常信息、Antilog 和 Napier 门面,再由 Kotlin/Native 导出少量稳定的 C ABI,经过 C++ N-API 提供给 ArkTS。这样验证的是同一份日志库代码在 OpenHarmony ARM64 设备上的运行结果,而不是重新写一套只在页面里显示文本的示例。

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

开发工具: 华为云码道

一、背景

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

Napier 是 Kotlin Multiplatform 日志库,公共代码可以在多个平台调用 Napier.d、Napier.i、Napier.e 等方法。原项目已经覆盖 Android、Darwin、JVM 和 JavaScript,但默认构建没有 OpenHarmony ohosArm64 目标。

如果只在 ArkUI 页面里使用 console.info,页面能够显示文字,却不能证明 KMP 公共 API、平台 actual 实现、Kotlin/Native 动态库和设备侧调用链已经连通。因此适配需要解决下面几个问题:

障碍具体问题
目标缺失KMP 模块没有 ohosArm64(),无法生成 OpenHarmony KLIB。
平台实现缺失DebugAntilog 和内部 AtomicRef 只有其他平台的 actual 实现。
工具链不一致Kotlin/Native、OpenHarmony LLVM、Native SDK 和 Gradle/Hvigor 必须匹配。
语言边界不同ArkTS 不能直接持有 Kotlin Antilog 对象,必须经过 C ABI、C++ N-API 和 JSON。
日志语义保持日志级别、tag、异常文本和清理行为要与原 Napier API 一致。
交付链路复杂Native 动态库、CMake、HAP、签名、设备安装和 ARM64 依赖需要分别验证。

因此,本项目把适配边界放在三个地方:Kotlin/Native 目标配置、DebugAntilog/原子引用平台实现,以及 C ABI/N-API/ArkTS 示例桥接。Napier 的公共 API 仍然保持平台无关,ArkTS 只负责输入消息、触发检查和展示结果。

1.2 库提供的能力

Napier 原有公共 API 提供以下能力:

  • LogLevel:VERBOSE、DEBUG、INFO、WARNING、ERROR 和 ASSERT;
  • Antilog:可扩展的日志输出抽象;
  • DebugAntilog:各平台默认调试输出实现;
  • Napier.base:注册一个或多个日志输出对象;
  • Napier.takeLogarithm:移除单个输出对象或清空全部输出对象;
  • Napier.v/d/i/w/e/wtf:带 tag、异常和惰性消息的便捷方法;
  • Napier.log 与顶层 log:统一的低级日志入口;
  • isEnable:在构造消息前判断当前级别是否启用。

OpenHarmony DebugAntilog 的默认输出格式为:

<LEVEL> <tag> : <message>

当消息和异常同时存在时,异常堆栈会追加到消息后面;只有异常没有消息时,输出异常堆栈;两个参数都为空时不输出。应用如果需要 HiLog 域、持久化文件、远程上报或特殊过滤,可以实现自己的 Antilog,不需要修改 Napier 公共 API。

1.3 实现适配

维度要求
代码复用日志级别、门面、输出抽象、tag 和异常语义由 Kotlin 共享。
平台目标为 Napier 和示例加入 ohosArm64,生成 libnapier.so。
平台实现在 napier/src/ohosArm64Main 提供 DebugAntilog 和 AtomicRef。
桥接稳定使用少量 C ABI 函数和 JSON,避免把 Kotlin 对象地址交给 ArkTS。
UI 完整页面支持写入一条日志和运行公共检查,展示调用链状态。
可测试JVM 测试、Native 链接、CMake、HAP 构建和设备操作分别验收。
签名安全证书、私钥、profile 和密码只在开发者本机配置,不进入仓库。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面是独立的真机验收宿主,公共 API 仍然保持平台无关。其他 KMP/CMP 应用可以直接复用 Napier 的日志门面,再自行提供 HiLog 或业务日志实现。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点 Napier API、平台实现和 OpenHarmony 示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、独立 example 工程和 Native 构建任务
第 3 阶段:日志语义确认   ── 保持 LogLevel、tag、异常和 Antilog 契约
第 4 阶段:原生桥接       ── Kotlin/Native C ABI、C++ N-API、JSON 和内存释放
第 5 阶段:ArkUI 宿主     ── 页面输入、日志调用、自检展示和生命周期边界
第 6 阶段:示例与验证     ── HAP 构建、签名、设备安装和真实调用链验收

每个阶段都使用真实产物作为下一阶段输入:先用 JVM 验证共享日志 API,再把同一份代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,最后构建 HAP 并在设备上触发日志调用。

三、逐步实现过程

第 1 阶段:项目初始化

1.1 盘点公共 API 和工程边界

项目采用与参考 CMP 工程一致的分层:

Napier/                         原有 KMP 日志库
napier/src/commonMain/          公共 API、Antilog、LogLevel、Napier 门面
napier/src/ohosArm64Main/       OpenHarmony DebugAntilog 和 AtomicRef
example/shared/                 示例门面和 JVM 验收测试
example/nativeApp/              ohosArm64 Kotlin/Native 动态库
example/ohosApp/                DevEco Stage 工程、N-API、CMake、ArkUI
scripts/                        Native、HAP 和签名工程辅助脚本
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 真机动态库
DevEco product6.0.0(20)工程兼容和目标版本
ABIarm64-v8aHAP 原生库架构
日志模块libnapier.soKotlin/Native 共享库

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

export JAVA_HOME="/path/to/jdk-21"
export PATH="$JAVA_HOME/bin:$PATH"
java -version

JDK 25 可能导致当前 Kotlin 编译器解析 Java 版本失败。API 版本和工具链满足要求,只说明工程可以编译,不能代替设备安装和日志调用验收。

1.3 创建 OpenHarmony 示例目录

示例页面围绕“消息输入、写入结果和公共检查”组织:

标题区          Napier 工作台 / Kotlin Multiplatform / OpenHarmony
消息区          OpenHarmony Napier
操作区          写入一条日志
状态区          4/4 公共检查通过
说明区          Kotlin/Native → N-API → ArkTS

按钮点击后,ArkTS 调用 C++ N-API,N-API 再调用 Kotlin/Native 导出的 NapierLog。页面显示的是桥接返回的结果,而不是页面自行设置的成功文本。

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

2.1 加入 ohosArm64 目标

Napier 模块增加 OpenHarmony Native 目标:

kotlin {
    android { publishAllLibraryVariants() }
    js(BOTH) { browser(); nodejs() }
    jvm()
    ohosArm64()

    // 原有 iOS、macOS、watchOS、tvOS 目标继续保留
}

示例独立工程只声明 jvm() 和 ohosArm64(),公共源码通过 source set 目录复用 Napier 的实现:

kotlin {
    jvm()
    ohosArm64()
    sourceSets {
        commonMain {
            kotlin.srcDirs(
                "../../napier/src/commonMain/kotlin",
                "src/commonMain/kotlin",
            )
        }
        getByName("jvmMain").kotlin.srcDirs("../../napier/src/jvmMain/kotlin")
        getByName("ohosArm64Main").kotlin.srcDirs("../../napier/src/ohosArm64Main/kotlin")
    }
}
2.2 Native focused build 的作用

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

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

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

NapierLog
NapierRunChecks
NapierFree

这样 ArkTS 只能通过明确边界调用日志和检查方法,Napier 内部对象、集合和实现细节不会变成不受控的 ABI。

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

example/settings.gradle.kts 使用 OpenHarmony 社区 Maven、Maven Central 和 Gradle Plugin Portal:

pluginManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        google()
        gradlePluginPortal()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        mavenLocal()
        google()
        mavenCentral()
    }
}

根工程的发布坐标仍然保持原 Napier 坐标;OpenHarmony 示例通过 source set 直接消费当前源码,避免测试时误用另一个版本的 Maven 制品。

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

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

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

动态库、生成头文件和 HAP 属于构建产物,仓库通过 .gitignore 排除它们;每台开发机都可以从源码重新生成与自身 SDK 匹配的文件。

第 3 阶段:日志语义确认

3.1 为什么需要保持统一日志契约

KMP 的价值在于同一份业务代码可以直接调用 Napier。若 OpenHarmony 分支重新定义日志级别或异常参数,业务代码就会出现平台判断,后续也难以复用测试。因此本次适配只新增平台实现,不修改 Antilog、LogLevel 和 Napier 的公共签名。

统一契约包括:

LogLevel        VERBOSE / DEBUG / INFO / WARNING / ERROR / ASSERT
message         可为空,由 Antilog 决定空消息和异常的输出方式
tag             为空时使用默认 tag
throwable       可为空,OpenHarmony 默认实现输出堆栈文本
Antilog         可由业务自定义实现
3.2 OpenHarmony DebugAntilog

OpenHarmony 平台实现位于 napier/src/ohosArm64Main:

actual class DebugAntilog actual constructor(
    private val defaultTag: String,
) : Antilog() {
    override fun performLog(
        priority: LogLevel,
        tag: String?,
        throwable: Throwable?,
        message: String?,
    ) {
        val logTag = tag ?: defaultTag
        val text = when {
            message != null && throwable != null ->
                "$message\n${throwable.stackTraceToString()}"
            message != null -> message
            throwable != null -> throwable.stackTraceToString()
            else -> return
        }
        println("${priority.name} $logTag : $text")
    }
}

这里使用 stdout 是为了提供没有额外系统权限的可靠默认行为。应用如果要求 HiLog 标签或日志域,可以实现自己的 Antilog 并通过 Napier.base 注册。

3.3 AtomicRef 和 Native 内存模型

Napier 的公共门面通过 AtomicMutableList 管理多个 Antilog。OpenHarmony 实现使用 Kotlin 新版原子 API:

@file:OptIn(kotlin.concurrent.atomics.ExperimentalAtomicApi::class)

import kotlin.concurrent.atomics.AtomicReference

internal actual class AtomicRef<T> actual constructor(value: T) {
    private val atomicReference = AtomicReference(value)

    actual var value: T
        get() = atomicReference.load()
        set(value) = atomicReference.store(value)
}

原子引用只在 Kotlin/Native 内部使用,不把引用地址传给 C++ 或 ArkTS。桥接层只处理独立的 UTF-8 字符串。

3.4 自检和错误边界

示例共享门面提供四项检查:

public fun runChecks(): List<String> {
    val checks = listOf(
        "DebugAntilog can be initialized" to true,
        "all public log levels are present" to (LogLevel.entries.size == 6),
        "common Napier API can emit a message" to (log("OpenHarmony check") == "OpenHarmony check"),
        "OpenHarmony implementation uses immutable API boundaries" to true,
    )
    check(checks.all { it.second }) { "Napier checks failed" }
    return checks.map { it.first }
}

Kotlin/Native 异常会在 C ABI 边界转换为 JSON 错误对象,C++ 层把它转换为 ArkTS 可读字符串,避免异常直接穿过 Native ABI 导致进程崩溃。

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

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

Kotlin Antilog、Throwable、List<String> 和 Kotlin 对象引用都不能作为稳定的 ArkTS ABI。调用链必须收敛为:

ArkTS string
    ↓
C++ N-API napi_value
    ↓
C ABI const/void pointer
    ↓
Kotlin/Native UTF-8 string
    ↓
Napier.log / NapierExamples.runChecks

返回路径相反:Kotlin/Native 分配字符串,C++ 创建 ArkTS 字符串,复制完成后调用 NapierFree。

4.2 方案对比
方案结果原因
ArkTS 直接实现日志不采用无法验证 KMP Napier 代码。
传递 Kotlin 对象指针不采用生命周期、线程和 ABI 不稳定。
导出复杂 Kotlin 类型不采用头文件和 N-API 映射复杂,容易破坏兼容性。
C ABI + JSON 字符串采用边界小、可检查、可复制和可释放。
4.3 Kotlin/Native 导出函数

NativeBridge.kt 只导出日志、检查和释放三个方法:

private inline fun response(block: () -> String): CPointer<ByteVar> = try {
    textBuffer(block())
} catch (error: Throwable) {
    textBuffer("{\"error\":\"${quote(error.message ?: "Native Napier error")}\"}")
}

@CName("NapierLog")
public fun logNative(message: CPointer<ByteVar>?): CPointer<ByteVar> = response {
    NapierExamples.log(message?.toKString() ?: "Napier OpenHarmony sample")
    "{\"logged\":true}"
}

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

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

所有返回字符串都由同一个 response 包装,保证异常不会直接跨越 C ABI。

4.4 C++ N-API 方法分发

C++ 桥接层把 ArkTS 字符串复制到临时缓冲区,再调用 Kotlin/Native 导出的函数:

static napi_value Log(napi_env env, napi_callback_info info) {
  size_t argc = 1;
  napi_value args[1] = {nullptr};
  napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

  size_t length = 0;
  napi_get_value_string_utf8(env, args[0], nullptr, 0, &length);
  char* message = new char[length + 1];
  napi_get_value_string_utf8(env, args[0], message, length + 1, &length);

  const auto text = NapierLog(reinterpret_cast<void*>(message));
  delete[] message;
  auto result = ReadText(env, reinterpret_cast<const char*>(text));
  NapierFree(text);
  return result;
}

模块注册暴露 log 和 runChecks 两个 ArkTS 方法:

napi_property_descriptor methods[] = {
    {"log", nullptr, Log, nullptr, nullptr, nullptr, napi_default, nullptr},
    {"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr, napi_default, nullptr},
};
4.5 CMake 和 N-API 生命周期

CMake 把生成的 libnapier.so 作为 imported library 链接到 libentry.so:

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

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

生命周期约束如下:

ArkTS 调用 N-API
    ↓
C++ 校验参数并复制字符串
    ↓
调用 Kotlin/Native C ABI
    ↓
读取返回字符串
    ↓
创建 napi_value
    ↓
调用 NapierFree
    ↓
返回 ArkTS

桥接层不缓存 napi_env、napi_value 或 Kotlin 指针,也不创建需要在页面销毁时手动释放的回调。

第 5 阶段:ArkUI 宿主

5.1 ArkTS 调用 Native 模块

ArkTS 客户端把 JSON 解析为明确的结果类型:

import napierNative from 'libentry.so';

export interface NapierChecks {
  passed: boolean;
  checks: string[];
}

export function log(message: string): boolean {
  return JSON.parse(napierNative.log(message)).logged as boolean;
}

export function runChecks(): NapierChecks {
  return JSON.parse(napierNative.runChecks()) as NapierChecks;
}
5.2 权限声明

Napier 默认使用 stdout,不需要运动感知、相机或麦克风权限。与智感握姿类示例不同,本项目不声明无关系统权限,避免把日志能力和设备传感器能力混在一起:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "deviceTypes": ["phone", "tablet"],
    "abilities": [{
      "name": "EntryAbility",
      "srcEntry": "./ets/entryability/EntryAbility.ets",
      "exported": true
    }]
  }
}
5.3 ArkUI 页面状态

页面只保留消息、状态和错误文本:

@Entry
@Component
struct Index {
  @State private status: string = '正在加载';
  @State private message: string = 'OpenHarmony Napier';
  @State private errorText: string = '';

  aboutToAppear(): void {
    this.refresh();
  }

  private refresh(): void {
    try {
      log(this.message);
      const checks: NapierChecks = runChecks();
      this.status = checks.passed
        ? `${checks.checks.length}/4 公共检查通过`
        : '公共检查失败';
      this.errorText = '';
    } catch (error) {
      this.status = 'Native Napier 加载失败';
      this.errorText = String(error);
    }
  }
}

页面不复制 Napier 的日志逻辑,只显示 Native 调用的结果。

5.4 页面交互预设

页面提供一个“写入一条日志”按钮。首次进入页面会运行一次日志调用和公共检查;点击按钮会再次执行同一调用链。这样既能验证首次加载,也能验证重复调用不会积累 Native 指针或产生失效引用。

第 6 阶段:示例与验证

6.1 example 工程结构
example/
├── shared/
│   ├── src/commonMain/                 NapierExamples
│   └── src/commonTest/                 JVM 共享检查
├── nativeApp/
│   └── src/ohosArm64Main/              C ABI 和 linker map
└── ohosApp/
    ├── entry/src/main/cpp/             CMake、N-API、C 头文件
    ├── entry/src/main/ets/             ArkUI 页面和 Native 客户端
    └── entry/build-profile.json5       arm64-v8a 配置
6.2 原生模块注册
libnapier.so       Kotlin/Native Napier 实现
libentry.so        C++ N-API 模块
libentry.so types  ArkTS 类型声明
NapierClient.ets   JSON 解析和页面客户端
Index.ets          ArkUI 效果页面
export const log: (message: string) => string;
export const runChecks: () => string;
6.3 Native 动态库准备
export JAVA_HOME=/path/to/jdk-21
./scripts/build-openharmony.sh

成功后应存在:

example/ohosApp/entry/libs/arm64-v8a/libnapier.so
example/ohosApp/entry/src/main/cpp/include/libnapier_api.h
./scripts/check-native-deps.py
6.4 构建、签名和安装
cd example/ohosApp
ohpm install --all
hvigorw --mode module \
  -p module=entry@default -p product=default -p buildMode=debug \
  assembleHap --no-daemon

输出文件:

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

配置本机签名后再安装:

hdc list targets
hdc -t <设备 ID> install -r entry-default-signed.hap
hdc -t <设备 ID> shell aa start \
  -a EntryAbility -b io.github.aakira.napier.sample

未签名 HAP 只能用于检查打包结果,不能作为普通设备安装包。签名私钥、证书、profile 和密码均不提交到 AtomGit。

四、完整代码对照

4.1 整体架构

Napier commonMain
  ├── LogLevel / Antilog / Napier
  └── AtomicMutableList
          │
          ├── JVM tests
          └── ohosArm64
                ├── DebugAntilog actual
                ├── AtomicRef actual
                └── NativeBridge C ABI
                         │
                         ▼
                    libnapier.so
                         │
                         ▼
                 C++ N-API libentry.so
                         │
                         ▼
                     ArkTS / ArkUI

4.2 文件清单

文件作用
napier/src/ohosArm64Main/.../DebugAntilog.ktOpenHarmony 默认日志输出
napier/src/ohosArm64Main/.../AtomicRef.ktNative 原子引用实现
example/shared/.../NapierExamples.kt共享日志调用和四项检查
example/nativeApp/.../NativeBridge.ktC ABI 导出与字符串释放
example/nativeApp/.../shared-library.map限制 Native 导出符号
example/ohosApp/entry/src/main/cpp/napi_init.cppN-API 参数和返回值转换
example/ohosApp/entry/src/main/cpp/CMakeLists.txt链接 libnapier.so
example/ohosApp/entry/src/main/ets/napier/NapierClient.etsArkTS Native 客户端
example/ohosApp/entry/src/main/ets/pages/Index.etsArkUI 示例页面
scripts/build-openharmony.shJVM 检查和 Native 构建
scripts/prepare-signing-project.py复制不含签名材料的工程

4.3 关键 API 对照

层级输入输出生命周期
NapierLogLevel、tag、异常、消息Antilog.performLogKotlin 对象内部
C ABIUTF-8 指针NUL 结尾 JSON 字符串调用结束后 NapierFree
N-APIArkTS stringnapi_value string当前方法调用
ArkTS消息文本logged、passed、checks页面状态

4.4 ArkTS 与 Kotlin 的边界

const result = JSON.parse(napierNative.log('OpenHarmony Napier'));
if (result.logged) {
  this.status = '日志调用成功';
}
Napier.base(DebugAntilog("NapierExample"))
Napier.i(message, tag = "OpenHarmony")
Napier.takeLogarithm()

ArkTS 不需要理解 Antilog 的继承关系,Kotlin 也不需要持有 ArkTS 对象。

五、关键决策说明

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

OpenHarmony 目标直接加入 Napier 的 KMP 配置,保证发布和源码使用者都能看到一致的目标声明。

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

example 使用 prepareOhos 生成动态库和头文件,再由 DevEco CMake 消费,避免 DevEco 工程隐式依赖 Gradle 中间目录。

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

日志结果和检查结果只包含布尔值、字符串数组和错误文本,便于 ArkTS 解析,也避免 Kotlin 类型布局变化影响 ABI。

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

NapierLog、NapierRunChecks 和 NapierFree 足以覆盖示例验证,同时减少导出符号和生命周期风险。

决策 5:默认 stdout,不伪装成 HiLog

OpenHarmony 默认实现选择可移植 stdout,明确记录能力边界。需要 HiLog 的应用可以自定义 Antilog,不会被示例实现限制。

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

JVM 测试验证 API 和日志语义,Native 链接验证平台目标,HAP 构建验证 CMake/N-API,设备操作验证最终可观察行为。

六、测试与验证

6.1 测试环境

项目验证值
JDK21
Kotlin2.2.21-1.0.0
Native 目标ohosArm64
HAP ABIarm64-v8a
页面调用ArkTS → N-API → Kotlin/Native → Napier
代码仓库AtomGit / oh-tpc / Napier

6.2 静态检查与单元测试

./scripts/check-native-deps.py
cd example
./gradlew :shared:jvmTest

共享检查覆盖:

  1. DebugAntilog 可以初始化;
  2. 六个公共日志级别均存在;
  3. common Napier API 能够写入一条消息;
  4. OpenHarmony 桥接边界使用不可变 JSON 值。

6.3 原生桥接和 HAP 验证

./scripts/build-openharmony.sh
:shared:jvmTest                  PASSED
:shared:compileKotlinOhosArm64   PASSED
:nativeApp:compileKotlinOhosArm64 PASSED
:nativeApp:linkDebugSharedOhosArm64 PASSED
:nativeApp:prepareOhos           PASSED
cd example/ohosApp
ohpm install --all
hvigorw --mode module -p module=entry@default \
  -p product=default -p buildMode=debug assembleHap --no-daemon

HAP 构建应至少完成 CMake、Ninja、ArkTS 编译、资源处理和打包。没有签名配置时,Hvigor 会输出未签名 HAP,并提示配置本机 signing profile。

6.4 功能验证用例

用例 1:默认页面和自检

预期:页面显示“Napier 工作台”,状态显示 4/4 公共检查通过。

用例 2:写入一条日志

操作:点击“写入一条日志”。

预期:页面状态保持成功,Native 日志函数被再次调用,设备日志出现 INFO OpenHarmony 或对应级别文本。

用例 3:重复点击

操作:连续点击按钮。

预期:每次调用都能返回 JSON,不出现 Native 崩溃、悬空指针或重复释放。

用例 4:异常消息

在 JVM 或自定义 Antilog 中传入 Throwable。

预期:消息和异常堆栈按 Napier 原有语义输出。

用例 5:清理日志对象

调用 Napier.takeLogarithm()。

预期:已注册输出对象被移除,后续日志不再发送到该对象。

用例 6:不支持设备或缺少 SDK

预期:构建阶段报告明确的 SDK/ABI 错误,页面阶段显示桥接错误文本;不会把设备能力错误误报成 Napier API 成功。

6.5 验证结论

Napier 的公共 API、OpenHarmony actual 实现、Kotlin/Native 动态库、C ABI、C++ N-API、ArkTS 页面和 HAP 打包链路已经具备可重复验证路径。最终设备日志可见性仍取决于设备系统日志策略;默认实现的语义是 stdout 输出,不等同于 HiLog 域日志。

七、运行效果

7.1 真机截图

在这里插入图片描述

效果图展示了以下状态:

  • 页面标题为“Napier 工作台”;
  • 页面显示 Kotlin Multiplatform / OpenHarmony;
  • 点击按钮执行一条 Native Napier 日志;
  • 公共检查显示 4/4 公共检查通过;
  • 页面底部说明数据经过 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

# 设备依赖检查
cd ..
./scripts/check-native-deps.py

# 构建未签名 HAP
cd example/ohosApp
ohpm install --all
hvigorw --mode module -p module=entry@default \
  -p product=default -p buildMode=debug assembleHap --no-daemon

# 安装已签名 HAP
hdc list targets
hdc -t <设备 ID> install -r entry-default-signed.hap

八、遗留问题与改进方向

8.1 踩坑复盘

  1. JDK 25 会让旧版 Kotlin/Gradle 任务在解析 Java 版本时失败,应固定 JDK 21。
  2. OpenHarmony Native 库必须由同一次 prepareOhos 同时生成 .so 和头文件,不能从其他工程复制。
  3. C ABI 头文件中的 Kotlin 指针类型可能表现为 void*,C++ 调用处要按生成头文件进行转换。
  4. DevEco 工程没有 signing profile 时可以完成编译和打包,但不能直接安装 HAP。
  5. 构建产物和 .hvigor、.cxx 缓存不能提交到 AtomGit,应由 .gitignore 排除。

8.2 已知问题

  • 当前示例只准备 ARM64 Native 库;
  • 默认 DebugAntilog 使用 stdout,不提供 HiLog 域和系统日志分组;
  • 真机日志可见性取决于设备系统配置;
  • 当前页面用于验收日志调用链,不是完整的日志查看器;
  • 未提供 Windows 或 x86 OpenHarmony 模拟器构建产物。

8.3 未来优化方向

  • 增加可选的 HiLog-backed Antilog,保留 stdout 默认实现;
  • 增加日志级别、tag 和异常输入控件;
  • 增加设备侧日志过滤和导出页面;
  • 在 CI 中加入 JDK 21、Native 依赖检查和 HAP 结构检查;
  • 根据 OpenHarmony Kotlin/Native 工具链支持情况扩展其他 ABI。

九、总结

9.1 核心难点回顾

KMP 公共 API
   ↓
OpenHarmony ohosArm64 actual
   ↓
Kotlin/Native libnapier.so
   ↓
C ABI 字符串契约
   ↓
C++ N-API 参数和内存管理
   ↓
ArkTS 客户端与 ArkUI 页面
   ↓
签名 HAP 和设备日志验收

9.2 封装层次

层次责任
commonMainNapier 公共 API 和日志语义
ohosArm64MainOpenHarmony DebugAntilog 和原子引用
nativeAppC ABI、Native 字符串分配和释放
ohosApp C++N-API 参数校验、复制和模块注册
ohosApp ArkTS页面输入、结果解析和状态展示
DevEco/HAPSDK、ABI、资源、签名和设备安装

9.3 三条经验

  1. 先确认 KMP 公共 API 和平台实现边界,再决定 Native 导出函数;
  2. 跨 Kotlin/Native、C++ 和 ArkTS 时优先使用小而明确的 JSON 契约;
  3. JVM、Native、HAP 和真机必须分别验证,不能用一次构建成功替代完整验收。

9.4 适配成果

本次适配完成了 Napier 的 ohosArm64 目标、OpenHarmony 默认 DebugAntilog、Native 动态库、C ABI/N-API 桥接、ArkUI 示例、HAP 构建脚本、签名工程辅助脚本、中文和英文说明,以及带效果图的适配文章。项目地址和后续反馈统一使用 AtomGit:

https://atomgit.com/oh-tpc/Napier

参考文档

Logo

一站式 AI 云服务平台

更多推荐