开源鸿蒙平台 CMP 三方库「carbon-compose」适配全流程
本文记录
carbon-compose接入 OpenHarmony 的完整过程,覆盖纯 UI 型 CMP 库的资产盘点、设计 token 数据层抽取、真实注册表编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
carbon-compose是 IBM Carbon Design System 的 Compose Multiplatform 非官方实现:按钮、标签、开关、通知、日期选择器、下拉等一整套组件,在 Android / iOS / Desktop / Wasm 上用同一套 Carbon 设计 token 着色与定尺。本次适配与社区早期路线(qrose / haze / PreCompose / voyager 的"零依赖内核提取")一脉相承但切入点不同:carbon-compose 的纯 Kotlin 资产不是算法,而是设计系统的数据模型——4 套主题、每套 299 个颜色 token、26 个字样、13 档间距、6 档动效时长与 6 条缓动曲线,全部来自上游:code-gen消费的@carbon/themes 11.75.0JSON。因此不需要重写组件、也不需要提取内核——真实的 token 数据被原样编译成 OpenHarmony KLIB 与双 ABI 动态库,一个完整的设计 token 引擎运行在真机上,ArkUI 目录页作为验收宿主;Compose 组件层原样留在磁盘上,等社区 CMP 鸿蒙线追上上游版本的那天再接回去。
项目地址: AtomGit/oh-tpc/carbon-compose.git
上游仓库: gabrieldrn/carbon-compose
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
carbon-compose 为 Compose Multiplatform 应用提供"IBM Carbon 设计语言"这一件事的完整答案:20 多个组件、4 套颜色主题、成体系的排版与间距刻度,上游铁律是绝不引入 Material3——一切颜色、间距、字体都必须来自 token。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套设计语言,但上游构建(Kotlin 2.4.0 / Gradle 9.6.1 / AGP 9.2.1 / Compose Multiplatform 1.11.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS、色值手抄进页面,页面可以显示色块,却无法证明公共设计 token 引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证 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 构建;约定插件 carbon.kmp.library 在配置期就会申请 AGP。 |
| UI 层跨不过去 | 组件层约 285 处 @Composable 全部站在 androidx.compose.* 上;社区鸿蒙 CMP 线是 1.9.2 / Kotlin 2.2.21,承载不了上游 Compose 1.11.1 的 API。 |
| 数据载体是 Compose 类型 | 颜色 token 生成物 import androidx.compose.ui.graphics.Color,排版用 TextStyle,间距用 Dp——ohos 编译器看不到这些类型。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:设计 token 数据层的 ohos 编译(数据值逐字节来自上游 JSON)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。组件渲染语义由 ArkUI 验收页复刻,token 数据完全由上游数据源负责。
1.2 库提供的能力
Demo 提供一个真实的设计 token 会话,对应桥接的全部入口:
- 元信息握手(meta):版本号、上游版本、数据源标注、四套主题名、各类 token 计数——全部来自真实 Native 调用而非静态文字;
- 主题加载(theme):一套主题的全量颜色(192 核心 token + button / tag / notification / ai / chat 五个分组共 299 个,
#AARRGGBB字符串逐字节来自@carbon/themesJSON)与深浅色标志; - 排版刻度(typography):26 个 Carbon type set 的家族 / 字号 / 行高 / 字重 / 字距;
- 间距与动效(spacing / motion):13 档间距、6 档动效时长、6 条贝塞尔缓动曲线、6 档按钮高度。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
op / name | 回显本次操作与目标主题(white / g10 / g90 / g100)。 |
colors 等 | 原生 JSON 对象嵌入,ArkTS JSON.parse 一次到位,不做双重转义。 |
isDark | 主题深浅色标志,驱动页面整体换肤。 |
引擎侧完整能力(四套主题全部 299×4 = 1196 个颜色值、字样 / 间距 / 动效刻度)全部在注册表里随编译进入 .so,JVM 侧保留 8 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的数据没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | token 数据值逐字节来自上游 :code-gen 的输入 JSON(code-gen/src/main/resources/*.json,一字未动);注册表由可复现生成器产出,上游 Compose 模块零改动留盘。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);:carbon / :catalog 等上游模块留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | token 模块零第三方依赖(连 kotlinx 都不引),不存在制品线覆盖问题;桥沿用 cmp_qrose 审核通过的 MiniJson。 |
| UI 完整 | 单页覆盖全部桥接入口:主题切换、色板、排版、间距动效、组件预览、版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
carbon-compose的组件层(285 处@Composable)在当前鸿蒙工具链线上无法编译,适配面天然落在数据层;ArkTS Demo 是把桥暴露的 token 引擎画出来——每一个色块、每一行字的取色都来自 Native 调用返回的真实 token 值。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 token 数据源、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:token 注册表 ── 可复现生成器、纯 Kotlin 数据模型、零依赖 KMP 模块
第 4 阶段:零漂移闸门 ── jvmTest 与上游 JSON 逐值比对(1196 处颜色值)
第 5 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主 ── 主题切换、五 Tab 目录、组件复刻和真机验收
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
carbon-tokens/ 新增:纯 Kotlin token 注册表(进 ohos 构建,jvm + ohosArm64 + ohosX64)
src/commonMain/.../CarbonTokenData.kt 生成的注册表(1433 行)
src/commonTest/.../CarbonTokenDataValidationTest.kt 零漂移闸门
src/commonTest/.../TestJsonParser.kt 测试专用迷你 JSON 解析器
tools/generate-registry.ps1 可复现生成器
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
code-gen/src/main/resources/*.json 上游数据源(原样保留,生成器与测试都读它)
carbon/ carbon-common/ carbon-test/ 上游 Compose 库模块 —— 留盘,不进构建
catalog/ catalog-android/ iosCatalog/ 上游演示应用 —— 留盘,不进构建
build-logic/ buildSrc/ config/ docs/ 上游构建设施 —— 留盘,不被引用
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :carbon-tokens 与 :example:nativeApp,rootProject.name = "ohos_carbon"。上游的 dokka / vanniktech 发布 / binaryCompatibilityValidator / AGP classpath / foojay 解析器全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin,org.gradle.java.home 写进 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(上游 9.6.1 wrapper 替换降级;2.2.21 线不支持 Gradle 9.x) |
| 依赖 | carbon-tokens 零第三方依赖,无制品线要求 |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.carbon |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | 规模 | 依赖形态 | 处置 |
|---|---|---|---|
carbon | 218 个 .kt,285 处 @Composable | compose runtime / foundation / ui / animation / resources + kotlinx-datetime + kermit | 留盘不进构建(组件层,工具链对不齐) |
carbon-common | 2 个 .kt | compose semantics | 留盘不进构建 |
carbon-test | 1 个 .kt | compose uiTest | 留盘不进构建 |
code-gen | 纯 JVM 生成器 + 4 份主题 JSON | kotlinpoet / serialization | 代码留盘;JSON 成为新模块的数据源 |
doc-parser | Kotlin/JS | @carbon/themes npm 包 | 留盘不进构建(JSON 的上游产线,保持不动) |
catalog / catalog-android | KMP 演示应用 | navigation / koin / settings | 留盘不进构建 |
expect/actual 盘点:全库仅 5 处(4 个尺寸枚举 + 1 个 rememberIsButtonPressed),平台耦合度极低——但它们是组件层的 expect,不影响 token 数据层;真正挡住 ohos 编译的是 Compose 运行时本身。
值得单独说明的是 foundation 层的资产形态:颜色主题(4 × 299 token)、排版刻度(26 个 type set)、间距(13 档)、动效(6 时长 + 6 缓动)、按钮高度(6 档)的值全部平台无关,只有载体类型(Color / TextStyle / Dp / CubicBezierEasing)是 Compose 的。这一刀切下去,适配面就从"285 个组件"缩小到"一张数据表"。
1.4 页面边界
标题区 CARBON DESIGN SYSTEM / carbon-compose · OpenHarmony 验收目录
主题区 White / Gray 10 / Gray 90 / Gray 100 四选一(切换即整页换肤)
Tab 栏 色板 / 排版 / 间距动效 / 组件 / 关于
色板页 13 个命名分区 + 其它,每 token 一个色块 + 名称 + 十六进制值
排版页 26 个 type set 按真实字号 / 行高 / 字重 / 字距渲染,附规格标注
间距动效页 13 档间距条、6 档时长、6 条缓动控制点、6 档按钮高度
组件页 按钮(6 变体 × 3 尺寸)、10 色标签、复选 / 单选 / 开关、进度条、4 态通知
状态行 最近一次操作结果(如"White 已加载 · 192 核心 token 来自 Kotlin/Native 注册表")
关于页 版本握手、数据源、token 计数、架构分层、验证链路、AtomGit 链接
页面启动时先调 carbonVersion() 做握手,再依次 dispatch meta / theme / spacing / typography / motion 拉全量数据。
第 2 阶段:目标与依赖打通
2.1 数据源验证(本适配的成败手)
KStore 适配先验证 kotlinx 制品线;carbon-compose 的对应动作是验证 token 数据源的唯一性与可达性。上游颜色 token 的产线是:doc-parser(Kotlin/JS)从 npm 包 @carbon/themes 11.75.0 提取 4 份 JSON → :code-gen(纯 JVM)在构建期把 JSON 生成 Compose Color 类。4 份 JSON 就在仓库里:
code-gen/src/main/resources/white.json 192 核心 token + 5 个组件分组
code-gen/src/main/resources/g10.json 结构与 white 完全一致
code-gen/src/main/resources/g90.json 深色主题
code-gen/src/main/resources/g100.json 深色主题
实测每份 JSON:192 个扁平颜色 token("background":"#FFFFFFFF" 这类)+ 5 个嵌套分组(buttonColors 15、tagColors 40、notificationColors 10、aiColors 21、chatColors 21),四套结构完全一致,合计每主题 299 个、四主题 1196 个颜色值。三个关键结论:其一,这是上游唯一的颜色数据源,直接消费它比手抄任何色表都更接近上游语义;其二,:code-gen 的生成物 import Compose 类型,ohos 编译器看不到,所以不能复用它的生成器,但可以复用它的输入;其三,JSON 里的值就是 #AARRGGBB 字符串,天然可跨语言,不需要任何转换。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
社区批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的)。carbon-compose 上游是 Kotlin 2.4.0 源,但进 ohos 构建的新模块(carbon-tokens、example/nativeApp)只用了标准库语法,2.2.21 完全吃得下;上游源码本来就整体留盘不编译,不存在"读不了 2.4 元数据"的问题——这一点与 KStore 不同(KStore 的真实引擎依赖 coroutines 1.11.0-0.1.0-07,被 stdlib 元数据反推到 2.4.20-0.1.0-07)。2.2.21 线的代价是 Gradle 必须钉 8.14.1(不支持 9.x),上游 9.6.1 wrapper 整体替换。
settings.gradle.kts 把 eazytec 仓库写在最前:
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()
}
}
rootProject.name = "ohos_carbon"
include(":carbon-tokens")
include(":example:nativeApp")
2.3 编译真实 token 注册表
carbon-tokens/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {} // 零依赖,标准库之外什么都没有
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
注册表本体 CarbonTokenData.kt(1433 行)由 tools/generate-registry.ps1 从 4 份 JSON 确定性生成:颜色值逐字节保留(键排序、值不转不换),spacing / typography / motion / 按钮高度从上游 foundation 源文件转录并写明出处。实测:
./gradlew :carbon-tokens:compileKotlinJvm :carbon-tokens:compileKotlinOhosArm64 :carbon-tokens:compileKotlinOhosX64 # BUILD SUCCESSFUL
2.4 测试的处置:注册表必须对照 JSON 逐值验证
数据表最容易出的问题不是编译失败,而是悄悄漂移——上游改了主题 JSON、注册表没跟上,页面照样能跑,颜色已经错了。处置是把"零漂移"做成测试(jvmTest),并沿用社区纪律:测试统一在 JVM 跑,根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
这样测试代码零妥协地保留在 commonTest,ohos 侧只承担编译与链接(详见 6.2)。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.gabrieldrn.carbon.tokens/**(纯 Kotlin 注册表)
↓ CarbonTokenData.themes / typeStyles / spacing / motion…
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API carbonDispatch(request) / carbonVersion()
↓
ArkUI Index.ets(五 Tab 目录页)
请求与响应:
请求 : {"op":"meta"}
{"op":"theme","name":"white"} // white / g10 / g90 / g100
{"op":"spacing"} {"op":"typography"} {"op":"motion"}
响应 : {"ok":true,"op":"theme","name":"g90","isDark":true,
"colors":{…192 项…},"buttons":{…},"tags":{…},
"notifications":{…},"ai":{…},"chat":{…}}
{"ok":false,"error":"…"}
两个不对称设计:请求是扁平键值对(桥内 MiniJson 70 行搞定,不引 kotlinx.serialization);响应是原生 JSON 嵌入——颜色值本来就是 #AARRGGBB 字符串,直接拼进响应对象,ArkTS JSON.parse 一次拿到 Record,读写两条路都不做双重转义。
3.2 载荷类型为什么固定 Map<String, String>
注册表不用任何业务模型包装颜色:token 名到十六进制字符串的映射原样暴露。这意味着桥不管 Carbon 的 token 语义——调用方(ArkTS)按 token 名取值、直接喂给 .backgroundColor() 这类接受字符串的 API;上游未来增删 token 名,桥零改动,注册表重生成即可。这等价于 KStore 的 T = JsonElement 决策:桥不绑定 schema,数据等价性交给源头与测试。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("CarbonDispatch")
fun carbonDispatch(request: String): String = try {
/* op 分发 */
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
非法 JSON、不支持的 op、未知主题名,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libcarbon_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoscarbon.so)
│ CarbonDispatch / CarbonVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制 token 语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("CarbonDispatch")
fun carbonDispatch(request: String): String // meta / theme / spacing / typography / motion 一口进
@CName("CarbonVersion")
fun carbonVersion(): String = CarbonTokenData.FORK_VERSION // 版本握手
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
CarbonDispatch | carbonDispatch(req) | 全部 5 个 op:注册表查询与 JSON 序列化 |
CarbonVersion | carbonVersion() | fork 版本握手(真机版本行的数据来源) |
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"carbonDispatch", nullptr, NapiCarbonDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"carbonVersion", nullptr, NapiCarbonVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoscarbon_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoscarbon_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohoscarbon SHARED IMPORTED)
set_target_properties(ohoscarbon PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(carbon_napi SHARED napi_init.cpp)
target_link_libraries(carbon_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoscarbon)
OHOS_ARCH 为空或 libohoscarbon.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const carbonDispatch: (request: string) => string;
export const carbonVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 启动握手和主题加载
aboutToAppear(): void {
this.version = carbonVersion(); // 版本握手:来自真实 Native 调用
this.loadMeta();
this.loadTheme('white');
this.loadSpacing();
this.loadTypography();
this.loadMotion();
}
private loadTheme(name: string): void {
const res = JSON.parse(this.dispatch('theme', name)) as ThemeResponse;
if (!res.ok) { this.status = `theme ${name} 失败: ${res.error ?? ''}`; return; }
this.colors = res.colors; /* …五个分组、深浅色标志… */
this.status = `${label} 已加载 · ${Object.keys(res.colors).length} 核心 token 来自 Kotlin/Native 注册表`;
}
取色统一走 c(key, fallback) 帮助函数:this.colors['background'] 这类 Record 索引直接作为 ArkUI 的 ResourceColor 字符串使用,非法主题返回 ok:false 进状态行,Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行与日志。
5.2 页面交互
五 Tab 自上而下:色板(13 个命名分区 + 其它的过滤 chips,每 token 色块 + 名称 + 十六进制值)、排版(26 个 type set 用 .fontSize(style.fontSize).fontWeight(…).lineHeight(…).letterSpacing(…) 实尺寸渲染,附 名称 · 家族 字号/行高 · 字重 标注)、间距动效(间距条实宽 = token dp、时长表、缓动控制点、按钮高度表)、组件(按钮 6 变体 × 3 尺寸、10 色标签、复选 / 单选 / 开关、双进度条、4 态内联通知)、关于(版本握手行、数据源、token 计数、ArkTS → NAPI → C ABI → Kotlin 注册表 → JSON 分层说明与仓库 / 社区链接)。
主题切换是页面的"验收戏眼":顶部四选一切换后,页面背景、文字、卡片、边框全部取自新主题的 background / textPrimary / layer01 / borderSubtle01……深色主题(g90 / g100)整页反转,证明主题引擎数据真的来自 so。
5.3 对比度和错误边界
组件页配色严格对齐 token 语义:Primary 品牌蓝白字、Danger 红、Tertiary 透明底描边、Disabled 灰底灰字;通知四态用 notificationBackground* 配 support* 圆点;ok:false 时错误原文进状态行。所有交互控件(复选 / 单选 / 开关)的选中色取 interactive token,深浅主题下自动跟随。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoscarbon)+ copyKnSo* 回填
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 2 + MiniJson(继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.carbon、versionName 0.7.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/libohoscarbon_api.h
├── src/main/cpp/types/libcarbon_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :carbon-tokens:jvmTest # 8 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp(注意是子目录,不是仓库根)
# File → Project Structure → Signing Configs 勾选自动签名后:
# Build → Build Hap(s) / App(s)
签名在 DevEco Studio 中勾选自动生成,证书写入本机 ~/.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.carbon -m entry
四、完整代码对照
4.1 整体架构
code-gen/src/main/resources/*.json(上游唯一颜色数据源,一字未动)
│ tools/generate-registry.ps1(可复现生成)
carbon-tokens(纯 Kotlin 注册表,jvm + ohosArm64 + ohosX64 → KLIB)
│ CarbonTokenData.themes / typeStyles / spacing / motion
Bridge.kt + MiniJson
│ CarbonDispatch / CarbonVersion
libcarbon_napi.so
│ carbonDispatch / carbonVersion
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机目录页(五 Tab + 整页换肤)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_carbon、上游模块移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 单条目钉版 |
build.gradle.kts(根) | 版本 0.7.1-ohos.1、全局禁用 Ohos*Test* 任务 |
gradle/wrapper/* | Gradle 降 8.14.1(沿 cmp_qrose wrapper) |
carbon-tokens/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,零依赖 |
carbon-tokens/tools/generate-registry.ps1 | 可复现生成器(读 code-gen JSON + foundation 常量) |
carbon-tokens/src/commonMain/.../CarbonTokenData.kt | 生成的注册表(1433 行,4 主题 × 299 token + 全部刻度) |
carbon-tokens/src/commonTest/.../CarbonTokenDataValidationTest.kt | 零漂移闸门(8 项用例) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、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 验收页面(五 Tab) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(20 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 注册表 | CarbonTokenData.themes | 4 套主题 × 299 颜色 token(#AARRGGBB 逐字节来自上游 JSON) |
| 注册表 | CarbonTokenData.typeStyles | 26 个 type set(家族 / 字号 / 行高 / 字重 / 字距) |
| 注册表 | CarbonTokenData.spacing / motionDurations / motionEasings | 13 档间距、6 档时长、6 条缓动 |
| 注册表 | CarbonTokenData.buttonHeightsDp | 按钮高度刻度(32/40/48/48/64/80) |
| Native | CarbonDispatch / CarbonVersion | C ABI JSON 导出 |
| N-API | carbonDispatch(req) / carbonVersion() | 向 ArkTS 暴露能力 |
| ArkTS | loadTheme(name) | 拉取主题并驱动整页换肤 |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(this.dispatch('theme', 'g90')) as ThemeResponse;
// res.ok === true;res.colors['interactive'] === '#FF4589FF'
// 页面背景 .backgroundColor(this.c('background'))、正文 .fontColor(this.c('textPrimary'))
val theme = CarbonTokenData.themes.getValue(name)
buildString {
append("\"colors\":")
append(stringMap(theme.colors)) // {"background":"#FF161616",…} 原生嵌入
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:复用数据而不是复用生成器
上游 :code-gen 已经把 JSON 变成 Kotlin——但生成物 import androidx.compose.ui.graphics.Color。改它的生成器为"双口味"要动上游模块;本 fork 的选择是只读它的输入 JSON,生成器重写为 60 行 PowerShell(确定性输出,签入仓库)。上游模块保持零改动,数据等价性交给逐值测试——上游改主题的那一天,重跑生成器加测试全绿,就是同步完成的证明。
决策 2:工具线选 2.2.21-1.0.0 + Gradle 8.14.1,而不是跟随上游 2.4 / 9.6.1
进 ohos 构建的新模块只用标准库,旧工具线完全够;而 2.2.21 线是社区已批准的钉法(qrose / haze)。代价是 Gradle 从上游 9.6.1 降到 8.14.1——上游构建本来就整体让位,这个代价不存在。与 KStore 的选择(被 coroutines 元数据反推到 2.4.20-0.1.0-07、Gradle 留 9.x)相反,原因很简单:token 注册表零依赖,没有 kotlinx 制品线拽着版本走。
决策 3:根构建替换(cmp_qrose 模式),上游模块全部留盘
上游根构建(dokka / vanniktech / apiValidator / AGP classpath / build-logic 约定插件)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。:carbon 等 Compose 模块、:catalog 演示应用、docs/ 文档站全部留盘不进 settings.gradle.kts——它们的存在方式只有这一种:等社区 CMP 鸿蒙线追上上游 Compose 版本,再原样接回。
决策 4:载荷固定 Map<String, String>,桥不绑定 schema
token 名到十六进制字符串的映射原样过桥:上游增删 token,桥零改动;ArkTS 按 token 名取值直接喂给接受字符串的颜色 API。业务语义(哪个 token 用在哪)留在 ArkTS 页面——那本来就是"设计系统的消费者"该待的位置。
决策 5:jvmTest 交叉验证 + 测试零妥协
数据表适配最大的风险是漂移而不是崩溃。8 项 jvmTest 把注册表与上游 JSON 逐值比对(1196 处颜色值 + 键集合全等 + 全部刻度断言),跑的是上游数据源的原始文件——"零漂移"不是声明,是每一行颜色值上的断言。ohos 侧只承担编译与链接,测试统一 JVM,与社区纪律一致。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :carbon-tokens:jvmTest
8 项用例全部通过(tests="8" skipped="0" failures="0" errors="0"),测试代码自带的 90 行迷你 JSON 解析器直接读上游 code-gen/src/main/resources/*.json:
| 测试类用例 | 钉住什么 |
|---|---|
theme colors match upstream json verbatim | 4 主题 × (192+15+40+10+21+21) = 1196 处颜色值逐值比对 + 六组键集合全等 |
token names are identical across the four themes | 四套主题 token 名单一致 |
dark and light themes are flagged correctly | white/g10 浅色、g90/g100 深色 |
spacing scale matches upstream SpacingScale | 13 档 2/4/8/…/160 |
type styles match upstream CarbonTypography | 26 个字样全字段 |
motion durations and easings match upstream Motion | 6 时长 + 6 缓动控制点 |
button heights match upstream ButtonSize | 32/40/48/48/64/80 |
fork metadata is stable | 版本号 / 数据源标注 |
6.3 HAP 验证
:carbon-tokens:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: CarbonDispatch / CarbonVersion / libohoscarbon_symbols 均为全局 T 符号(双 ABI)
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs
CompileArkTS: finished(首轮 Row/flexWrap 报错修复后通过)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
双 ABI 产物:libohoscarbon.so arm64 约 2.7 MB、x86_64 约 2.2 MB(含 Kotlin/Native 运行时与全部 token 数据),加各自 libc++_shared.so(约 1.9 / 1.7 MB)与 CMake 产出的 libcarbon_napi.so。
6.4 功能用例
- 启动后"关于"Tab 出现版本握手
0.7.1-ohos.1(来自真实 Native 调用,非静态文字); - 状态行出现「White 已加载 · 192 核心 token 来自 Kotlin/Native 注册表」;
- 色板 Tab 逐分区展示色块:背景区第一个色块为 White 主题
background = #FFFFFFFF,交互蓝interactive = #FF0F62FE与 Carbon 品牌蓝一致; - 切换 Gray 90 / Gray 100,整页换肤为深色,
background变#FF161616/#FF262626,interactive变#FF4589FF——主题引擎数据真实来自 so; - 排版 Tab 26 个字样按真实字号 / 行高 / 字重渲染,规格标注与渲染一致;
- 间距 Tab 间距条实际宽度与标注 dp 一致(2/4/8/…/160);
- 组件 Tab 按钮、10 色标签、4 态通知的取色与色板中的 buttonColors / tagColors / notificationBackground* 分组逐一对得上;
- 杀进程重启,再次切换主题数据依旧(注册表静态编译在 so 内,无网络依赖);
hdc shell hilog | grep CarbonNapi可见每次 dispatch 的字节数日志。
6.5 验证结论
token 注册表、序列化边界、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的设计 token 会话:版本握手、四主题整页换肤、色板逐值渲染与组件复刻(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/carbon-compose.git
cd carbon-compose
export JAVA_HOME="/path/to/jdk-21"
./gradlew :carbon-tokens:compileKotlinOhosArm64 :carbon-tokens:compileKotlinOhosX64 # klib
./gradlew :carbon-tokens:jvmTest # 8 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp,自动签名后:
# Build → Build Hap(s) / App(s)
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.carbon -m entry
hdc shell hilog | grep CarbonNapi
八、遗留问题与改进方向
8.1 踩坑复盘
- 纯 UI 库的适配面要重新定义:挡在 ohos 前面的不是平台 API,是 Compose 运行时;此时该盘点的是"数据资产在哪",而不是"哪些文件能编"——carbon-compose 的答案是 code-gen 的 4 份 JSON;
settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Kotlin 不允许对象成员之间用逗号:生成器最初给每个
public val成员收尾写了,(模仿 mapOf 条目风格),编译器在每个成员处报 “Property getter or setter expected”;成员级收尾必须是裸); - Int 字面量不会自动转 Double 参数:
letterSpacingSp = 0、p1y = 0必须生成0.0,生成器为此加了Format-Double; - 原始字符串里的多层嵌套模板在 KN 编译器上解析失败:
"""…${jsonString("… ${x} …")}…"""两层嵌套连锁炸出 60+ 个错误;先把插值提为局部变量、保持原始字符串内单层模板(qrose 的桥一直是单层写法,不是巧合); - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); entry/libs/是 gitignore 的,clone 后必须先跑 link 任务回填三件套,否则 DevEco 的 CMake 阶段直接FATAL_ERROR: Missing libohoscarbon.so;- DevEco 打开的是
example/harmonyApp子目录,不是仓库根——仓库根是 Gradle 工程,两套构建互不认识; .flexWrap()是 Flex 的属性、不存在于 Row:ArkTS 首轮编译在真机构建时才暴露(Property 'flexWrap' does not exist on type 'RowAttribute');需要换行的横向容器直接用Flex({ wrap: FlexWrap.Wrap }),子项间距用 margin;- ArkTS 严格模式下对象字面量依赖上下文类型:三元表达式里两份字面量可能失去推断,显式声明类型再逐字段赋值最稳;
- Windows 上并跑多个仓库构建时 Gradle daemon 偶发 native OOM 崩溃(
arena.cpp),停掉闲置 daemon 重试即可,与仓库配置无关; - link 时的 CAdapter clang 警告(
api.cpp … non-void function does not return a value in all control paths)来自 HarmonyOS Kotlin 生成的胶水,不是业务代码,so 照常产出(与 qrose 记录一致)。
8.2 已知问题
- Compose 组件层(285 处
@Composable)不跨桥:社区鸿蒙 CMP 线(1.9.2 / Kotlin 2.2.21)承载不了上游 Compose 1.11.1;组件以 ArkTS 复刻验收,Compose 源码留盘待工具链对齐; - 上游 12 个
ImageBitmap手绘图标不在注册表(依赖 Compose 图形原语),ArkTS 页面未使用图标; CarbonDesignSystem {}/CarbonTheme {}等 Compose 入口天然不存在于鸿蒙侧,注册表只提供数据;- IBM Plex 字体(.otf,Compose resources)不随 so 分发,验收页用系统字体族近似展示字样尺寸,字族度量与 Plex 有细微差异;
- android / ios / wasm 目标剥离(源码留在树里),发布、dokka、kover、apiValidation 一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.carbon匹配的签名。
8.3 未来优化方向
- 生成器进 CI:上游 JSON 变更时自动重生成 +
jvmTest拦截漂移; - 把 4 个尺寸枚举的完整 dp 刻度、图标路径数据纳入注册表;
- 评估
.har形态发布 token 包,让 ArkTS 工程直接依赖而不是走 so 三件套; - 等社区 CMP 鸿蒙线追上上游 Compose 版本后,评估
:carbon组件层直接编译; - 补模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 carbon-compose(纯 UI 的 CMP 设计系统库)
→ 盘点数据资产:code-gen 的 4 份 @carbon/themes JSON + foundation 刻度常量
→ 工具链对齐 Kotlin 2.2.21-1.0.0 / Gradle 8.14.1(零依赖,旧线足够)
→ 根构建替换(cmp_qrose 模式,上游模块零改动留盘)
→ carbon-tokens 纯 Kotlin 注册表(4 主题 × 299 token + 26 字样 + 间距 + 动效)
→ 零漂移闸门:jvmTest 与上游 JSON 逐值比对(1196 处颜色值)
→ C ABI(@CName × 2)+ Map<String,String> 载荷
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析
→ ArkUI 真机五 Tab 目录验收(四主题整页换肤 / 色板 / 排版 / 组件)
9.2 封装层次
- 数据层:上游
@carbon/themes 11.75.0JSON(唯一颜色数据源,一字未动); - 注册表层:
CarbonTokenData(纯 Kotlin、零依赖,jvm + 双 ohos KLIB); - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、主题换肤、JSON 解析、组件复刻与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 纯 UI 库的适配先找"数据资产"再找"可编译文件":carbon-compose 能上鸿蒙的不是某个 .kt,而是设计系统本身的数据模型;找到唯一数据源(code-gen JSON),适配面就从 285 个组件缩小到一张表——路线选择必须基于"什么能被逐值证明没漂",而不是"什么文件碰巧不依赖 Compose";
- 零依赖是工具链自由的根源:因为注册表连 kotlinx 都不引,版本选择不再被制品线反推,可以沿用社区最成熟的
2.2.21+ Gradle 8.14.1 旧线;依赖越少,鸿蒙工具链的兼容矩阵越小; - 数据型适配的验收闸门是逐值测试而不是能跑:页面显示色块不等于数据正确;1196 处颜色值上的断言 + 上游 JSON 原文件作为测试输入,才让"零漂移"可审计——这与 KStore 用 51 项 desktopTest 钉住引擎语义是同一条纪律在不同库形态上的投影。
9.4 适配成果
当前适配已完成:
- carbon-compose
0.7.1设计 token 引擎的双 ABI Kotlin/Native 编译(4 主题 × 299 颜色 token、26 字样、13 间距、6+6 动效、按钮高度刻度,零第三方依赖); - 可复现生成器(确定性输出,签入仓库)与上游
:code-genJSON 数据源的完整落地; - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证双 ABI 全局 T 符号); - DevEco Stage ArkUI 五 Tab 验收页面(四主题整页换肤、色板、排版、间距动效、组件复刻、版本握手);
- 8 项 JVM 交叉验证全绿(1196 处颜色值逐值比对)+ 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。
参考文档
更多推荐

所有评论(0)