开源鸿蒙平台 KMP_CMP 三方库「UUID」适配全流程
本文记录
uuid(com.benasher44:uuid)适配开源鸿蒙(OpenHarmony)平台的完整过程,覆盖 KMP/CMP 工程盘点、ohosArm64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、签名 HAP 和真机验收。本次适配复用 Kotlin 侧的 UUID 解析、格式化、字节转换、版本/variant 判断、名称 UUID 和 v4 随机生成逻辑,再由 ArkTS 通过 N-API 消费。这样验证的是同一份 KMP 代码在 OpenHarmony ARM64 设备上的运行结果,而不是重新写一套只在页面里生效的 UUID 逻辑。
项目地址: AtomGit/oh-tpc/uuid
开发工具: 华为云码道
一、背景
1.1 为什么做开源鸿蒙平台 KMP/CMP 适配
UUID 是 Kotlin Multiplatform 生态的基础三方库。应用生成订单号、请求 ID、追踪标识时都需要它。如果业务直接在 ArkTS 里手写一个随机字符串函数,Android、iOS、桌面和鸿蒙四端会出现四种实现,RFC 4122 的版本位和变体位也可能各自不一致。
如果只把页面重新写成 ArkTS,页面可能会显示几个"生成 UUID"按钮,却无法证明共享 Kotlin 模型、Native 动态库和真机随机源已经连通。适配需要解决下面几个问题:
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 上游 com.benasher44:uuid 覆盖 JVM、JS、Wasm、Apple、Linux 和 Windows,默认没有 ohosArm64(),无法生成 OpenHarmony KLIB 和动态库。 |
| 工具链不一致 | Kotlin/Native 2.2.21-1.0.0、OpenHarmony LLVM、Native SDK 和 Gradle 插件必须使用匹配版本。 |
| 随机源边界 | uuid4() 在 JVM 使用 SecureRandom,在 Native 平台读取 /dev/urandom;必须在真机上验证 OpenHarmony 随机源可用。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin data class,必须经过 C ABI、C++ N-API 和 JSON。 |
| 符号导出失控 | 动态库默认导出全部 Kotlin/Native 运行时符号,需要 linker version script 收敛 ABI。 |
| 交付链路复杂 | Native 动态库、CMake、HAP、签名、设备安装和 ARM64 依赖需要分别验证。 |
因此,本项目把适配边界放在三个地方:Kotlin/Native 目标配置、C ABI/N-API 桥接层和 ArkUI 页面层。UUID 模型仍由 KMP 维护,ArkTS 只负责页面状态和 JSON 解析。
1.2 库提供的能力
uuid 公共模块提供以下能力:
Uuid:128 位不可变 UUID 值类型,携带最高/最低 64 位;uuidFrom:把 RFC 4122 文本解析为类型安全的Uuid;toString:输出规范的小写 36 字符格式;bytes:UUID 与 16 字节数组互转;version/variant:读取 RFC 4122 版本位与变体位;uuid4():使用平台随机源生成版本 4 UUID,OpenHarmony Native 上来自/dev/urandom;nameBasedUuidOf(namespace, name, hasher):传入 MD5 得到 v3、传入 SHA-1 得到 v5 的名称 UUID。
关键行为约定如下:
| API | 约定值 | 含义 |
|---|---|---|
uuid4().version | 4 | 版本 4 随机 UUID |
uuid4().variant | 2 | RFC 4122 变体 |
uuid4().bytes.size | 16 | 128 位等于 16 字节 |
uuidFrom(nil).version | 0 | nil UUID 用于边界测试 |
未知输入在解析层直接抛出异常,页面显示错误文本,不会把非法字符串当成 UUID 继续传播。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | UUID 解析、格式化、字节转换、版本/variant、名称 UUID 和 v4 随机由 Kotlin 共享。 |
| 平台目标 | 为 uuid 模块和示例加入 ohosArm64,生成 libuuid.so。 |
| 桥接稳定 | 使用少量 C ABI 函数和 JSON,避免把 Kotlin 对象地址交给 ArkTS。 |
| UI 完整 | 页面支持随机生成、读取当前、版本/variant 展示和 8 项自检。 |
| 可测试 | JVM 测试、Native 链接、HAP 构建、设备安装分别验收。 |
| 签名安全 | 源码工程保持未签名,签名工程由脚本复制到本机后配置。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面是独立的真机验收宿主,公共 API 仍然保持平台无关。其他 KMP/CMP 应用可以继续依赖
com.benasher44:uuid,再自行决定如何展示和消费 UUID。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 KMP 模块、UUID 公共模型和 OpenHarmony 示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、独立 example 工程和 Native 构建任务
第 3 阶段:门面与 JSON ── 建立 UuidExamples、确定性目录、随机示例和 JSON 契约
第 4 阶段:原生桥接 ── Kotlin/Native C ABI、C++ N-API、内存释放和类型检查
第 5 阶段:宿主页面封装 ── ArkTS UuidClient、ArkUI 页面状态和生命周期
第 6 阶段:示例与验证 ── HAP 构建、签名、设备安装和效果图
每个阶段都使用真实产物作为下一阶段输入:先用 JVM 验证共享模型,再把相同代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,最后安装签名 HAP 观察真机生成的 UUID。
三、逐步实现过程
第 1 阶段:项目初始化
1.1 盘点公共 API 和工程边界
项目采用与参考 CMP 工程一致的分层:
uuid/ KMP UUID 模型、解析、随机和名称 UUID
vico/ 与参考工程一致的库聚合层
sample/ android/desktop/shared/web/ios 主机入口
example/shared/ 示例门面和 JVM 验收测试
example/nativeApp/ ohosArm64 Kotlin/Native 动态库
example/ohosApp/ DevEco Stage 工程和 ArkUI 页面
scripts/ Native、HAP 和签名工程辅助脚本
docs/openharmony/ 验收记录和真机效果图
guide/ 集成指南
example 是独立 Gradle 工程,不把 DevEco 工程当作 Kotlin 子模块。这样可以分别执行 Gradle 和 Hvigor,也可以把 ohosApp 复制到另一个目录后在 DevEco Studio 中配置签名。
1.2 固定工具链和版本矩阵
本项目使用以下版本约定:
| 项目 | 配置 | 用途 |
|---|---|---|
| Kotlin Multiplatform | 2.2.21-1.0.0 | JVM、Kotlin/Native 和 KLIB |
| JDK | 21 | Gradle、Kotlin 编译和 Native 任务 |
| OpenHarmony 目标 | ohosArm64 | ARM64 真机动态库 |
| DevEco product | 6.0.0(20) | 工程兼容和目标版本 |
| ABI | arm64-v8a | HAP 原生库架构 |
| 随机源 | /dev/urandom | uuid4() 的 Native 实现 |
执行 Gradle 脚本前先选择 JDK 21:
export JAVA_HOME="/path/to/jdk-21"
java -version
/dev/urandom 是否返回随机字节还要在真机上单独验证,编译成功只说明工程可以安装,不能替代随机源检查。
1.3 创建 OpenHarmony 示例目录
示例页面没有把 UUID 能力伪装成普通列表,而是围绕"当前 UUID、版本、variant、来源和检查状态"组织:
标题区 UUID 工作台 / Kotlin Multiplatform / OpenHarmony
状态徽标 NATIVE READY
UUID 区 当前 UUID random、1f6c4ff5-6cf2-4f6e-b823-6d59d33c0d6f
属性区 VERSION 4 / VARIANT 2 / SOURCE libuuid.so
操作区 重新读取 / 读取当前
自检区 8/8 公共检查通过
页面只展示一条随机生成的 UUID;确定性目录(包含 nil 全零值)保留给桥接测试,不渲染成多条 UI 记录。
第 2 阶段:目标与依赖打通
2.1 加入 ohosArm64 目标
uuid/build.gradle.kts 在保留全部既有目标的同时加入 ohosArm64():
kotlin {
explicitApi()
js(IR) { browser(); nodejs() }
jvm()
wasmJs { d8() }
wasmWasi { nodejs() }
// Apple、Linux、Windows 目标保持不变
mingwX64()
linuxX64()
linuxArm64()
ohosArm64()
// ...
}
JVM 等既有目标让库可以继续按原坐标发布;ohosArm64 则把同一份 commonMain 代码编译成 OpenHarmony KLIB。
2.2 Native focused build 的作用
example/nativeApp 构建一个受 linker map 约束的 shared library:
kotlin {
ohosArm64 {
binaries.sharedLib {
baseName = "uuid"
linkerOpts(
"--entry=0",
"--version-script=${project.file("src/ohosArm64Main/linker/shared-library.map")}",
)
linkerOpts("-lace_napi.z", "-luv", "-lhilog_ndk.z")
}
}
}
链接器只导出五个 C ABI 符号:
UuidCatalog
UuidGet
UuidRandom
UuidRunChecks
UuidFree
这样 ArkTS 只能通过明确的边界获取 UUID 和自检结果,Kotlin/Native 内部实现不会变成不受控的 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/")
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
mavenLocal()
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
}
}
共享示例通过 Maven 坐标消费已发布的 uuid 产物(根工程 GROUP=com.benasher44、VERSION=0.8.4-ohos.1,先执行 ./gradlew publishToMavenLocal):
sourceSets {
commonMain.dependencies {
api("com.benasher44:uuid:${property("uuidVersion")}")
}
}
如果业务工程与库在同一个多模块仓库内,也可以写成项目依赖:
commonMain.dependencies {
implementation(project(":uuid"))
}
2.4 通过构建产物消费共享库
prepareOhos 在 Native 链接成功后复制动态库和 C 头文件:
val prepareOhos by tasks.registering(Copy::class) {
dependsOn("linkDebugSharedOhosArm64")
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libuuid.so")
into("libs/arm64-v8a")
}
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libuuid_api.h")
into("src/main/cpp/include")
}
into(rootProject.layout.projectDirectory.dir("../example/ohosApp/entry"))
}
动态库、生成头文件和 HAP 属于构建产物,仓库通过 .gitignore 排除它们;每台开发机都可以从源码重新生成与自身 SDK 匹配的文件。
第 3 阶段:门面与 JSON 契约
3.1 为什么需要统一 JSON 契约
ArkTS、C++ 和 Kotlin/Native 不能直接共享 Kotlin 对象,因此边界使用 UTF-8 JSON:
uuid4() on OpenHarmony
↓ /dev/urandom
UuidExamples.random()
↓ UuidExample
C ABI UuidRandom()
↓ const char*
N-API randomUuid()
↓ JSON string
ArkUI page
页面只消费 JSON,不复制版本位和变体位的判断逻辑。这样 JVM 测试和设备页面使用同一套 uuid4() 与 version/variant 规则。
3.2 UuidExamples 门面
公共示例门面位于 example/shared/src/commonMain:
public object UuidExamples {
private val templates: List<String> = listOf(
"00000000-0000-0000-0000-000000000000",
"f47ac10b-58cc-4372-a567-0e02b2c3d479",
"550e8400-e29b-41d4-a716-446655440000",
)
public fun catalog(): List<UuidExample> = templates.mapIndexed { index, value ->
example("template-$index", uuidFrom(value))
}
public fun get(index: Int): UuidExample {
require(index in templates.indices) { "Unknown UUID example index: $index" }
return example("template-$index", uuidFrom(templates[index]))
}
public fun random(): UuidExample = example("random", uuid4())
}
templates[0] 是 UUID nil 值,专门覆盖解析、版本和 variant 的边界情况;random() 每次调用 uuid4() 生成新值。
3.3 JSON 结构
JSON 边界保持字段稳定:
{
"id": "random",
"value": "1f6c4ff5-6cf2-4f6e-b823-6d59d33c0d6f",
"version": 4,
"variant": 2
}
自检结果返回:
{
"passed": true,
"checks": ["three deterministic UUID templates are available", "..."]
}
所有字符串经过 quote 处理,避免错误信息或未来扩展字段破坏 JSON。
3.4 自检和错误边界
UuidExamples.runChecks() 覆盖八项检查:
- 三条确定性 UUID 模板可用;
- RFC 4122 文本往返一致;
- UUID 字节为 16 个 8 位组;
- variant 与 version 可读取;
- 随机 UUID 使用 version 4;
- 非法索引被拒绝;
- 公共 API 无平台类型;
- OpenHarmony 桥接只使用不可变 JSON 值。
Native 异常会转换为 {"error":"..."},C++ 在创建 ArkTS 字符串后立即调用 UuidFree。这样页面能显示可读错误,同时不会泄漏 Kotlin/Native 堆内存。
第 4 阶段:原生桥接(技术难点)
4.1 Kotlin/Native 对象不能直接交给 ArkTS
Kotlin/Native 的 Uuid 和 UuidExample 属于 Kotlin 运行时对象。ArkTS 只能接收 JavaScript 值,C++ 不能把 Kotlin 对象地址直接当成 JavaScript 对象。
最终采用三层桥接:
ArkTS
│ JSON string
▼
C++ N-API entry
│ const char*
▼
Kotlin/Native C ABI
│ UuidExamples
▼
UTF-8 JSON + explicit free
4.2 方案对比
| 方案 | 优点 | 缺点 | 选用 |
|---|---|---|---|
| 直接导出 Kotlin 对象 | 代码少 | ABI、生命周期和类型不可控 | ❌ |
| 只导出字节指针 | 实现简单 | 页面要自行拼装格式化逻辑 | ❌ |
| C ABI + JSON | 边界清晰、易扩展、易调试 | 有一次序列化开销 | ✅ |
| 在 ArkTS 重写完整模型 | 页面调用简单 | KMP 和 ArkTS 逻辑容易分叉 | ❌ |
4.3 Kotlin/Native 导出函数
@CName("UuidCatalog")
public fun catalogNative(): CPointer<ByteVar> = response {
UuidExamples.catalog().joinToString(prefix = "[", postfix = "]") { it.toJson() }
}
@CName("UuidGet")
public fun uuidNative(index: Int): CPointer<ByteVar> = response {
UuidExamples.get(index).toJson()
}
@CName("UuidRandom")
public fun randomUuidNative(): CPointer<ByteVar> = response {
UuidExamples.random().toJson()
}
@CName("UuidRunChecks")
public fun checksNative(): CPointer<ByteVar> = response {
val checks = UuidExamples.runChecks()
"{\"passed\":true,\"checks\":[${checks.joinToString { quote(it) }}]}"
}
@CName("UuidFree")
public fun freeNative(pointer: CPointer<ByteVar>?) {
if (pointer != null) nativeHeap.free(pointer.rawValue)
}
返回值使用 Native heap 分配的、以 0 结尾的 C 字符串。每一块返回缓冲区都由 UuidFree 释放;response 包装会把 Kotlin 异常转成 {"error":"..."}。
4.4 C++ N-API 方法分发
C++ 注册四个 ArkTS 方法:
napi_property_descriptor methods[] = {
{"getCatalog", nullptr, Catalog, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"getUuid", nullptr, GetUuid, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"randomUuid", nullptr, RandomUuid, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
napi_default, nullptr},
};
getUuid 会检查参数数量、整数类型、非负范围,再调用 UuidGet。所有方法都遵循"调用 Native、创建 ArkTS 字符串、释放 Native 缓冲区"的顺序。
CMake 将 Kotlin/Native 动态库作为 imported library:
add_library(uuid SHARED IMPORTED)
set_target_properties(uuid PROPERTIES
IMPORTED_LOCATION
"${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libuuid.so")
add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE uuid libace_napi.z.so)
4.5 N-API 生命周期
ArkTS randomUuid()
│
▼
UuidRandom()
│
▼
napi_create_string_utf8(...)
│
▼
UuidFree(nativeBuffer)
│
▼
return JS string
C++ 负责完成跨语言字符串转换和 Native 释放,ArkTS 页面不需要知道 Kotlin/Native 的堆实现。
第 5 阶段:宿主页面封装
5.1 ArkTS 类型化客户端
系统桥接的 TypeScript 封装在 UuidClient.ets:
import uuidNative from 'libentry.so';
export interface UuidExample {
id: string;
value: string;
version: number;
variant: number;
}
export function randomUuid(): UuidExample {
return JSON.parse(uuidNative.randomUuid()) as UuidExample;
}
export function runChecks(): UuidChecks {
return JSON.parse(uuidNative.runChecks()) as UuidChecks;
}
ArkTS 只做 JSON 解析和类型声明,不写任何版本位判断。
5.2 模块声明与权限
uuid4() 只依赖系统随机源,不访问传感器或账户数据,因此宿主 entry/src/main/module.json5 不需要声明任何 requestPermissions。 Ability 配置保持最小:
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"exported": true
}
]
这也说明适配范围判断的重要性:能力属于系统事件的应用(如握姿监听)需要权限,纯算法库适配则应避免多余声明。
5.3 ArkUI 页面状态
Index.ets 保存当前 UUID、检查状态和错误文本:
@State private selected: UuidExample = emptyUuid();
@State private status: string = '正在加载';
@State private errorText: string = '';
aboutToAppear(): void {
this.refresh();
}
private refresh(): void {
try {
this.selected = randomUuid();
const checks: UuidChecks = runChecks();
this.status = checks.passed ? `${checks.checks.length}/8 公共检查通过` : '公共检查失败';
this.errorText = '';
} catch (error) {
this.status = 'Native UUID 加载失败';
this.errorText = String(error);
}
}
页面加载时自动执行一次 randomUuid + runChecks,属性区直接渲染 JSON 里的 version 和 variant,不写 if (code === 4) 这样的业务映射。
5.4 页面交互预设
页面提供两个操作按钮:
- 重新读取:生成新的 v4 UUID 并重新执行 8 项自检;
- 读取当前:只生成新的 UUID,用于观察每次调用都返回不同值。
异常路径下,按钮会把错误文本显示在页面底部,状态点从绿色变为红色,便于在真机上直接定位 Native 侧问题。
第 6 阶段:示例与验证
6.1 example 工程结构
example/
├── shared/
│ ├── src/commonMain/.../UuidExamples.kt
│ └── src/commonTest/.../UuidExamplesTest.kt
├── nativeApp/
│ ├── src/ohosArm64Main/.../NativeBridge.kt
│ └── src/ohosArm64Main/linker/shared-library.map
└── ohosApp/
├── AppScope/
├── entry/src/main/cpp/
│ ├── CMakeLists.txt
│ └── napi_init.cpp
└── entry/src/main/ets/
├── pages/Index.ets
└── uuid/UuidClient.ets
shared 验证公共模型,nativeApp 产生 Native 动态库,ohosApp 负责 ArkUI 页面和 N-API 调用。三者的边界清晰,任何一层失败都能单独定位。
6.2 原生模块注册
napi_init.cpp 通过共享库构造函数注册 entry 模块:
static napi_module uuidModule = {1, 0, nullptr, Init, "entry", nullptr, {0}};
extern "C" __attribute__((constructor)) void RegisterUuidModule() {
napi_module_register(&uuidModule);
}
类型声明位于:
example/ohosApp/entry/src/main/cpp/types/libentry/index.d.ts
ArkTS 使用:
import uuidNative from 'libentry.so';
6.3 Native 动态库准备
执行:
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
脚本依次执行根模块测试、示例测试、linkDebugSharedOhosArm64 和 prepareOhos。成功后生成:
example/ohosApp/entry/libs/arm64-v8a/libuuid.so
example/ohosApp/entry/src/main/cpp/include/libuuid_api.h
还可以检查 ARM64 ELF 的强依赖:
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
6.4 构建、签名和安装
未配置签名时,可以在 DevEco Studio 构建未签名 HAP;真机安装需要签名 HAP。先把源码工程复制成本机签名工程:
python3 scripts/prepare-signing-project.py /path/to/uuid-signing
在复制后的工程里配置证书、profile、p12 和密码,然后执行:
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
scripts/build-hap.sh /path/to/uuid-signing
产物位于:
entry/build/default/outputs/default/entry-default-signed.hap
安装并启动:
hdc list targets -v
hdc -t <设备序列号> install -r \
/path/to/uuid-signing/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b com.benasher44.uuid.sample
签名证书、profile、p12 和密码只保存在本机,不要提交到 AtomGit。
四、完整代码对照
4.1 整体架构
OpenHarmony ArkUI page
│ randomUuid()
▼
UuidClient.ets -> libentry.so
│ N-API
▼
UuidRandom / UuidRunChecks
│ C ABI
▼
libuuid.so
│ Kotlin/Native
▼
UuidExamples -> uuid4() -> /dev/urandom -> JSON
4.2 文件清单
| 文件 | 职责 |
|---|---|
uuid/src/commonMain/kotlin/uuid.kt | Uuid 值类型、解析、uuid4()、version/variant |
uuid/src/nativeMain/kotlin/urandom.kt | Native 平台 /dev/urandom 随机源 |
example/shared/.../UuidExamples.kt | 示例门面、确定性目录和八项自检 |
example/nativeApp/.../NativeBridge.kt | C ABI、JSON 返回和内存释放 |
example/ohosApp/.../UuidClient.ets | N-API JSON 解析和类型封装 |
example/ohosApp/.../Index.ets | 真机预览页面 |
example/ohosApp/entry/src/main/cpp/napi_init.cpp | N-API 导出和参数检查 |
example/nativeApp/src/ohosArm64Main/linker/shared-library.map | C ABI 符号白名单 |
scripts/build-openharmony.sh | 测试、Native 链接和产物复制 |
scripts/build-hap.sh | 已配置签名工程的 HAP 构建 |
scripts/prepare-signing-project.py | 复制未签名工程到本机签名目录 |
docs/openharmony/VALIDATION.md | 自动检查和真机验收记录 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| Kotlin | uuid4() | 平台随机源生成 v4 UUID |
| Kotlin | uuidFrom | RFC 4122 文本转 Uuid |
| Kotlin | Uuid.version / Uuid.variant | 读取版本位与变体位 |
| Kotlin | nameBasedUuidOf | v3/v5 名称 UUID |
| Native | UuidRandom | 返回一个随机 UUID JSON |
| Native | UuidRunChecks | 执行八项自检 |
| N-API | randomUuid | 向 ArkTS 暴露随机方法 |
| ArkTS | UuidClient.randomUuid | JSON 解析为 UuidExample |
4.4 ArkTS 与 Kotlin 的边界
ArkTS 只负责界面状态和 JSON 解析:
this.selected = randomUuid();
this.status = `${checks.checks.length}/8 公共检查通过`;
Kotlin 负责 UUID 语义和随机源:
public fun random(): UuidExample = example("random", uuid4())
两者之间只传输 JSON 字符串,不传输 Kotlin 对象、ArkTS class 实例或未校验的动态结构。
五、关键决策说明
决策 1:把 ohosArm64 加入公共构建约定
UUID 是跨端基础库,只有真正链接 ohosArm64 动态库,才能证明共享 Kotlin 代码可以进入 OpenHarmony 运行时,而不是只停留在 JVM 测试。
决策 2:独立消费者必须通过构建产物消费
example 单独解析 com.benasher44:uuid,ohosApp 单独接收 .so 和头文件,避免根工程编译通过却无法在 DevEco 中打包。
决策 3:JSON 作为跨语言数据契约
JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,并允许后续增加可信字段而不暴露内部对象布局。
决策 4:version script 只开放五个 C ABI 入口
目录、按索引获取、随机、自检和释放已经覆盖示例所需能力;符号白名单可以降低 Native 生命周期和兼容风险,新增符号时先改 shared-library.map 再链接。
决策 5:随机页面和确定性目录分开
页面只展示 randomUuid() 的结果;确定性目录(含 nil UUID)用于桥接测试和边界用例。两者分开后,页面语义保持单一,也避免把全零 UUID 误当成生成失败。
决策 6:把库验证和设备验证分开
JVM 测试验证 UUID 规则,Native 链接验证 ABI,ELF 检查验证依赖,Hvigor 验证 HAP,真机验证随机源和页面。每一层都有明确的失败边界。
六、测试与验证
6.1 测试环境
本次真机验证使用:
- macOS;
- JDK 21;
- Kotlin Multiplatform
2.2.21-1.0.0; - DevEco Studio 6 及 OpenHarmony ARM64 Native SDK;
- 已签名
entry-default-signed.hap; - USB 连接的 HarmonyOS ARM64 真机(HUAWEI Mate 60 Pro);
hdc设备序列号FMR0223825079397。
6.2 静态检查与单元测试
./gradlew :uuid:jvmTest :sample:shared:jvmTest
(cd example && ./gradlew :shared:jvmTest)
测试覆盖确定性目录、RFC 4122 文本往返、16 字节长度、version/variant 位、非法索引拒绝和随机 UUID 非 nil。
6.3 原生桥接和 HAP 验证
./scripts/build-openharmony.sh
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
scripts/build-hap.sh /path/to/uuid-signing
验证结果:
libuuid.so: 0 unresolved strong imports
Hvigor BUILD SUCCESSFUL
entry-default-signed.hap generated
6.4 功能验证用例
用例 1:默认页面和自检
启动应用后页面显示"UUID 工作台"和 8/8 公共检查通过。当前 UUID 由 Native randomUuid() 生成。
用例 2:重新读取
点击"重新读取",页面生成一条新的 v4 UUID,8 项自检重新执行。连续点击每次值都不同,证明随机源工作正常。
用例 3:读取当前
点击"读取当前",只刷新 UUID 值,页面状态提示"已生成新的 Kotlin/Native UUID"。
用例 4:版本与 variant
属性区显示 VERSION 4、VARIANT 2、SOURCE libuuid.so,与 RFC 4122 约定一致。
用例 5:UUID 格式
页面 UUID 为 36 字符小写文本,连字符符合 8-4-4-4-12 位置,可直接复制用于业务联调。
用例 6:错误边界
Native 侧异常会以 {"error":"..."} 返回,页面底部显示红色错误文本,应用不会崩溃;重新构建 Native 库并覆盖安装后恢复。
6.5 验证结论
自动测试、Kotlin/Native ARM64 链接、ELF 依赖检查、Hvigor HAP 构建、签名安装和真机页面自检均已完成。真机页面已显示随机生成的版本 4 UUID,说明从 /dev/urandom 到 KMP 模型再到 ArkTS 页面的链路可用。
七、运行效果
7.1 真机截图

截图中可以看到:
- 页面标题为"UUID 工作台";
- 副标题说明 Kotlin Multiplatform / OpenHarmony;
- 右上角显示
NATIVE READY徽标; - 当前 UUID 为 random
1f6c4ff5-6cf2-4f6e-b823-6d59d33c0d6f; VERSION 4、VARIANT 2、SOURCE libuuid.so;- 操作区提供"重新读取"和"读取当前"两个按钮;
8/8 公共检查通过;- 底部说明"数据由 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)
# 设备依赖检查
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
# 复制本机签名工程并构建 HAP
python3 scripts/prepare-signing-project.py /path/to/uuid-signing
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
scripts/build-hap.sh /path/to/uuid-signing
# 安装和启动
hdc -t <设备序列号> install -r \
/path/to/uuid-signing/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b com.benasher44.uuid.sample
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配:必须把共享模型真正编译成
ohosArm64动态库。 - N-API 不负责业务判断:C++ 只做类型检查、字符串转换和释放,UUID 语义放在 Kotlin。
- version script 必须覆盖全部 C 符号:新增
UuidRandom后如果shared-library.map没有同步,HAP 链接阶段会报undefined symbol。 - 头文件和动态库必须同源:
libuuid_api.h和libuuid.so必须来自同一次prepareOhos,否则签名不一致会导致运行异常。 - 签名配置需要绑定产品:
products[].signingConfig必须指向signingConfigs,否则 Hvigor 会继续生成未签名 HAP。
8.2 已知问题
- 当前只打包
arm64-v8a,没有 32 位或 x86 模拟器库,模拟器调试需要另行补充; - 确定性目录(含 nil UUID)不上屏,只用于桥接测试,页面语义保持单一;
- 文章中的签名配置只适用于本地开发机,不能直接复制到其他环境;
- 当前示例是单页面生成模型,多页面应用需要在业务层集中管理 Native 客户端。
8.3 未来优化方向
- 增加跨平台随机源
expect/actual抽象,让测试可以注入确定性随机序列; - 将 UUID 生成封装为
Flow<Uuid>,减少业务层轮询; - 为 Compose Multiplatform 页面提供 UUID 卡片和复制交互示例;
- 增加 release/shared 模式 Native 构建和体积对比;
- 在持续集成中加入
ohosArm64链接和 HAP 未签名构建任务。
九、总结
9.1 核心难点回顾
本次适配真正需要处理的不是一个 uuid4() 调用,而是一条完整跨端链路:
OpenHarmony /dev/urandom
→ Kotlin/Native uuid4()
→ C ABI
→ C++ N-API
→ JSON
→ ArkTS UuidClient
→ ArkUI 真机页面
9.2 封装层次
- KMP 层:定义稳定的 UUID 解析、格式化、版本/variant 和随机生成;
- Native 层:生成 ARM64 动态库,并通过 version script 输出有限入口;
- N-API 层:完成参数检查、字符串转换和内存释放;
- ArkTS 层:管理 JSON 解析、页面生命周期、错误展示和视觉呈现;
- DevEco 层:完成 CMake、HAP、签名、安装和运行。
9.3 三条经验
- 先让共享模型在 JVM 和 Native 通过,再接入 ArkUI;
- 用 JSON 和少量 C ABI 代替跨语言对象传递;
- 把自动测试、HAP 构建和真机随机源分别记录,避免把"编译成功"误认为"设备行为正确"。
9.4 适配成果
当前 uuid 已完成:
ohosArm64目标与libuuid.so动态库;uuid4()真机随机生成(version 4 / variant 2);- JVM 和 OpenHarmony ARM64 共用的八项自检;
- Kotlin/Native + C ABI + N-API 桥接;
重新读取 / 读取当前真机页面与效果图;- 签名 HAP 构建、设备安装和真机验收记录;
- 与参考 CMP 工程一致的模块和脚本组织;
- AtomGit 项目文档和 OpenHarmony 验收记录。
参考文档
更多推荐



所有评论(0)