本文记录 KMPAuth 接入 OpenHarmony 的完整过程,覆盖上游库盘点、Compose 边界切分、真实认证引擎编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI / N-API 桥接、ArkTS 回注 HTTP 传输、ArkUI 真机页面、HAP 构建和真机验收。

KMPAuth 是 Kotlin Multiplatform 生态中广泛使用的 Compose Multiplatform 认证库:Google / Apple / Facebook 登录、邮箱密码、匿名、手机号等凭据源,叠加 Firebase / Supabase 会话后端,UI 侧给出 GoogleButtonUiContainer 这类开箱即用按钮与 rememberXxxSignInState 状态。本次适配与社区早期路线(qrose / haze 的"零依赖内核提取"、KStore 的"存储引擎原样编译")一脉相承但有本质增量:认证引擎(KMPAuth 门面、后端注册表、凭据模型、类型化异常)与 Firebase Auth REST 引擎被原样编译成 OpenHarmony KLIB,Compose UI 隔离进不参与 ohos 编译的源集;而认证库绕不开的 HTTP,以"ArkTS 回注传输"的方式解决——.so 内不链 TLS,引擎挂起、请求冒泡给 ArkTS 系统网络栈执行、响应回灌继续,一个完整的注册 / 登录 / 匿名 / token 刷新 / 会话持久化闭环运行在真机上。

项目地址: AtomGit/oh-tpc/KMPAuth.git

上游仓库: mirzemehdi/KMPAuth

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

KMPAuth 为 Kotlin Multiplatform 应用回答"登录这一件事":凭据从哪来(provider:Google / Apple / Facebook / 邮箱 / 匿名 / 手机号)、会话交给谁管(backend:Firebase / Supabase / 自定义)、UI 怎么接(Compose 按钮容器 + 状态)。3.0 版本把架构切成了三层——kmpauth-core 定义 AuthProviderBackend 接口与进程级注册表,providers/* 只管凭据获取,backends/* 只管会话——KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、浏览器共享。OpenHarmony 应用同样需要这套认证能力,但上游构建(Kotlin 2.4.0 / Gradle 9.6.1 / AGP 9.2.1 / Compose 1.11)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明公共认证引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证会话引擎、REST 协议层、token 刷新链路、文件持久化、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.4.0 / Gradle 9.6.1 / AGP 9.2.1(com.android.kotlin.multiplatform.library)与鸿蒙工具链无法共存于一个 Gradle 构建。
依赖矩阵引擎依赖 kotlinx-coroutines、kotlinx-serialization-json,每个都要有 ohos 变体;GitLive firebase-kotlin-sdk 与 supabase-kt 没有 ohos 制品。
Compose 边界core 与各 provider 的 commonMain 直接 import androidx.compose.*,社区 CMP 线与上游 Compose 1.11 / Kotlin 2.4 组合对不齐。
HTTP 边界REST 引擎的平台接缝是"1 个 HTTP 传输 + 1 个会话存储";Kotlin/Native 侧没有现成免 TLS 负担的 HTTP 栈。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:认证引擎与 Firebase REST 会话后端的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI / N-API 桥接层。认证协议与错误映射完全由上游原版实现负责,ArkTS 只负责调用 Native、执行回注 HTTP、解析 JSON 和展示结果。

1.2 库提供的能力

上游能力按"ohos 可编译性"拆开看:

上游能力依赖ohos 处置
KMPAuth 门面、KMPAuthBackend 注册表、AuthCredential / KMPAuthUser 模型、7 个类型化异常纯 Kotlin + coroutines整体编译(进 KLIB)
Firebase 会话:REST 引擎(Identity Toolkit 全协议)纯 Kotlin + kotlinx-serialization + 2 个平台接缝整体编译(接缝补 ohos actual)
Supabase 会话io.github.jan-tennert.supabase:auth-kt,无 ohos 制品留盘不进构建
Google / Apple / Facebook 凭据获取平台原生 SDK + Compose留盘不进构建
rememberXxxSignInState 等 Compose 状态androidx.compose.runtime隔离进 composeMain 不编译

Demo 提供一个真实的认证会话,对应桥接的全部入口:

  • 初始化(initialize):注入 Firebase Web 配置(apiKey / projectId / appId)与应用沙箱 filesDir,走公开 API KMPAuth.initialize { firebase(...) };
  • 注册 / 登录(signUp / signIn):AuthCredential.EmailPassword 经 Identity Toolkit accounts:signUp / accounts:signInWithPassword 兑换会话,登录后自动经 accounts:lookup 补全用户资料;
  • 匿名登录(signInAnonymously):accounts:signUp 无参形态,已有匿名会话则续用而非新建匿名账号;
  • ID Token(token):currentUserIdToken(forceRefresh),缓存过期或强制刷新时经 Secure Token 服务 securetoken.googleapis.com/v1/token 换新;
  • 会话闭环:refresh token 以 JSON 文件落 filesDir,杀进程重启后自动恢复并在首次用 token 前刷新——证明是落盘而不是内存;
  • 账号生命周期:登出(清会话)、删除账号(不可逆)、重新认证(敏感操作前置)、重置密码邮件、邮件链接登录两步流;
  • 版本握手(version):返回 fork 版本号,来自真实 Native 调用而非静态文字。

1.3 实现适配

维度要求
代码复用kmpauth-core 与 kmpauth-firebase 的 commonMain + restEngineMain 全部文件原样编译,不提取、不重写;引擎与协议逐行零改动。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm("desktop") 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);Compose 状态面移入不注册的 composeMain;uihelper / providers / supabase / deprecated / sampleApp 留盘不进 settings。
桥接稳定2 个 C ABI + UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
HTTP 纪律REST 引擎的传输接缝由"ArkTS 回注"实现:引擎挂起 → 请求冒泡 → 系统网络栈执行 → 响应回灌;.so 内零 TLS 依赖。
依赖纪律版本主体与上游完全一致(coroutines / serialization 1.11.0),只把仓库换到 ohos 制品线(换线不换版),上游测试零改动全绿。
UI 完整单页覆盖全部桥入口:配置卡、账号卡、10 个操作按钮、状态 / 用户 / JWT 结果卡、HTTP 往返日志和版本握手。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit,不放任何第三方镜像站链接。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。KMPAuth 的 Compose 按钮容器与状态留在上游源码树(composeMain),等社区 CMP 与上游版本对齐后整体接回;ArkTS Demo 是认证引擎的验收宿主。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块/依赖边界,确认"引擎纯 Kotlin、UI Compose"的切分线
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版
第 3 阶段:源集拆分       ── composeMain 隔离 6 个状态文件,PhoneVerificationUi 抽回 commonMain
第 4 阶段:ohos 引擎接线  ── 仿上游 wasmJs 五件套写 ohosMain actual(REST 引擎 + 文件存储 + 传输)
第 5 阶段:桥协议与回注   ── @CName C ABI + MiniJson + ArkTS 回注传输(suspendCancellableCoroutine)
第 6 阶段:ArkUI 宿主     ── DevEco Stage 工程、CMake NAPI、认证操作页,覆盖全部桥入口

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
kmpauth-core/                        认证引擎(进 ohos 构建,jvm("desktop") + ohosArm64 + ohosX64)
kmpauth-core/src/composeMain/        上游 Compose 状态面(SignInState、rememberEmailAuthState 等 6 文件,ohos 不编译)
kmpauth-core/src/ohosMain/           新增:loadPlatformBackends / isAndroidPlatform actual
kmpauth-core/src/desktopMain/        新增:JVM 交叉验证 actual(ServiceLoader)
backends/firebase/kmpauth-firebase/  Firebase 会话后端(进 ohos 构建)
  src/commonMain/                    FirebaseAuthBackend expect + FirebaseBackendOptions + firebase() 配置 DSL
  src/restEngineMain/                上游原样:FirebaseRestAuthEngine(Identity Toolkit 全协议)
  src/ohosMain/                      新增:REST 引擎 actual + 回注传输 + POSIX 会话存储 + 桥 + MiniJson + SpinLock
  src/desktopMain/                   新增:JDK HttpClient 传输 + ~/.kmpauth 存储 + ServiceLoader 服务类
kmpauth-uihelper/ providers/* backends/supabase deprecated/ sampleApp/ build-logic/
                                    留盘不进 settings.gradle.kts
example/nativeApp/                   Kotlin/Native 动态库(@CName 出口)
example/harmonyApp/                  DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md       鸿蒙适配说明(中英双份)

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :kmpauth-core、:backends:firebase:kmpauth-firebase 与 :example:nativeApp,rootProject.name = "ohos_kmpauth"。上游的 AGP 约定插件(build-logic)、Compose 插件、vanniktech 发布、二进制校验、dokka 全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(Temurin 21.0.12,org.gradle.java.home 钉绝对路径)
OpenHarmony 目标ohosArm64 + ohosX64(jvm("desktop") 保留作交叉验证)
kotlinxcoroutines / serialization-json 1.11.0-0.1.0-07(ohos 制品线)
Gradle9.6.1(沿用上游 wrapper;2.4.20 线支持 Gradle 9.x)
DevEco product5.1.0(18) 兼容,API 26
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.kmpauth
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version   # openjdk 21.0.12
1.3 源码依赖度盘点
模块源集依赖形态处置
kmpauth-corecommonMain + 5 平台源集coroutines + compose runtime(api)引擎编译;compose 依赖随状态文件一起移出编译面
kmpauth-firebasecommonMain + nonWasmMain + restEngineMain + 5 平台源集core + serialization-json + GitLive(仅 nonWasmMain)commonMain + restEngineMain 编译;nonWasmMain(GitLive)不进
其余 7 个上游模块-Compose / 原生 SDK / supabase-kt留盘不进构建

关键发现是 expect 面极小。core 的 commonMain 只有 3 个 expect:loadPlatformBackends()(注册表发现)、isAndroidPlatform()、phoneAuthPlatformUiContext();firebase 的 restEngineMain 只有 1 个 expect:defaultFirebaseSessionStorage()——REST 引擎把 HTTP 抽象成了单方法接口 FirebaseRestTransport(suspend fun post(url, jsonBody): String),把会话存储抽象成了双方法接口 FirebaseSessionStorage(load / save)。这意味着 ohos 侧只需要补 4 个 actual + 1 套接缝实现,引擎零改动。

还有一个上游设计红利:REST 引擎本来就不是为鸿蒙写的——上游在 Desktop(GitLive firebase-java-sdk 无 auth 实现,issue #204)和 wasm(无 Firebase SDK 目标)已经在用它,ohos 是同一份引擎的第三个平台。错误映射(账号碰撞 KMPAuthUserCollisionException、需近期登录 KMPAuthRecentLoginRequiredException)、token 刷新、会话恢复的 stale 语义都是上游双端服役过的原件。

1.4 页面边界
标题区          KMPAUTH · OPENHARMONY / 版本握手行(3.0.5-ohos.1,来自真实 Native 调用)
Tab 切换        认证操作 / 关于
配置卡          apiKey / projectId / appId 三输入框 + filesDir 展示 + 初始化按钮
账号卡          email / password 输入框(密码框)
操作卡          注册 / 登录 / 匿名登录 / 当前用户 / 获取 ID Token / 刷新 Token /
                重认证 / 重置密码邮件 / 登出 / 删除账号(10 按钮,按破坏性配色)
结果卡          状态行 + 当前用户卡(uid/email/displayName/providerId/isAnonymous/providerIds)+ JWT 全文(可复制)
日志卡          带时间戳的 HTTP 往返日志(→ POST … / ← 响应摘要,最新在前,上限 100 条)
关于页          版本握手、架构分层、HTTP 策略、会话持久化说明、仓库与社区链接

页面启动时先做版本握手,再自动注入 getContext().filesDir;未初始化点操作给 Toast 提示。

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

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的 kotlinx 制品。用 .pom / .module 逐个验证:

org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.20-0.1.0-07            ✓ 200
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07       ✓ 200(.module 含 ohosArm64/ohosX64 变体)
org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0-0.1.0-07    ✓ 200
org.jetbrains.kotlin.multiplatform 插件 marker 2.4.20-0.1.0-07      ✓ 200
dev.gitlive:firebase-auth / io.github.jan-tennert.supabase:*         ✗ 无 ohos 制品

三个关键结论:其一,kotlinx 版本主体与上游声明逐字相同(coroutines 1.11.0、serialization 1.11.0),只是仓库换线——commonMain 里一行依赖坐标都不用改;其二,serialization 编译器插件不需要:REST 引擎只用树 API(buildJsonObject / parseToJsonElement),编译面无一处 @Serializable;其三,GitLive 与 supabase-kt 无 ohos 制品,决定"只编 REST 引擎、不编 GitLive"的边界,与上游 wasm 的处境同构。

2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)

与 KStore 同款结论:coroutines 1.11.0-0.1.0-07 的 stdlib 元数据是 2.4.20,旧钉法 Kotlin 2.2.21-1.0.0(qrose / haze 用的)编译期直接报 metadata is 2.4.0, expected 2.3.0 or less。工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07。上游源码本来就是 Kotlin 2.4.0 编的,2.4.20 同代,源码级兼容成本为零;Gradle 9.6.1 上游 wrapper 原样沿用,不像 2.2.21 旧线必须降 8.14.1。

settings.gradle.kts 把 eazytec 仓库写在最前:

pluginManagement {
    repositories {
        // HarmonyOS Kotlin distribution MUST come first
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}
// dependencyResolutionManagement 同样置前

rootProject.name = "ohos_kmpauth"

include(":kmpauth-core")
include(":backends:firebase:kmpauth-firebase")
include(":example:nativeApp")

gradle.properties 钉 JDK 21 与版本号,并开启 kotlin.native.distribution.downloadFromMaven=true(KN 发行版含 ohos 交叉工具链从 Maven 镜像下载):

org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.12.101-hotspot
version=3.0.5-ohos.1
kotlin.native.ignoreDisabledTargets=true
kotlin.native.distribution.downloadFromMaven=true

gradle/libs.versions.toml 收敛为 Kotlin + kotlinx 两条版本线,无 Compose、无 AGP、无发布插件。

2.3 编译真实引擎

kmpauth-core/build.gradle.kts 收敛为纯 KMP 模块:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm("desktop")                            // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {
            api(libs.kotlinx.coroutines.core)
        }
        commonTest.dependencies {
            implementation(kotlin("test"))
            implementation(libs.kotlinx.coroutines.test)
        }
    }
}

kmpauth-firebase 同构,但要多接一个中间源集:上游的 restEngineMain(REST 引擎)要挂到 desktop 与 ohos 两个目标下面,而 GitLive 的 nonWasmMain 不进:

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm("desktop")
    ohosArm64()
    ohosX64()

    sourceSets {
        val restEngineMain = register("restEngineMain") {
            dependsOn(getByName("commonMain"))
            dependencies {
                // JSON runtime API only — no @Serializable, no compiler plugin.
                implementation(libs.kotlinx.serialization.json)
            }
        }
        getByName("desktopMain") { dependsOn(restEngineMain.get()) }
        getByName("ohosMain") { dependsOn(restEngineMain.get()) }

        commonMain.dependencies {
            api(project(":kmpauth-core"))
        }

        // 上游 jvmTest 原样复用为 desktop 交叉验证套件(仅排除两个 jvmMain 专属用例文件)
        getByName("desktopTest") {
            kotlin.srcDir("src/jvmTest/kotlin")
            kotlin.exclude("com/mmk/kmpauth/firebase/backend/DesktopWebAuthFlowPageTest.kt")
            kotlin.exclude("com/mmk/kmpauth/firebase/backend/ManualE2ETest.kt")
            dependencies {
                implementation(kotlin("test"))
                implementation(libs.kotlinx.coroutines.test)
            }
        }
    }
}

实测:

.\gradlew.bat :kmpauth-core:compileKotlinOhosArm64 :kmpauth-core:compileKotlinOhosX64          # BUILD SUCCESSFUL
.\gradlew.bat :backends:firebase:kmpauth-firebase:compileKotlinOhosArm64 `
              :backends:firebase:kmpauth-firebase:compileKotlinOhosX64                          # BUILD SUCCESSFUL

引擎与 REST 协议的 commonMain / restEngineMain 文件一个没动。

2.4 Compose 面的处置:composeMain 隔离 + 类型抽回

core 的 commonMain 里有 6 个文件 import androidx.compose.*(SignInState、EmailAuthState、AnonymousAuthState、OAuthState、PhoneAuthState、LocalKMPAuthBackend,全是 remember* 状态面)。处置分三步:

  1. 搬移:6 个文件 git mv 到 src/composeMain/,该目录不注册进任何源集,ohos / desktop 都不编译,纯留档等社区 CMP 与上游对齐后接回;对应的 2 个 Compose 测试同步搬到 src/composeTest/。
  2. 类型抽回:PhoneVerificationUi 接口声明在 PhoneAuthState.kt 里,但 AuthProviderBackend.signInWithPhone 的签名引用了它——把它抽回 commonMain/auth/PhoneVerificationUi.kt(纯接口,零 Compose),搬移后的文件留注释指路。这是 qrose 适配里"Code128Type 抽回 commonMain"的同款手法。
  3. expect 保留:phoneAuthPlatformUiContext() 的 expect 跟着状态文件进了 composeMain,jvm 的 @Composable actual 无需处理(ohos 构建用 desktopMain 全新目录,不编译上游 jvmMain——后者的 phone actual 也是 @Composable)。

desktopMain 因此是全新最小目录:ServiceLoader 版 loadPlatformBackends + isAndroidPlatform()=false,不碰任何 Compose。

2.5 测试依赖的处置

commonTest 的依赖表对 ohos 测试编译可见,但 Kotlin/Native 没有 ohos 测试运行器。处置是两条纪律的组合:测试统一在 JVM 跑(desktopTest),根构建全局禁用 ohos 测试任务:

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

firebase 的上游 jvmTest 只依赖 commonMain + restEngineMain 符号,直接 srcDir 复用为 desktopTest;唯一例外是 DesktopWebAuthFlow 页面构建用例(测上游 jvmMain 的浏览器流页面,不进本构建)——拆到独立文件 DesktopWebAuthFlowPageTest.kt 后排除,上游构建行为不变。

第 3 阶段:桥协议建模

3.1 JSON 契约
com.mmk.kmpauth.core/** + com.mmk.kmpauth.firebase restEngine(上游引擎,原样编译)
        ↓ KMPAuth 门面 / KMPAuthBackend 注册表 / FirebaseRestAuthEngine
KmpAuthOhosBridge + MiniJson(扁平 JSON 进出,suspend 协程编排)
        ↓ UTF-8 JSON ⇄(httpRequest / httpResponse 回注)
N-API kmpauthDispatch() / kmpauthVersion()
        ↓
ArkUI Index.ets(认证操作页)

请求 17 个 op,覆盖引擎全部无 UI 面:

{"op":"initialize","apiKey":"...","projectId":"...","appId":"...","filesDir":"..."}
{"op":"currentUser"} / {"op":"signOut"} / {"op":"deleteAccount"} / {"op":"version"}
{"op":"signUp","email":"...","password":"..."}   {"op":"signIn","email":"...","password":"..."}
{"op":"signInAnonymously"}
{"op":"signInWithIdToken","providerId":"google.com","idToken":"...","accessToken":null,"rawNonce":null}
{"op":"token","forceRefresh":false}
{"op":"reauthenticate","email":"...","password":"..."}
{"op":"sendPasswordResetEmail","email":"..."}
{"op":"sendSignInLinkToEmail","email":"...","url":"..."}
{"op":"isSignInWithEmailLink","link":"..."}      {"op":"signInWithEmailLink","email":"...","link":"..."}
{"op":"poll"}                                     {"op":"httpResponse","id":1,"body":"{...}"}

响应四种 kind,其中 httpRequest 是回注协议的核心:

{"ok":true,"kind":"result","op":"signIn","user":{"uid":"...","email":"...","isAnonymous":false,"providerIds":["password"]}}
{"ok":true,"kind":"result","op":"token","token":"eyJhbGciOiJSUzI1NiIs..."}
{"ok":true,"kind":"httpRequest","id":1,"method":"POST",
 "url":"https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=...",
 "body":"{...}","headers":{"Content-Type":"application/json"}}
{"ok":true,"kind":"pending"}
{"ok":false,"kind":"error","op":"signIn","error":"Firebase auth failed: EMAIL_NOT_FOUND","errorType":"IllegalStateException"}

ArkTS 侧一个固定循环吃掉所有 kind:

let res = this.callBridge(JSON.stringify(request));
while (res.kind === 'httpRequest') {
  const body = await this.postJson(res.url, res.body);           // @ohos.net.http
  res = this.callBridge(JSON.stringify({ op: 'httpResponse', id: res.id, body: body }));
}
while (res.kind === 'pending') {
  await this.sleep(50);
  res = this.callBridge('{"op":"poll"}');
}
// res.kind === 'result' 或 'error'

一次业务操作可能多轮往返:登录 → accounts:lookup 补资料 → securetoken 刷新,ArkTS 只管循环,协议状态机全在 Kotlin。

3.2 传输反转:HTTP 为什么这样设计

REST 引擎的接缝是 FirebaseRestTransport(suspend post(url, body): String)。ohos actual 的实现是回注:

internal class OhosRestTransport : FirebaseRestTransport {
    override suspend fun post(url: String, jsonBody: String): String =
        withTimeout(60_000) {
            suspendCancellableCoroutine { continuation ->
                HttpPump.offer(PendingHttpPost(nextRequestIdValue(), url, jsonBody, continuation))
            }
        }
}

post() 挂起的瞬间就是交接点:HttpPump 单槽保管请求,下一次 dispatch 返回把它作为 httpRequest 冒泡给 ArkTS;httpResponse op 到达时 continuation.resume(body),引擎在协程里继续跑。三个边界全部闭合:

  • 超时:60 秒无回灌 → TimeoutCancellationException → 失败 Result,桥不会挂死;
  • 互斥:同一时刻只允许一个业务操作(busy 保护),引擎每操作串行,单槽足够;
  • 线程:桥的编排状态由 SpinLock(AtomicInt compareAndSet)保护——该 Kotlin 分支不带 kotlin.concurrent.SynchronizedObject,而 kotlinx.coroutines 的 Mutex 是 suspend-only,自旋锁是临界区仅数个字段写时的最小正确解。

.so 因此零 TLS 依赖:证书校验、HTTP/2、DNS 全部交给系统网络栈,库侧只保留请求构造与响应解析——这正是"引擎在 Kotlin、IO 在宿主"的分层。

3.3 错误通道

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI;上游引擎抛出的类型化异常(碰撞、需近期登录、网络错误)在桥侧转成 {"ok":false,"error":...,"errorType":"KMPAuthUserCollisionException"} 文本通道,ArkTS 统一渲染。非法 JSON、缺 op、缺参数、不认识的 op、未初始化就操作、并发操作,全部走同一个 ok:false 通道。

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

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libkmpauth_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohoskmpauth.so)
  │ KMPAuthDispatch / KMPAuthVersion
  ▼
KmpAuthOhosBridge → KMPAuth → FirebaseRestAuthEngine
  ⇅ HTTP 回注(DisposeString 纪律)

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制认证语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("KMPAuthDispatch")
fun kmpauthDispatch(request: String): String = KmpAuthOhosBridge.dispatch(request)

@CName("KMPAuthVersion")
fun kmpauthVersion(): String = KmpAuthOhosBridge.VERSION   // "3.0.5-ohos.1"
C ABI 符号ArkTS 方法覆盖内核
KMPAuthDispatchkmpauthDispatch(req)17 个 op:初始化 / 注册 / 登录 / 匿名 / token / 生命周期
KMPAuthVersionkmpauthVersion()fork 版本握手(真机版本行的数据来源)
4.3 C++ N-API 方法分发
static napi_property_descriptor desc[] = {
    {"kmpauthDispatch", nullptr, NapiKmpAuthDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"kmpauthVersion", nullptr, NapiKmpAuthVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoskmpauth_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoskmpauth_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。

4.4 CMake 和类型声明
add_library(ohoskmpauth SHARED IMPORTED)
set_target_properties(ohoskmpauth PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(kmpauth_napi SHARED napi_init.cpp)
target_link_libraries(kmpauth_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoskmpauth)

OHOS_ARCH 为空或 libohoskmpauth.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const kmpauthDispatch: (request: string) => string;
export const kmpauthVersion: () => string;

nm_modname="kmpauth_napi" 与 ArkTS import 名、index.d.ts 导出名三处一致。

第 5 阶段:ArkUI 宿主

5.1 注入路径和触发操作
aboutToAppear(): void {
  this.version = kmpauthVersion();                    // 版本握手:真实 Native 调用
  this.filesDir = getContext(this).filesDir;          // 沙箱路径,随 initialize 下发
  this.log('版本握手: ' + this.version);
}

private initialize(): void {
  this.run('initialize', {
    apiKey: this.apiKey.trim(), projectId: this.projectId.trim(),
    appId: this.appId.trim(), filesDir: this.filesDir,
  });
}

run() 内部走 3.1 的回注循环,busy 期间互斥;未初始化点操作 → Toast 提示。

5.2 页面交互

认证操作 Tab 自上而下:配置卡(三输入框 + filesDir 展示 + 初始化)、账号卡(邮箱 / 密码)、操作卡 10 按钮(注册深绿 / 登录蓝 / 匿名灰 / token 橙 / 删除深红,语义与破坏性对齐)、结果卡(状态行 + 用户卡 + JWT 全文可复制)、日志卡(时间戳,→ POST 与 ← 响应 逐轮留痕,最新在前、上限 100 条)。关于 Tab 展示版本握手、ArkTS → NAPI → C ABI → Kotlin 引擎 → @ohos.net.http → Firebase 分层说明与 AtomGit 仓库 / 社区链接。

5.3 对比度和错误边界

主操作深绿按钮白字,破坏性操作红色系,ok:false 时错误原文与 errorType 进状态行与日志。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志区;回注循环有 20 轮上限,防异常场景下的无限往返。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoskmpauth)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/com/mmk/kmpauth/sample/ohos/
│       └── Bridge.kt                         @CName × 2(转发到库内 KmpAuthOhosBridge)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.kmpauth、versionName 3.0.5-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/          linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
        ├── src/main/cpp/CMakeLists.txt       IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp        N-API 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohoskmpauth_api.h
        ├── src/main/cpp/types/libkmpauth_napi/{index.d.ts, oh-package.json5}
        ├── src/main/module.json5             requestPermissions: ohos.permission.INTERNET(唯一权限)
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面
6.2 构建、签名和安装
.\gradlew.bat :kmpauth-core:desktopTest :backends:firebase:kmpauth-firebase:desktopTest    # 29 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example\harmonyApp
hvigorw assembleHap --mode module -p product=default -p buildMode=debug --no-daemon

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组)。

hdc list targets -v
hdc -t <设备序列号> install -r \
  entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
  -a EntryAbility -b org.terminator.ohos.kmpauth -m entry

四、完整代码对照

4.1 整体架构

com.mmk.kmpauth.core/**(上游引擎原样编译)
    │ KMPAuth 门面 / KMPAuthBackend 注册表 / AuthCredential / KMPAuthUser / 类型化异常
com.mmk.kmpauth.firebase commonMain + restEngineMain(上游原样编译)
    │ FirebaseAuthBackend expect / FirebaseRestAuthEngine(Identity Toolkit 全协议)
ohosMain(新增接缝)
    │ REST actual + OhosRestTransport(回注)+ POSIX 会话存储 + 自动注册
example/nativeApp Bridge.kt + 库内 KmpAuthOhosBridge + MiniJson
    │ KMPAuthDispatch / KMPAuthVersion
libkmpauth_napi.so
    │ kmpauthDispatch / kmpauthVersion
Index.ets
    │ JSON parse + 回注 HTTP 循环 + @State 更新
    ▼
ArkUI 真机页面(配置 / 账号 / 操作 / 结果 / 日志)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_kmpauth、7 个上游模块移出
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 + kotlinx 1.11.0-0.1.0-07 两条线,无 Compose/AGP/发布插件
build.gradle.kts(根)版本 3.0.5-ohos.1、全局禁用 Ohos*Test* 任务
kmpauth-core/build.gradle.kts纯 KMP:jvm("desktop") + ohosArm64 + ohosX64
kmpauth-firebase/build.gradle.kts同上 + restEngineMain 中间源集接线 + desktopTest 复用 jvmTest
kmpauth-core/src/composeMain/上游 6 个 Compose 状态文件(不编译,等 CMP 对齐接回)
kmpauth-core/src/commonMain/.../PhoneVerificationUi.kt唯一抽回的纯接口(signInWithPhone 签名依赖)
kmpauth-core/src/{ohos,desktop}Main/4 个 expect 的最小 actual
kmpauth-firebase/src/ohosMain/REST actual、回注传输、POSIX 会话存储、@EagerInitialization 注册、KmpAuthOhosBridge、MiniJson、SpinLock(9 个新文件,全仓新增源码面)
kmpauth-firebase/src/desktopMain/JDK HttpClient 传输、~/.kmpauth 存储、ServiceLoader 服务类 + META-INF/services
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(认证操作 + 关于)
README.OpenHarmony_CN.md鸿蒙适配主文档(24 节)

4.3 关键 API 对照

层次API作用
引擎KMPAuth.signIn / signUp / signInAnonymously凭据兑换会话,suspend,内部走注册表分发
引擎KMPAuth.currentUserIdToken(forceRefresh)JWT 获取;过期经 Secure Token 刷新
引擎KMPAuthBackend.registerDefault后端自动注册(ohos 走 @EagerInitialization)
RESTFirebaseRestAuthEngine.call()Identity Toolkit 端点调用 + 错误码→类型化异常映射
RESTFirebaseRestTransport.postHTTP 接缝(ohos:回注;desktop:JDK HttpClient;wasm:fetch)
RESTFirebaseSessionStorage会话持久化接缝(ohos:filesDir JSON 文件)
NativeKMPAuthDispatch / KMPAuthVersionC ABI JSON 导出
N-APIkmpauthDispatch(req) / kmpauthVersion()向 ArkTS 暴露能力
ArkTSdispatch(op)触发操作、驱动回注循环、刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const res = await kmpauth('signIn', { email: 'a@b.c', password: 'secret' });
// res.ok === true;res.user.uid / email / providerIds 可直接渲染
// 杀进程重启后 kmpauth('currentUser') 仍返回同一 uid(filesDir 会话恢复)
// 库内 ohosMain:桥把 op 翻译成上游公开 API,一行认证逻辑都不写
"signIn" -> {
    val email = required(op, req, "email"); val password = required(op, req, "password")
    userResult(op) { KMPAuth.signIn(AuthCredential.EmailPassword(email, password)) }
}

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:真编译引擎 + REST 后端,而不是 ArkTS 重写认证

认证协议的错误映射(EMAIL_EXISTS / CREDENTIAL_TOO_OLD_LOGIN_AGAIN → 类型化异常)、token 刷新链、会话恢复 stale 语义是上游双端(Desktop #204 / wasm)服役过的原件,重写必漂移且失去同步能力。REST 引擎的接缝只有"1 传输 + 1 存储",ohos 补两个 actual 就把整个后端接活——这比提取"零依赖内核"更保守:什么都没抽,自然什么都不漂。

决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 沿用 9.6.1

与 KStore 相同的推导:coroutines 1.11.0-0.1.0-07 基于 stdlib 2.4.20 构建,旧钉法 2.2.21 读不了 2.4 元数据;上游本来就是 Kotlin 2.4.0,2.4.20 同代零降级。Gradle 方面 2.4.20 线支持 9.x,上游 wrapper 9.6.1 原样保留。

决策 3:composeMain 隔离 + 类型抽回,而不是砍 commonMain

上游 core 的 commonMain 混装"引擎 + Compose 状态"。整体不编等于丢引擎,整体都编等于拖 Compose。第三条路:状态面 git mv 到不注册的 composeMain(上游构建与 ohos 构建都看得见这批文件的归属),被引擎签名引用的 PhoneVerificationUi 抽回 commonMain。等社区 CMP 与上游 Compose 1.11 / Kotlin 2.4 对齐,把目录注册回源集即可整体复活,目录结构已按此预留。

决策 4:HTTP 走 ArkTS 回注,而不是在 .so 里链 TLS

Kotlin/Native 侧自做 HTTPS 要么拖 Ktor + curl 引擎(依赖矩阵爆炸、无 ohos 制品),要么手写 TLS(不可审计)。回注方案把 IO 完整交给系统 @ohos.net.http:证书校验、代理、HTTP/2 都是系统能力;库侧只保留请求构造与响应解析——认证语义 100% 在 Kotlin,字节传输 100% 在宿主。代价是桥协议从"同步一来一回"升级为"挂起-回灌"状态机,由 suspendCancellableCoroutine + 60 秒超时 + 单槽 HttpPump 闭合。

决策 5:desktopTest 交叉验证 + 测试近乎零改动

引擎逻辑在 commonMain / restEngineMain,JVM 与 ohos 跑同一份代码;29 项 desktopTest 里 25 项是上游原测试文件(FirebaseRestAuthEngineTest 24 项 + 发现机制 1 项),唯一改动是把一个 jvmMain 专属用例拆到独立文件再排除——上游构建行为不变。测试近乎零改动,交叉验证"引擎没漂"的说服力才成立。

决策 6:jvm(“desktop”) 而不是复用上游 jvmMain

上游 core 的 jvmMain/PhoneAuthState.jvm.kt 是 @Composable、firebase 的 jvmMain 引 GitLive——复用等于把 Compose 与 GitLive 拖进 ohos 构建。desktop 用全新最小源集(4 个 actual + JDK 传输 + ServiceLoader),桌面目标验证的恰好是"ohos .so 里跑的同一条 REST 路径"。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x86_64)、JDK 21(Temurin 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

.\gradlew.bat :kmpauth-core:desktopTest :backends:firebase:kmpauth-firebase:desktopTest

29 项用例全部通过(上游原测试文件):

测试类用例数钉住什么
FirebaseRestAuthEngineTest24REST 协议契约:请求体构造、响应解析、碰撞/需近期登录异常映射、token 刷新链、会话恢复 stale 语义、web-flow 会话收编
FirebaseBackendDiscoveryTest1ServiceLoader 自动发现后端(经 desktopMain 服务类)
KMPAuthBackendTest2注册表:默认后端、多后端、no-backend 失败语义
LoggerContractTest2日志契约

依赖换线没有 API 漂移:coroutines / serialization 1.11.0 换成 -0.1.0-07 线后版本主体不变,上游测试照跑全绿——"换线不换版"的直接证据。

6.3 HAP 验证

:kmpauth-core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:backends:firebase:kmpauth-firebase:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(copyKnSo* 回填 entry/libs)
llvm-nm --defined-only: KMPAuthDispatch / KMPAuthVersion 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libkmpauth_napi.so)
CompileArkTS: finished(仅 deprecation 警告)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

entry-default-unsigned.hap 内含双 ABI 各三件套(libkmpauth_napi.so 约 65KB / libohoskmpauth.so zip 后约 4.4MB(debugShared 未压缩约 9.4MB,含 Kotlin/Native 运行时与 coroutines)/ libc++_shared.so 约 1.3MB);module.json 确认唯一权限 ohos.permission.INTERNET。

6.4 功能用例

  1. 启动后标题行显示 3.0.5-ohos.1、日志出现 版本握手: 3.0.5-ohos.1(来自真实 Native 调用,非静态文字);
  2. filesDir 自动注入真机沙箱路径(/data/storage/el2/base/haps/entry/files)并随 initialize 下发;
  3. 填入 Firebase Web 配置点初始化,返回 ok:true 且 apiKey 尾 4 位回显;
  4. 匿名登录:用户卡出现 uid、isAnonymous:true,日志逐轮显示 → POST https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=… 与 ← {"idToken":…} 回注往返;
  5. 邮箱注册 / 登录:用户卡显示 email、providerIds:["password"];错误密码在状态行呈现 INVALID_PASSWORD 类错误原文;
  6. 获取 ID Token:JWT 全文可复制;forceRefresh 时日志出现 securetoken 往返;
  7. 杀掉应用重启后点当前用户:uid 不变(会话从 filesDir 恢复,恢复后的 token 首次使用前自动刷新);
  8. 登出后当前用户为空、会话文件删除;删除账号后 Firebase 控制台用户消失;
  9. 每步操作的 HTTP 往返按时间戳滚动留痕(→/← 成对)。

6.5 验证结论

认证引擎、REST 协议层、token 刷新链路、文件持久化、Native 动态库、C ABI、N-API、ArkTS 回注 HTTP、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的认证会话(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/KMPAuth.git
cd KMPAuth
export JAVA_HOME="/path/to/jdk-21"
./gradlew :kmpauth-core:compileKotlinOhosArm64 :backends:firebase:kmpauth-firebase:compileKotlinOhosArm64   # klib
./gradlew :kmpauth-core:desktopTest :backends:firebase:kmpauth-firebase:desktopTest                          # 29 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.kmpauth -m entry
hdc shell hilog | grep KmpAuth

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :kmpauth-core / :kmpauth-firebase 真正编译成 ohosArm64 / ohosX64;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Kotlin 2.2.21 旧线编译 kotlinx 1.11.0-0.1.0-07 报 metadata 2.4 不兼容——工具链版本由依赖制品的 stdlib 元数据反推,钉 2.4.20-0.1.0-07;
  4. Gradle 9.6 + 该 Kotlin 分支下 by getting / by registering 从废弃升级为脚本编译错误,且注册自定义源集后不生成目标访问器——统一改 getByName("desktopMain") / register("restEngineMain") 字符串 API;
  5. 该分支 stdlib 没有 kotlin.concurrent.SynchronizedObject / synchronized,AtomicLong 也没有 addAndFetch / fetchAndAdd——桥内自写 SpinLock(AtomicInt compareAndSet 循环),请求号用 CAS 自增;
  6. cinterop 的 POSIX API 需 @file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class),否则 ohos 会话存储的 fopen/fread 全线报 opt-in 错误;
  7. 上游 jvmTest 混入了 jvmMain 专属类的用例(DesktopWebAuthFlow),desktopTest 编译失败——拆到独立文件再 exclude,上游构建行为不变;
  8. hvigorw assembleHap 到 PackageHap 报 spawn java ENOENT:打包工具是 jar,PATH 里没有 java——把 DevEco 自带 jbr(…\DevEco Studio\jbr\bin)加进 PATH 即过;
  9. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  10. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  11. 认证 Demo 的 module.json5 别忘 requestPermissions: ohos.permission.INTERNET——qrose 这类纯本地渲染库没有权限项,照抄它的模板会得到一个所有网络操作都失败的 Demo;
  12. 签名材料由 DevEco 自动写入 build-profile.json5,证书绝不进入 AtomGit(仓库内保持空 signingConfigs)。

8.2 已知问题

  • Compose 按钮容器与 remember* 状态不在(composeMain 留档),ohos 应用以 ArkTS 自绘 UI + 桥调用替代;
  • Google / Apple / Facebook 原生凭据流不在:SDK 无鸿蒙版本;signInWithIdToken 保留为兑换通道,应用层取得 ID Token 后即可登录;
  • Supabase 后端不在:supabase-kt 无 ohos 制品,待制品线出现后按 desktopMain 同款模式接入;
  • 手机号登录不可用:REST 引擎需 reCAPTCHA(与上游 Desktop / wasm 同一限制);OAuth 浏览器流同理(webFlowRunner = null);
  • currentUserFlow 不跨 C ABI:桥是请求-响应式,订阅式推送过不了 C 签名,ArkTS 以操作后读回观察状态;
  • HTTP 依赖 ArkTS 宿主回注:脱离宿主直接调 KMPAuthDispatch 且不回灌 httpResponse,60 秒后以超时错误结束(而非永久挂起);
  • android / js / wasmJs / apple 目标剥离(源文件留在树里),发布、dokka、apiCheck 一并剥离,本 fork 不发 Maven Central;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.kmpauth 匹配的签名。

8.3 未来优化方向

  • 增加 currentUserFlow 状态变化推送(N-API 线程安全回表),替代操作后读回;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 等 supabase-kt / 社区 CMP 的 ohos 制品线,分别解锁第二个后端与 Compose UI 面;
  • 把回注传输协议(挂起-回灌状态机)沉淀为社区通用"宿主 HTTP"模式,供其他网络类 KMP 库复用;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 kmpauth-core + kmpauth-firebase(commonMain + restEngineMain)
    → 验证 ohos kotlinx 制品线(coroutines / serialization)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1 沿用)
    → 根构建替换(cmp_qrose 模式)+ composeMain 隔离 + PhoneVerificationUi 抽回
    → ohosArm64 / ohosX64 KLIB + ohosMain 接缝 actual
    → HTTP 反转:回注传输(suspendCancellableCoroutine + HttpPump + 60s 超时)
    → C ABI(@CName × 2)+ 17 op JSON 协议
    → C++ N-API(DisposeString 纪律)
    → ArkTS 回注循环 + ArkUI 真机单页验收(注册 / 登录 / 匿名 / token / 会话持久化)

9.2 封装层次

  • 引擎层:上游原版 KMPAuth 门面、KMPAuthBackend 注册表、凭据 / 用户模型、类型化异常;
  • 协议层:上游原版 FirebaseRestAuthEngine(Identity Toolkit 请求构造、响应解析、错误映射、token 刷新、会话编解码);
  • Native 层:双 ABI 动态库,2 个 C ABI 输出 JSON,回注传输与 POSIX 会话存储在库内 ohosMain;
  • N-API 层:参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:配置注入、回注 HTTP 循环、页面状态、日志与错误文本;
  • DevEco 层:CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. expect 面决定适配成本:KMPAuth 全库编译面只有 4 个 expect、2 个接缝接口——上游架构分层(provider / backend / core)本身就是为"换平台实现后端"设计的,适配成本由上游架构质量决定,跨桥前先盘点 expect 清单,别急着写代码;
  2. 网络类库的答案是"传输反转":存储库(KStore)的 IO 落在 kotlinx-io,网络库的 IO 没有等价制品——把 HTTP 交给宿主系统栈、把协议留在 Kotlin,.so 零 TLS 依赖的同时认证语义一寸不让;挂起-回灌状态机(挂起点即交接点、超时兜底、单槽回灌)是这个模式的三要素;
  3. 交叉验证要"同路径"而不是"同文件":desktop 目标不复用上游 jvmMain(那里是 GitLive 路径),而是与 ohos 走同一条 REST 路径——验证的是".so 里跑的代码",不是"另一份相似代码";29 项测试里 25 项上游原文件零改动,是"引擎没漂"的可信证明。

9.4 适配成果

当前适配已完成:

  • KMPAuth 3.0.5 真实认证引擎的双 ABI Kotlin/Native 编译(上游引擎文件零改动,新增源码集中在 ohos 接缝与桥);
  • HarmonyOS kotlinx 制品线在认证库上的完整落地(coroutines / serialization-json 1.11.0-0.1.0-07);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号)+ 17 op JSON 桥协议;
  • ArkTS 回注 HTTP 传输(@ohos.net.http),.so 零 TLS 依赖,单操作多轮往返自动衔接;
  • 会话文件持久化与重启恢复(filesDir + POSIX);
  • DevEco Stage ArkUI 单页验收页面(配置、10 操作、用户 / JWT 结果卡、HTTP 往返日志、版本握手);
  • 29 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so)+ 签名、设备安装和真机验证;
  • AtomGit 项目文档、OpenHarmony README(中英双份)、博客文章和本地效果图。

适配过程 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐