开源鸿蒙平台 KMP 三方库「kasechange」适配全流程
本文记录
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 Kotlin2.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 带原文与合法值列表。 |
result | convert 返回目标格式转换结果。 |
words | split / convertAll 返回分词数组。 |
results | convertAll 返回全部 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 Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Eclipse Temurin,org.gradle.java.home 钉路径) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 从 7.6.1 升级;2.2.21 线要求 Gradle 8.x,与 qrose 同款) |
| 依赖 | 零第三方依赖(上游本就如此,无制品线问题) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.kasechange |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | 文件数 | 依赖形态 | 处置 |
|---|---|---|---|
src/commonMain | 6 | 纯 Kotlin + stdlib 多平台字符 API(uppercase/titlecase/isDigit),零第三方、零 expect/actual、零 UI、零 AGP | 整体编译(jvm + 双 ohos) |
src/commonTest | 5 | kotlin.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 方法 | 覆盖内核 |
|---|---|---|
KasechangeConvert | kasechangeConvert(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.toml | HarmonyOS Kotlin 2.2.21-1.0.0 钉版(仅此一行依赖) |
build.gradle.kts(根) | 库即根项目(jvm + 双 ohos)、版本 1.4.1-ohos.1、全局禁用 Ohos*Test* 任务 |
gradle.properties | JDK 21 路径、version=1.4.1-ohos.1、KN 发行版走 Maven |
gradle/wrapper | Gradle 8.14.1(7.6.1 升级) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、三 kind 分发、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面 |
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.entries | 12 种格式,枚举同时是分词器(from 直接复用) |
| 引擎 | CaseFormatterConfig / WordSplitterConfig | 自定义组合(随编译进 .so,未单独桥接) |
| Native | KasechangeConvert | C ABI JSON 导出(三 kind) |
| N-API | kasechangeConvert(req) | 向 ArkTS 暴露能力 |
| ArkTS | recalc() | 一次 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 项用例全部通过(上游原测试文件,零改动):
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
CaseFormatterTest | 13 | 12 种格式逐一重组 + 自定义 CaseFormatterConfig |
ComplexTest | 15 | 端到端互转(snake_case → camelCase 等)+ 3 个自定义格式 |
UniversalWordSplitterTest | 15 | 通用分词:大小写突变 / 边界字符 / 数字分组 |
UniversalWordSplitter2Test | 15 | universalWordSplitter(false):数字不作大写的行为矩阵 |
WordSplitterTest | 13 | 各 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 功能用例
- 启动即自动完成首次
convertAll,无需点击; - 默认输入
xml_http_request_v2_updated,结果卡片显示xmlHttpRequestV2Updated(from=Universal splitter,to=camelCase); - 分词行显示
xml | Http | Request | V2 | Updated——数字V2单独成组(数字视作大写行为); - 切换 from 为
snake_case(LOWER_UNDERSCORE),分词行变为xml | http | request | v2 | updated; - 切换 from 为
Universal (digits lower),v2并入小写行为; - to 依次切换 12 种格式,结果卡片与「全部 12 种格式」列表同名行始终一致;
- 点预设 chip
atomgit.com/oh-tpc/kasechange,URL 类字符串转换正常(点号边界 + 连续小写); - 输入含空格、中文、emoji、引号的任意文本,永不报错(转换是全函数),状态行保持耗时显示;
- 每次桥调用往返耗时实时显示在状态行(亚毫秒量级)。
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 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让上游
src/commonMain真正编译成ohosArm64/ohosX64动态库; - multigradle 约定构建是本类仓库最大的适配障碍:目标声明藏在插件里,
settings阶段就读gradle.properties的 Android 属性——只能整体替换,不能打补丁; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;String.format是 JVM 专属扩展:桥里写"\\u%04x".format(code)在 JVM 编译通过、Kotlin/Native 直接报None of the following candidates is applicable(还列出一串无关的CaseFormatter.format重载误导定位)——控制字符转义改手写 HEX 拼接一次过;- 本 SDK 的
Select组件没有fontSize属性:CompileArkTS报Property 'fontSize' does not exist on type 'SelectAttribute',删掉即过(Text的fontSize不受影响); - 库在根项目时,禁用
Ohos*Test*任务的规则必须写allprojects而不是subprojects——后者盖不住根项目自己,gradlew check会在 ohos 测试任务上卡住; - 命令行
ohpm install先报Failed to find the executable 'node':需要把 DevEco 自带 node(tools/node)加进 PATH 并设NODE_HOME; hvigorw assembleHap走到PackageHap报spawn java ENOENT:打包 JVM 用 DevEco 自带 JBR(jbr/bin加进 PATH),系统旧 JDK 会让 hvigor 半路报不相干的错;- DevEco 打开工程后自动生成
.clang-tidy/.clangd,模板仓库不跟踪这类工具缓存——补进 gitignore 再提交,否则混进提交; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(),且要先napi_create_string_utf8再释放; - CMake 的 IMPORTED 库要配
IMPORTED_NO_SONAME TRUE+$ORIGINrpath,否则 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 三条经验
- 零依赖库的适配面等于构建面:kasechange 源码零改动,全部工作量落在 multigradle 拆除、工具链钉版和桥接层——判断一个 KMP 库的适配成本,先看 expect 数量和依赖树,两者皆空时构建就是全部;
- 工具链选线看依赖牵引:KStore 被 kotlinx 牵引升到
2.4.20-0.1.0-07(Gradle 留 9.x),kasechange 无牵引选 qrose 稳定线2.2.21-1.0.0(Gradle 必须降到 8.14.1)——同一套方法论,两种结论,选线依据永远是"谁在牵引版本"而不是历史惯例; - 字符串库的验收特征是"全函数 + 逐字对照":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。
参考文档
更多推荐



所有评论(0)