本文记录 kasechange 接入 OpenHarmony 的完整过程,覆盖上游构建盘点、multigradle 约定插件拆除、真实转换引擎编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

kasechange 是 PearX Team 的 Kotlin Multiplatform 字符串命名风格转换库:一条「分词 → 重组」管线把任意字符串在 12 种命名格式(SCREAMING_SNAKE_CASE、snake_case、PascalCase、camelCase、TRAIN-CASE、kebab-case、UPPER SPACE CASE、Title Case、Sentence case、lower space case、UPPER.DOT.CASE、dot.case)之间互转——WordSplitter 按大小写突变、边界字符、数字分组切词,CaseFormatter 按目标格式重组,universalWordSplitter 提供通用分词行为,CaseFormatterConfig / WordSplitterConfig 支持自定义组合。本次适配与 KStore 那次"依赖矩阵决定工具链"(kotlinx 四件套把 Kotlin 钉到 2.4.20-0.1.0-07)形成鲜明对照:kasechange 零第三方依赖、零 expect/actual、零 UI,上游 6 个源码文件与 71 个测试用例一字未动,唯一的障碍是构建本身——上游目标平台全部由 net.pearx.multigradle 约定插件生成,它不认识 ohosArm64。拆掉约定、换成社区已过审的 cmp_qrose 模式显式构建(HarmonyOS Kotlin 2.2.21-1.0.0 + Gradle 8.14.1),真实的转换引擎(CaseFormat、WordSplitterConfigurable、全部扩展函数)被原样编译成 OpenHarmony 动态库,ArkUI 页面作为验收宿主。

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

上游仓库: pearxteam/kasechange

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

kasechange 为 Kotlin Multiplatform 应用提供"字符串 ↔ 命名风格"这一件事的完整答案:"xml_http_request_v2_updated".toCamelCase() 一行得到 "xmlHttpRequestV2Updated",toCase(to, from) 覆盖任意格式互转,splitToWords 单独取分词结果,12 个便捷扩展(toSnakeCase / toPascalCase / toKebabCase …)开箱即用。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套能力——JSON snake_case 字段与鸿蒙侧 camelCase 属性互转是数据模型层最高频的需求之一,配置键、路由名、环境变量名、UI 文案(Title Case / Sentence case)都在这条管线上。但上游构建(multigradle 1.13.0 约定插件 / Kotlin 1.8.20 / Gradle 7.6.1 / JDK 8)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示转换结果,却无法证明转换引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证转换引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony 制品。
约定构建上游不声明任何 kotlin {} 目标——JVM/JS/Android/Native 目标全部由 net.pearx.multigradle.simple.project 按 gradle.properties 属性生成,Sonatype OSS + GitHub Packages + GPG + github-release 发布链也挂在上面;这套约定无法与鸿蒙工具链共存。
工具链过旧Kotlin 1.8.20 / Gradle 7.6.1 / JDK 8 连 HarmonyOS Kotlin 插件都解析不到,必须整体钉到 2.2.21-1.0.0 线。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:转换引擎的 ohos 原样编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。转换语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。

1.2 库提供的能力

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

  • 单格式转换(convert):任选来源分词器与 12 种目标格式,输入文本实时转换;
  • 分词(split):splitToWords 的独立出口,展示大小写突变 / 边界字符 / 数字分组三类切词行为;
  • 全格式一览(convertAll):一次 Native 调用返回 12 种格式的全部转换结果,单屏对照;
  • 分词行为切换:from 可选通用分词器(数字视作大写 / 数字不作大写)或任一 CaseFormat(每种格式的分隔符反过来就是切词边界——上游枚举同时实现 WordSplitter 的语义直接复用);
  • 状态行:每次桥调用的往返耗时实时显示,异常时展示桥返回的错误原文。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文与合法值列表。
resultconvert 返回目标格式转换结果。
wordssplit / convertAll 返回分词数组。
resultsconvertAll 返回全部 12 种格式的转换结果映射。

引擎侧完整能力(12 种格式、通用分词器、CaseFormatterConfig / WordSplitterConfig 自定义组合)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 71 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的转换逻辑没有漂移。

1.3 实现适配

维度要求
代码复用上游 6 个 commonMain 文件(约 317 行)与 5 个 commonTest 文件原样编译,零改动;全仓唯一新增 Kotlin 源文件是 example 里的桥接层 Bridge.kt。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块即根项目,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);multigradle、Sonatype、GitHub Packages、GPG、github-release 全部剥离,本 fork 不发 Maven Central。
桥接稳定使用 1 个 C ABI 和 UTF-8 JSON(3 个 kind),不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律上游本就零第三方依赖,无制品线问题;测试零改动全绿。
UI 完整单页覆盖全部桥接入口:输入、预设、来源分词、目标格式、分词结果、单格式结果、12 格式一览和状态行。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。kasechange 本身不碰 UI,适配面天然最小;ArkTS Demo 只是把桥暴露的转换管线画出来。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界(零依赖/零 expect)、确认交付物
第 2 阶段:目标与依赖打通 ── multigradle 拆除、根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版
第 3 阶段:桥协议建模     ── convert/split/convertAll JSON 契约、from 语义、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 输入联动、预设 chips、双选择器、分词行、结果卡片和全格式列表
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
src/commonMain/kotlin/net/pearx/kasechange/   上游转换引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64,零改动)
  CaseFormat.kt                              12 种格式枚举(同时实现 CaseFormatter + WordSplitter)+ universalWordSplitter 工厂
  StringExtensions.kt                        toCase ×3 / 12 个便捷扩展 / splitToWords ×2
  formatter/CaseFormatter(.Configurable).kt  重组接口 + 可配置实现
  splitter/WordSplitter(.Configurable).kt    分词接口 + 边界/大小写/数字分词算法
src/commonTest/kotlin/.../test/               上游 71 个用例(零改动,JVM 上运行)
example/nativeApp/                           Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                          DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md               鸿蒙适配说明(中英双份)
README.upstream.md                           上游原文保存

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :example:nativeApp,rootProject.name = "ohos_kasechange"——库本身就是根项目,src/ 目录保持与上游逐字一致(git rename 记录为零、blame 不断链)。上游的 multigradle 插件、github-release 插件、Sonatype OSS / GitHub Packages 发布段、GPG 签名全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Eclipse Temurin,org.gradle.java.home 钉路径)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(wrapper 从 7.6.1 升级;2.2.21 线要求 Gradle 8.x,与 qrose 同款)
依赖零第三方依赖(上游本就如此,无制品线问题)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.kasechange
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块文件数依赖形态处置
src/commonMain6纯 Kotlin + stdlib 多平台字符 API(uppercase/titlecase/isDigit),零第三方、零 expect/actual、零 UI、零 AGP整体编译(jvm + 双 ohos)
src/commonTest5kotlin.test 断言,无 mock 无 IO零改动保留(JVM 交叉验证)

expect 声明全库 0 处——这是本次适配与 KStore(1 处 expect,补 8 行 actual)最大的不同:kasechange 连平台接缝都没有,commonMain 可以不加一行代码直接进入 ohos 编译。过去挡住这类库适配的从来不是源码,而是构建约定。

1.4 页面边界
标题区          kasechange + KN + NAPI 徽标 / 副标题(net.pearx.kasechange · 字符串命名风格转换 · OpenHarmony 适配 Demo)
输入区          单行输入框(默认 xml_http_request_v2_updated,onChange 实时联动全页)
预设区          三个 chips:xml_http_request_v2_updated / atomgit.com/oh-tpc/kasechange / XMLHttpRequestV2
选择区          来源分词(from):Universal splitter / Universal (digits lower) / 任一 CaseFormat
                目标格式(to):12 种 CaseFormat
分词区          splitToWords 结果(xml | Http | Request | V2 | Updated)
结果区          单格式转换结果卡片(可复制) + 状态行(桥调用耗时 / 错误原文)
列表区          全部 12 种格式一览(每行展示名 + 转换结果,均可复制)

页面启动时 aboutToAppear 即发起首次 convertAll,之后任何输入 / 选择变化都实时刷新全部区块。

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

2.1 依赖盘点(零依赖的顺风局)

KStore 适配的成败手是"ohos kotlinx 制品线验证";kasechange 没有这道坎——gradle/libs.versions.toml 里只钉一行 HarmonyOS Kotlin 2.2.21-1.0.0,commonMain.dependencies {} 保持空。真正的障碍在第 2.2 节:multigradle 约定构建。

2.2 multigradle 拆除(本适配的主要工作量)

上游的目标平台声明不在自己的构建脚本里:settings.gradle.kts 声明 net.pearx.multigradle.simple.settings 插件,根 build.gradle.kts apply net.pearx.multigradle.simple.project 后按 gradle.properties 里的 kotlinVersion / compileSdkVersion / buildToolsVersion / junitJupiterVersion 等属性生成 JVM / JS / Android / Native 目标与测试矩阵,并挂上 maven-publish + signing + GitHub Packages + Sonatype OSS 发布链和 publishDevelop / publishRelease / release 任务。这套约定插件的最后一个版本停在 2023 年,不可能认识 ohosArm64()。

处置是整体替换而非打补丁:

// settings.gradle.kts(替换后全文)
pluginManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        google()
        mavenCentral()
    }
}

rootProject.name = "ohos_kasechange"

include(":example:nativeApp")

eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64——这是 qrose / KStore 两代适配沉淀的同一条纪律。

2.3 编译真实转换器

根 build.gradle.kts 收敛为"库即根项目"的极简 KMP 构建:

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

allprojects {
    group = "net.pearx.kasechange"
    version = providers.gradleProperty("version").orElse("1.4.1-ohos.1").get()
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)
    jvm()            // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {}
        commonTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

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

两处与模板仓库的细节差异:其一,qrose 的库模块是子项目,禁用 Ohos*Test* 任务写在 subprojects {} 里;kasechange 的库是根项目,subprojects 盖不住自己,必须写 allprojects——写错这半句,gradlew check 会在 ohos 测试任务上卡住。其二,Gradle wrapper 从 7.6.1 升到 8.14.1、JDK 从 8 升到 21(Temurin,路径钉进 gradle.properties),这是 2.2.21 工具链线的硬要求。实测:

./gradlew compileKotlinJvm compileKotlinOhosArm64 compileKotlinOhosX64   # BUILD SUCCESSFUL
./gradlew jvmTest                                                        # 71 项全绿

上游 6 个源码文件一个没动——git diff --stat -- src/ 为空是本次适配最硬的复用证据。

2.4 测试依赖的处置

commonTest 依赖只有 kotlin.test(ohos 制品随工具链提供),不存在 KStore 遇到的 turbine 缺制品问题。纪律仍然保留两条:测试统一在 JVM 跑,根构建全局禁用 Ohos*Test* 任务——ohos 侧只承担编译与链接。

第 3 阶段:桥协议建模

3.1 JSON 契约
net.pearx.kasechange/**(上游引擎,原样编译)
        ↓ CaseFormat / WordSplitter / 12 扩展函数
Bridge.kt(@CName × 1,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API kasechangeConvert()
        ↓
ArkUI Index.ets(单页滚动)

请求与响应:

请求 : {"kind":"convert","text":"xml_http_request_v2_updated","from":"LOWER_UNDERSCORE","to":"CAMEL"}
       {"kind":"split","text":"...","from":"UNIVERSAL"}
       {"kind":"convertAll","text":"...","from":"UNIVERSAL"}
响应 : {"ok":true,"result":"xmlHttpRequestV2Updated"}
       {"ok":true,"words":["xml","Http","Request","V2","Updated"]}
       {"ok":true,"words":[...],"results":{"UPPER_UNDERSCORE":"XML_HTTP_REQUEST_V2_UPDATED", ...共 12 项}}
       {"ok":false,"error":"unknown CaseFormat 'XXX'; expected one of UPPER_UNDERSCORE, ..."}

三个设计取向:from 可省略(默认 UNIVERSAL 通用分词器);convertAll 一次调用返回全 12 格式,Demo 单次 Native 调用喂满整页;错误信息携带全部合法值,外部集成方照抄即可。

3.2 from 语义为什么直接复用上游枚举

桥内分派这样写:

private fun parseSplitter(raw: String): WordSplitter = when (raw.trim().uppercase()) {
    "", "UNIVERSAL" -> universalWordSplitter()                    // 数字视作大写
    "UNIVERSAL_DIGITS_LOWER" -> universalWordSplitter(false)      // 数字不作大写
    else -> parseFormat(raw)                                      // CaseFormat 本身就是 WordSplitter
}

from 填 CaseFormat 名不是新发明:上游枚举同时实现了 CaseFormatter 与 WordSplitter,每种格式的分隔符反过来就是切词边界(LOWER_UNDERSCORE 按 _ 切、LOWER_HYPHEN 按 - 切)。桥透出 12 + 2 种分词行为,全部来自上游原件。

3.3 错误通道

导出函数整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("KasechangeConvert")
fun kasechangeConvert(request: String): String {
    return try {
        /* kind 分发 */
    } catch (t: Throwable) {
        """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
    }
}

非法 JSON、未知 kind / from / to,全部走同一个 ok:false 通道,ArkTS 侧统一渲染。值得一提的是转换本身是全函数——任意字符串(空串、中文、emoji、引号、换行)都有合法输出,Demo 的错误通道只服务于外部集成方的非法枚举名。

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

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libkasechange_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohoskasechange.so)
  │ KasechangeConvert
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;给 12 种格式各导一个 C 符号又会让版本演进变成灾难,所以选择单 C ABI + JSON kind 分发。

4.2 Kotlin/Native 导出函数
@CName("KasechangeConvert")
fun kasechangeConvert(request: String): String   // convert / split / convertAll 一口进
C ABI 符号ArkTS 方法覆盖内核
KasechangeConvertkasechangeConvert(req)CaseFormat.format(text, splitter) + splitToWords + universalWordSplitter 全家
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"kasechangeConvert", nullptr, NapiKasechangeConvert, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr}};

回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoskasechange_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()——顺序是先 napi_create_string_utf8 再释放。libohoskasechange_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。字符串取值用"先探长度再 std::vector<char>"两段式,规避 API 26 头文件的 const 签名变化。

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

add_library(kasechange_napi SHARED napi_init.cpp)
target_link_libraries(kasechange_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoskasechange)

OHOS_ARCH 为空或 libohoskasechange.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。IMPORTED_NO_SONAME TRUE 顺带挡住 Windows 盘符进 RUNPATH 的老坑。

export const kasechangeConvert: (request: string) => string;

第 5 阶段:ArkUI 宿主

5.1 一次调用喂满整页
private recalc(): void {
  const req: BridgeRequest = { kind: 'convertAll', text: this.text, from: this.fromName };
  const started = Date.now();
  const raw = kasechangeConvert(JSON.stringify(req));
  const res: BridgeResponse = JSON.parse(raw);
  if (res.ok) {
    this.words = res.words ?? [];
    this.allResults = res.results ?? {} as Record<string, string>;
    this.result = this.allResults[this.toName] ?? '';
    this.errorMsg = '';
  } else {
    this.errorMsg = res.error ?? 'unknown bridge error';
  }
  this.elapsedMs = `${Date.now() - started} ms`;
}

输入 onChange、from onSelect 都走 recalc();to onSelect 只做本地索引切换(12 格式结果已在 allResults 里),零额外 Native 调用。

5.2 页面交互

单页自上而下:标题区(kasechange + KN + NAPI 徽标)、输入框(实时联动)、三个预设 chips(含本仓库 AtomGit 地址)、from / to 双选择器、分词结果行(xml | Http | Request | V2 | Updated)、蓝色单格式结果卡片(可复制)与状态行(桥调用耗时 / 错误原文)、分割线、全部 12 种格式列表(每行展示名 + 结果,均可复制)。

5.3 对比度和错误边界

结果卡片深蓝字白底突出当前选择;12 格式列表浅灰卡片承载全景;ok:false 时错误原文进状态行。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoskasechange)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName × 1 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.kasechange、versionName 1.4.1-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/libohoskasechange_api.h
        ├── src/main/cpp/types/libkasechange_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面
6.2 构建、签名和安装
./gradlew jvmTest                                              # 71 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
ohpm install --all
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书材料写入本机,不进入仓库(仓库内 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.kasechange -m entry

四、完整代码对照

4.1 整体架构

net.pearx.kasechange/**(上游引擎原样编译)
    │ CaseFormat(12 格式,枚举即分词器)/ CaseFormatter / WordSplitter
    │ String.toCase / toCamelCase…×12 / splitToWords / universalWordSplitter
Bridge.kt + MiniJson
    │ KasechangeConvert(convert / split / convertAll)
libkasechange_napi.so
    │ kasechangeConvert
Index.ets
    │ JSON parse + @State 更新
    ▼
ArkUI 真机页面(单页滚动)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_kasechange、multigradle 移出
gradle/libs.versions.tomlHarmonyOS Kotlin 2.2.21-1.0.0 钉版(仅此一行依赖)
build.gradle.kts(根)库即根项目(jvm + 双 ohos)、版本 1.4.1-ohos.1、全局禁用 Ohos*Test* 任务
gradle.propertiesJDK 21 路径、version=1.4.1-ohos.1、KN 发行版走 Maven
gradle/wrapperGradle 8.14.1(7.6.1 升级)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt1 个 C ABI、三 kind 分发、MiniJson、错误通道
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鸿蒙适配主文档(18 节)

4.3 关键 API 对照

层次API作用
引擎String.toCase(to, from)任意格式互转(from 默认通用分词器)
引擎String.toCamelCase(from) …×12便捷扩展,一一对应 to 枚举
引擎String.splitToWords(splitter)分词出口
引擎universalWordSplitter(treatDigitsAsUppercase)通用分词器两种数字行为
引擎CaseFormat.entries12 种格式,枚举同时是分词器(from 直接复用)
引擎CaseFormatterConfig / WordSplitterConfig自定义组合(随编译进 .so,未单独桥接)
NativeKasechangeConvertC ABI JSON 导出(三 kind)
N-APIkasechangeConvert(req)向 ArkTS 暴露能力
ArkTSrecalc()一次 convertAll 喂全页 + 状态行刷新

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(kasechangeConvert(JSON.stringify({
  kind: 'convert',
  text: 'xml_http_request_v2_updated',
  from: 'LOWER_UNDERSCORE',
  to: 'CAMEL'
})));
// res.ok === true; res.result === 'xmlHttpRequestV2Updated'
val to = parseFormat(json.str("to"))                 // CaseFormat.CAMEL
val splitter = parseSplitter(json.str("from"))       // CaseFormat.LOWER_UNDERSCORE
to.format(text, splitter)                            // 上游原件,桥内零转换逻辑

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

五、关键决策说明

决策 1:真编译整库而不是 ArkTS 重写

命名转换的规格看似简单,实际分词器要处理「数字视作大写」「连续大写缩写(XMLHttp)」「首词不大写」等边角;ArkTS 重写等于引入第二份实现,漂移无人兜底。kasechange 全库零 expect、零依赖,连"内核提取"的前提都不存在——整个库就是内核,原样编译是唯一合理选项。

决策 2:工具链钉 2.2.21-1.0.0 线,Gradle 升 8.14.1

KStore 的结论是"版本跟着依赖走"(kotlinx 牵引升 2.4.20-0.1.0-07、Gradle 留 9.x);kasechange 零依赖、无牵引,选社区已过审面最大的稳定线:HarmonyOS Kotlin 2.2.21-1.0.0(qrose 同款),该线要求 Gradle 8.x,wrapper 从 7.6.1 升到 8.14.1,JDK 从 8 升到 21。上游源码是 Kotlin 1.8.20 写的纯稳定 API,升到 2.2.21 编译零告警——两条工具链线的经验不能互相套用,选线依据是依赖牵引方向。

决策 3:根构建替换 + 库留在根项目

multigradle 约定构建对鸿蒙没有保留价值,整体替换为极简 ohos 构建。与 KStore(多模块 include(":kstore"))不同,kasechange 上游是单模块仓库(src/ 在根),本 fork 不挪目录——src/ 与上游逐字一致、git 历史与 blame 不断链,代价只是根项目同时扮演库模块(kotlin {} 直接写在根 build.gradle.kts)。

决策 4:单 C ABI + 三 kind + MiniJson

12 种格式 × 若干分词器若各导一个 C 符号,每加一种格式就是一次 ABI 变更;单入口 JSON 分发让桥签名永不变化。MiniJson 继承 cmp_qrose 审核模式(扁平对象 + 引号状态机),本次补齐 \uXXXX / \n / \t 转义——字符串库的输入天然含任意字符,转义必须做满。

决策 5:jvmTest 交叉验证 + 测试零改动

转换逻辑在 commonMain,JVM 与 ohos 跑同一份代码;71 项 jvmTest 跑的就是上游原测试文件,是"内核没漂"的闸门。上游测试文件一个字节不动,交叉验证的有效性才成立。

六、测试与验证

6.1 测试环境

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

6.2 JVM 交叉验证

./gradlew jvmTest

71 项用例全部通过(上游原测试文件,零改动):

测试类用例数钉住什么
CaseFormatterTest1312 种格式逐一重组 + 自定义 CaseFormatterConfig
ComplexTest15端到端互转(snake_case → camelCase 等)+ 3 个自定义格式
UniversalWordSplitterTest15通用分词:大小写突变 / 边界字符 / 数字分组
UniversalWordSplitter2Test15universalWordSplitter(false):数字不作大写的行为矩阵
WordSplitterTest13各 CaseFormat 作分词器 + 自定义边界字符

Kotlin 1.8.20 → 2.2.21 升级没有 API 漂移:上游测试照跑全绿——这是"零依赖库 + 稳定 API"的直接证据。

6.3 HAP 验证

:compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
:example:nativeApp:linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: KasechangeConvert / libohoskasechange_symbols 均为全局 T 符号(双 ABI)
ohpm install --all: install completed
BuildNativeWithNinja: finished(双 ABI 编出 libkasechange_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

entry-default-unsigned.hap 约 5.3 MB(5,262,224 B),内含双 ABI 各三件套:KN so arm64 约 2.2 MB / x86_64 约 1.8 MB(debugShared 未 strip,含 Kotlin/Native 运行时)、libkasechange_napi.so 约 65 KB、libc++_shared.so 约 1.8 MB,另有 ArkTS 字节码 ets/modules.abc。链接期唯一告警 api.cpp:448 [-Wreturn-type] 来自社区 CAdapter 生成代码,与 qrose 路线完全一致。

6.4 功能用例

  1. 启动即自动完成首次 convertAll,无需点击;
  2. 默认输入 xml_http_request_v2_updated,结果卡片显示 xmlHttpRequestV2Updated(from=Universal splitter,to=camelCase);
  3. 分词行显示 xml | Http | Request | V2 | Updated——数字 V2 单独成组(数字视作大写行为);
  4. 切换 from 为 snake_case(LOWER_UNDERSCORE),分词行变为 xml | http | request | v2 | updated;
  5. 切换 from 为 Universal (digits lower),v2 并入小写行为;
  6. to 依次切换 12 种格式,结果卡片与「全部 12 种格式」列表同名行始终一致;
  7. 点预设 chip atomgit.com/oh-tpc/kasechange,URL 类字符串转换正常(点号边界 + 连续小写);
  8. 输入含空格、中文、emoji、引号的任意文本,永不报错(转换是全函数),状态行保持耗时显示;
  9. 每次桥调用往返耗时实时显示在状态行(亚毫秒量级)。

6.5 验证结论

转换引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的转换会话:任意输入实时转换、分词行为切换、单格式结果与 12 格式全景对照(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/kasechange.git
cd kasechange
export JAVA_HOME="/path/to/jdk-21"
./gradlew compileKotlinOhosArm64 compileKotlinOhosX64                          # 引擎 ohos 编译
./gradlew jvmTest                                                              # 71 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
ohpm install --all
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.kasechange -m entry
hdc shell hilog | grep Kasechange

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让上游 src/commonMain 真正编译成 ohosArm64 / ohosX64 动态库;
  2. multigradle 约定构建是本类仓库最大的适配障碍:目标声明藏在插件里,settings 阶段就读 gradle.properties 的 Android 属性——只能整体替换,不能打补丁;
  3. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  4. String.format 是 JVM 专属扩展:桥里写 "\\u%04x".format(code) 在 JVM 编译通过、Kotlin/Native 直接报 None of the following candidates is applicable(还列出一串无关的 CaseFormatter.format 重载误导定位)——控制字符转义改手写 HEX 拼接一次过;
  5. 本 SDK 的 Select 组件没有 fontSize 属性:CompileArkTS 报 Property 'fontSize' does not exist on type 'SelectAttribute',删掉即过(Text 的 fontSize 不受影响);
  6. 库在根项目时,禁用 Ohos*Test* 任务的规则必须写 allprojects 而不是 subprojects——后者盖不住根项目自己,gradlew check 会在 ohos 测试任务上卡住;
  7. 命令行 ohpm install 先报 Failed to find the executable 'node':需要把 DevEco 自带 node(tools/node)加进 PATH 并设 NODE_HOME;
  8. hvigorw assembleHap 走到 PackageHap 报 spawn java ENOENT:打包 JVM 用 DevEco 自带 JBR(jbr/bin 加进 PATH),系统旧 JDK 会让 hvigor 半路报不相干的错;
  9. DevEco 打开工程后自动生成 .clang-tidy / .clangd,模板仓库不跟踪这类工具缓存——补进 gitignore 再提交,否则混进提交;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free(),且要先 napi_create_string_utf8 再释放;
  12. CMake 的 IMPORTED 库要配 IMPORTED_NO_SONAME TRUE + $ORIGIN rpath,否则 Windows 盘符进 RUNPATH,模拟器 dlopen 解析失败。

8.2 已知问题

  • 交付形态是「KN so + N-API + HAP Demo」,不是 ohm 形式的 HAR 包(社区 KMP 库统一走 so 路线,与 qrose / KStore 一致);
  • 桥透出 12 种预定义格式 + 两种通用分词行为 + 任一格式作分词器;CaseFormatterConfig / WordSplitterConfig 自定义组合只在 Kotlin 消费侧可用,ArkTS 侧要自定义格式需在桥里加 kind(向后兼容扩展);
  • MiniJson 只解析扁平对象(一层 string:string),不支持嵌套——桥协议本身用不到嵌套;
  • ohosTest 目录未建:社区路线的测试基线是 JVM commonTest + 设备端 Demo 验收;
  • 上游 JS / WASM / Android / iOS 等目标随 multigradle 拆除移出构建(源文件留在树里),本 fork 的支持面是 jvm + 双 ohos;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.kasechange 匹配的签名。

8.3 未来优化方向

  • 桥增加自定义格式 kind(透出 CaseFormatterConfig / WordSplitterConfig 的连接符与大小写矩阵);
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建(check_only.yml 已加双 ABI 链接);
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 kasechange(100% commonMain,零依赖零 expect)
    → multigradle 约定构建整体拆除(本适配的主要工作量)
    → 工具链对齐 HarmonyOS Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 + JDK 21
    → 根构建替换(cmp_qrose 模式,库即根项目,src/ 零改动)
    → ohosArm64 / ohosX64 动态库(唯一新增 Kotlin 是 example 里的 Bridge.kt)
    → C ABI(@CName × 1)+ 三 kind JSON 分发
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析
    → ArkUI 真机单页验收(实时转换 / 分词切换 / 12 格式全景)

9.2 封装层次

  • 引擎层:上游原版 CaseFormat、CaseFormatter(Factory)、WordSplitter 与 12 个字符串扩展;
  • Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、预设与选择器联动、结果渲染与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 零依赖库的适配面等于构建面:kasechange 源码零改动,全部工作量落在 multigradle 拆除、工具链钉版和桥接层——判断一个 KMP 库的适配成本,先看 expect 数量和依赖树,两者皆空时构建就是全部;
  2. 工具链选线看依赖牵引:KStore 被 kotlinx 牵引升到 2.4.20-0.1.0-07(Gradle 留 9.x),kasechange 无牵引选 qrose 稳定线 2.2.21-1.0.0(Gradle 必须降到 8.14.1)——同一套方法论,两种结论,选线依据永远是"谁在牵引版本"而不是历史惯例;
  3. 字符串库的验收特征是"全函数 + 逐字对照":71 项 JVM 用例钉住引擎行为,真机侧任何输入(含中文、emoji、引号)都不允许报错,12 格式列表与 JVM 输出逐字一致——这是"编码器 100% 复用"的可验证表达。

9.4 适配成果

当前适配已完成:

  • kasechange 1.4.1 真实转换引擎的双 ABI Kotlin/Native 编译(上游 commonMain / commonTest 文件零改动,git diff --stat -- src/ 为空);
  • multigradle 约定构建到社区 ohos 构建的完整替换(cmp_qrose 模式,HarmonyOS Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 + JDK 21);
  • 1 个 C ABI 符号(三 kind 分发)和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 单页验收页面(实时转换、分词切换、12 格式全景、状态行);
  • 71 项 JVM 交叉验证全绿(上游原测试文件)+ HAP 构建(约 5.3 MB,双 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐