本文记录 Highlight 接入 OpenHarmony 的完整过程,覆盖正则高亮库盘点、xregex 依赖的 vendor 处置、匹配引擎真编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、addOrMerge 富文本合并算法的移植、ArkUI 真机页面、HAP 构建和真机验收。

Highlight 是基于正则表达式的跨平台文本 / 语法高亮库:给定一组 Scheme(每个 Scheme 是「一个 XRegex + 一个 Matcher」),它把命中结果变成平台富文本——Compose 侧是 AnnotatedString + SpanStyle,View 侧是 SpannedString。本次适配与 KStore 的"制品线换线"路线形成互补:上游依赖 com.neoutils.xregex:xregex:1.0.0-rc1 只发布了 jvm / js 变体,ohos 上永远解析不到——因此把它的 90 行 commonMain 源码按 MIT 许可原样 vendor 进仓库、补 jvm / ohos 两套 actual;同时上游的富文本层(toAnnotatedString / addOrMerge 碰撞切分合并)站在 Compose SpanStyle 上编不进 ohos,算法被移植成纯颜色 Style 的桥内实现,ArkTS 拿到的是切分合并完毕的 segments,用 Span 渲染。匹配引擎(Highlight / Scheme / Matcher / UiColor)一行未改,运行在真机上。

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

上游仓库: NeoUtils/Highlight

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

Highlight 为 Kotlin Multiplatform 应用提供"正则命中 → 富文本"这一件事的完整答案:Highlight { textColor { ... } } 一行建高亮器,TextColorScheme / BackgroundColorScheme / SpanStyleScheme 三种着色语义,Matcher.fully(...) 整体命中一色、Matcher.groups(...) 每个捕获组一色,Scheme.range 还能把匹配限定在文本区间内。上游在 Compose 侧用 toAnnotatedString / rememberAnnotatedString / rememberTextFieldValue 把结果接进 Text 与 BasicTextField,其中的 addOrMerge 负责处理多 scheme 命中区域重叠的碰撞切分与样式合并。KMP 项目把匹配引擎放在 commonMain,由 Android、桌面、Web 共享。OpenHarmony 应用同样需要这套能力(搜索词高亮、代码编辑器、日志着色),但上游构建(Kotlin 2.1.21 / CMP 1.8.2 / AGP 8.11.2)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示彩色文字,却无法证明匹配引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证引擎、正则抽象层、富文本合并算法的移植、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
依赖无 ohos 制品引擎 API 直接暴露 com.neoutils.xregex.XRegex 类型(Scheme.regex),而 xregex 1.0.0-rc1 在 Maven Central 只有 jvm / js 变体——不是版本后缀问题,是根本不存在。
Android 注解混入UiColor.Integer 上的 @ColorInt 把 androidx.annotation 拖进 commonMain 编译。
富文本层跨不了桥toAnnotatedString 站在 Compose AnnotatedString / SpanStyle 上,社区 CMP 1.9.2 线编不进上游 Compose 1.12 的 UI 层;SpanStyle 也过不了 C ABI。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:匹配引擎的 ohos 真编译、xregex 的 vendor 内联、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。匹配与合并语义完全由上游原版实现负责(合并算法为逐行移植),ArkTS 只负责调用 Native、解析 JSON 和渲染 Span。

1.2 库提供的能力

Demo 提供五个 Tab,对应桥接的全部入口:

  • 文本颜色(text):TextColorScheme + Matcher.fully,color 一词整体着色——对应上游 README 的 Quick Start;
  • 背景色(background):BackgroundColorScheme(蓝)与 TextColorScheme(白)两个 scheme 命中同一个词,addOrMerge 把它们合并成"蓝底白字"一段——对应上游 View 示例的双 scheme 叠加;
  • 捕获组(groups):("\w+")\s*=\s*("\w+") + Matcher.groups(Blue, Green),两个捕获组两种颜色;
  • 代码高亮(code):fun 关键字、函数调用(两组捕获)、@注解、字符串字面量四条 scheme 组合,复刻上游 CodeHighlightExample;
  • 自定义(custom):任意正则 + textColor / backgroundColor + 单色或分组色串,实时验证引擎的通用性。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文(非法正则不崩溃)。
kind / count回显样例类型与着色段数。
segments切分合并完毕的 [{text, color, background}] 数组,ArkTS ForEach 直接渲染 Span。
matches[{text, start, end}] 命中明细,状态行"匹配 N 处"的数据来源。

引擎侧完整能力(Scheme.range 区间限定、捕获组索引着色、UiColor 三态、DSL builder)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 7 项 jvmTest 交叉验证用例(上游本无测试,本仓新增),证明 ohos .so 里执行的匹配逻辑没有漂移。

1.3 实现适配

维度要求
代码复用highlight-core 的 15 个 commonMain 文件原样编译(仅剥一个 @ColorInt 注解);xregex 90 行 common 源码按 MIT 原样 vendor,3 个 expect 在 jvm / ohos 各补 actual。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);highlight-compose、highlight-view-based、build-logic 与四个上游 example 留盘不进构建。
桥接稳定1 个 C ABI(HighlightRender)和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
算法移植addOrMerge 碰撞切分合并逐行移植成纯颜色 Style(color, background)(非空覆盖合并语义对齐 SpanStyle.merge),只在桥内,不进库 public API。
UI 完整五 Tab 覆盖全部桥接入口:文本色 / 背景色 / 捕获组 / 代码高亮 / 自定义正则,非法输入返回 ok:false 不崩溃。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游的价值一半在引擎、一半在 Compose 富文本扩展;ohos 侧把引擎真编译、把合并算法移植进桥,ArkTS Span 只做最后一步渲染。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点三模块依赖边界、验证 xregex 制品线(结论:无 ohos 变体)、确定 vendor 路线
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1、vendor xregex + 剥注解
第 3 阶段:桥协议建模     ── kind/schemes JSON 契约、UiColor → #RRGGBB、addOrMerge 移植、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 五 Tab、TextArea 实时重高亮、ForEach Span 渲染、状态行
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
highlight/core/                       上游匹配引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64)
highlight/core/.../xregex/            vendor:xregex 1.0.0-rc1 的 9 个 common 文件 + jvm/ohos actual
highlight/compose/                    上游 Compose 富文本扩展,留盘不进 settings
highlight/view-based/                 上游 Android View/Spannable 扩展,留盘不进 settings
build-logic/                          上游 convention plugin(AGP + 发布),留盘不进构建
example/nativeApp/                    Kotlin/Native 动态库(Bridge + MiniJson + addOrMerge 移植)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 页面
example/{compose,view-based,desktop,web}/  上游示例,留盘不进构建
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存
doc/img/                              真机效果图

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :highlight:core 与 :example:nativeApp,rootProject.name = "ohos_highlight"。上游的 AGP / CMP / vanniktech 发布插件、build-logic 组合构建、js/js-store 全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,qrose / haze 同款已审线)
JDK21(org.gradle.java.home 指向 Eclipse Adoptium 21.0.12.101-hotspot)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(上游 8.13 升一格;2.2.21 旧线的要求,与 KStore 的 2.4.20 线留 9.x 不同)
DevEco product5.1.0(18) 兼容、hvigor modelVersion 5.1.0
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.highlight
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块文件依赖形态处置
highlight-core15(全 commonMain)纯 Kotlin + xregex + androidx.annotation(仅一个 @ColorInt)整体编译(jvm + 双 ohos),剥注解、vendor xregex
highlight-compose12(common + android + jvm + js)CMP runtime / ui(AnnotatedString / SpanStyle / remember)留盘不进构建(UI 层编不进社区 CMP)
highlight-view-based16(全 androidMain)Android Spannable / TextWatcher / appcompat留盘不进构建(无鸿蒙对应物)

与 KStore"全库只有一个 expect"不同,highlight-core 自身零 expect,麻烦全在依赖上。Scheme.regex: XRegex 把 xregex 钉进公开 API 签名,.module 元数据验证的结论是残酷的:

com.neoutils.xregex:xregex:1.0.0-rc1
  variants: jvmApi-published / jvmRuntime-published / jsApi-published / jsRuntime-published / metadataApiElements
  (没有 ohosArm64 / ohosX64,也没有任何原生 target 变体)

好消息是这 90 行 common 源码只有一个外部假设——kotlin.text.Regex。处置:9 个文件逐字拷入 highlight/core/src/commonMain/kotlin/com/neoutils/xregex/,仅有的 3 个 expect 声明在 jvm / ohos 各补一份 actual。同作者、同 MIT 许可,vendor 无合规问题。

1.4 页面边界
标题区          Highlight · OpenHarmony Demo(libhighlight_napi.so → libohoshighlight.so)
Tab 切换        文本颜色 / 背景色 / 捕获组 / 代码高亮 / 自定义
前四 Tab        提示行(当前 kind 的语义说明)+ TextArea(预置样例文本,可编辑)
                +「应用高亮」按钮 + 结果卡片(ForEach Span 渲染 segments)+ 状态行
自定义 Tab      额外提供 pattern 输入(默认 \bcolor\b)、textColor/backgroundColor 选择、
                单色输入(默认 #E66123)、分组色串输入(如 #0000FF,#008000)
状态行          高亮 N 段 · 匹配 M 处;失败时显示 失败: <error 原文>

页面切换 Tab 时自动重置预置文本,点「应用高亮」才发起 Native 调用——每次调用都是一次完整的"正则匹配 + 碰撞合并 + 切分"管线。

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

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

KStore 的经验是"先验证生态制品线再定适配路线"。对 Highlight 跑一遍同样的验证,结论走向另一个分支:eazytec Maven 上没有 com.neoutils.xregex 的 ohos 制品,Maven Central 上也没有(上游只发过 jvm / js)。"换线不换版"不可用,可选路线只剩三条:

路线评估
改 API:XRegex 换成 kotlin.text.Regex动 Scheme / 两个 Scheme / 两个 Scope 共 6 个文件的公开签名,上游 diff 面最大,审核叙事最差。
等上游发 ohos 变体上游 xregex 停在 1.0.0-rc1,不可控。
vendor:源码内联 + 补 actual90 行纯 Kotlin 逐字拷贝,签名零变化,diff 面集中在"新增目录"。✅
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)

Highlight 没有新版 kotlinx 依赖(引擎只用 kotlin.text),不存在 KStore 那种"coroutines 元数据 2.4.0 逼着 Kotlin 升 2.4.20"的压力——直接沿用社区已审的 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 同线)。该线要求 Gradle 8.x,上游 wrapper 是 8.13,实测升到 8.14.1 与 cmp_qrose 完全对齐。工具链版本跟着依赖走,两条线(2.2.21 降 Gradle、2.4.20 留 9.x)的经验不能互相套用。

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_highlight"

include(":highlight:core")
include(":example:nativeApp")
2.3 编译真实引擎

highlight/core/build.gradle.kts 从"convention plugin + Android + 发布"收敛为纯 KMP 模块:

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

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

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

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

三处源码处置:UiColor.Integer 剥掉 @ColorInt 与 androidx.annotation import(commonMain 里唯一的 Android 符号);xregex 9 个 common 文件原样 vendor;3 个 expect 的 actual 这样写(jvm / ohos 同构):

// highlight/core/src/ohosMain/kotlin/com/neoutils/xregex/extension/XRegex.ohos.kt
actual fun XRegex.findAll(
    text: CharSequence,
    range: IntRange
): List<XMatch> {
    val regex = Regex(
        pattern = pattern,
        options = flags.mapNotNull { it.toRegexOption() }.toSet()   // i/m/s/x → RegexOption
    )
    return regex.findAll(text, range.first)
        .takeWhile { it.range.first <= range.last }                 // range 语义 = 上游 scheme.range
        .mapIndexed { index, result ->
            XMatch(
                index = index,
                text = result.value,
                range = result.range,
                groups = result.groups.mapIndexed { groupIndex, group ->
                    if (group == null) return@mapIndexed null
                    XMatch.Group(index = groupIndex, text = group.value, range = group.range)
                }
            )
        }.toList()
}

实测:

./gradlew :highlight:core:compileKotlinOhosArm64 :highlight:core:compileKotlinOhosX64   # BUILD SUCCESSFUL
./gradlew :highlight:core:jvmTest                                                        # 7 项全绿

引擎的 15 个 commonMain 文件一个没动(除剥注解),vendor 的 9 个文件逐字保留。

2.4 构建隔离的处置

上游四个 example(compose / view-based / desktop / web)、highlight-compose、highlight-view-based、build-logic 组合构建全部留盘不进 settings.gradle.kts;kotlin-js-store/(yarn.lock,服务于已剥离的 js target)从仓库删除并加入 gitignore。根构建全局禁用 ohos 测试任务(commonTest 实际跑在 jvm 上):

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

第 3 阶段:桥协议建模

3.1 JSON 契约
com.neoutils.highlight.core/**(上游引擎 + vendor xregex,原样编译)
        ↓ Highlight / Scheme / Matcher / XRegex.findAll
Bridge.kt(@CName × 1,扁平 JSON 进出 + addOrMerge 移植)
        ↓ UTF-8 JSON
N-API highlightRender(request)
        ↓
ArkUI Index.ets(五 Tab,ForEach Span 渲染)

请求与响应:

请求 : {"kind":"text|background|groups|code|custom","text":"..."}
       custom 追加:{"pattern":"\\bcolor\\b","type":"textColor|backgroundColor",
                     "color":"#E66123","colors":"#0000FF,#008000"}
响应 : {"ok":true,"kind":"...","count":N,
        "segments":[{"text":"...","color":"#RRGGBB"|null,"background":"#RRGGBB"|null},...],
        "matches":[{"text":"...","start":N,"end":N},...]}
       {"ok":false,"error":"..."}

前四个 kind 在桥内使用与上游示例逐条对应的预置 scheme 集;custom 走通用构造路径(TextColorScheme / BackgroundColorScheme 直接实例化),一条通路同时覆盖两种着色与 fully / groups 两种匹配语义。

3.2 为什么输出 segments 而不是 AnnotatedString

上游 Compose 层的核心是 toAnnotatedString:遍历 scheme → regex.findAll → 按捕获组查 matcher.matches → addOrMerge 处理重叠。AnnotatedString / SpanStyle 编不进 ohos,但算法是纯区间运算。桥内用 (color: Long?, background: Long?) 的 Style 替代 SpanStyle,合并语义对齐 SpanStyle.merge(非空覆盖):

// example/nativeApp/.../Bridge.kt —— addOrMerge 的纯颜色移植(逐行对应上游 compose 扩展)
private fun MutableList<Span>.addOrMerge(span: Span) {
    val collisions = filter { it.start < span.end && span.start < it.end }
    if (collisions.isEmpty()) { add(span); return }
    removeAll(collisions)
    addAll(collisions.flatMap {
        buildList {
            if (it.start < span.start) add(it.copy(end = span.start))            // 左残段
            add(Span(it.style.merge(span.style),                                  // 重叠段:非空覆盖
                maxOf(it.start, span.start), minOf(it.end, span.end)))
            if (it.end > span.end) add(Span(it.style, span.end, it.end))          // 右残段
        }
    })
    add(span)
}

UiColor 三态统一转 ARGB 再格式化:Hex / Rgb / Integer → alpha 为 FF 时输出 #RRGGBB,否则 #AARRGGBB。渲染端 ArkTS Span 原生支持这两个格式。

3.3 错误通道

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

@CName("HighlightRender")
fun highlightRender(request: String): String = try {
    /* kind 分发 → 样例/自定义 scheme → render → ok(...) */
} catch (t: Throwable) {
    """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、custom 缺 pattern、正则编译失败,全部走同一个 ok:false 通道——Demo 的自定义 Tab 输入 [unclosed 这类坏正则时,页面显示"失败: …"而不崩溃,这正是真机验收用例之一。

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

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

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制匹配语义,所以选择 C ABI + JSON。与 KStore 的双符号不同,Highlight 的语义可以收敛为一个纯函数(文本 + 规则 → 着色结果,无状态、无版本握手需求),所以只有一个 @CName。

4.2 Kotlin/Native 导出函数
@CName("HighlightRender")
fun highlightRender(request: String): String   // 五种 kind 一口进
C ABI 符号ArkTS 方法覆盖内核
HighlightRenderhighlightRender(req)scheme 构建 + findAll + addOrMerge + segments 切分
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"highlightRender", nullptr, NapiHighlightRender, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

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

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

add_library(highlight_napi SHARED napi_init.cpp)
target_link_libraries(highlight_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoshighlight)

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

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

第 5 阶段:ArkUI 宿主

5.1 组装请求和触发渲染
private applyHighlight(kind: string): void {
  const request: Record<string, string> = { 'kind': kind, 'text': this.text };
  if (kind === 'custom') {
    request['pattern'] = this.pattern;
    request['type'] = this.customType;
    request['color'] = this.colorText;
    request['colors'] = this.colorsText;
  }
  try {
    const raw: string = highlightRender(JSON.stringify(request));
    const parsed: HighlightResult = JSON.parse(raw) as HighlightResult;
    if (!parsed.ok) { this.status = `失败: ${parsed.error ?? 'unknown error'}`; return; }
    this.segments = parsed.segments ?? [];
    this.matches = parsed.matches ?? [];
    this.status = `高亮 ${this.count} 段 · 匹配 ${this.matches.length} 处`;
  } catch (e) { /* Native 异常同样收进状态行,不抛出 */ }
}

渲染端一个 ForEach 解决全部着色语义——四分支正好对应桥的 color / background 可空组合:

ForEach(this.segments, (seg: Segment, idx: number) => {
  if (seg.color && seg.background) {
    Span(seg.text).fontColor(seg.color).backgroundColor(seg.background)
  } else if (seg.color) {
    Span(seg.text).fontColor(seg.color)
  } else if (seg.background) {
    Span(seg.text).backgroundColor(seg.background)
  } else {
    Span(seg.text)
  }
}, (seg: Segment, idx: number) => `${idx}`)
5.2 页面交互

五个 Tab 自上而下:提示行(当前 kind 的语义说明,如"演示 BackgroundColorScheme —— 匹配区域加背景色")、TextArea(预置样例文本,可编辑后重新点按钮验证)、「应用高亮」按钮、结果卡片(浅灰底圆角,Text 内 ForEach 渲染 Span)、状态行(高亮 N 段 · 匹配 M 处 或红色错误文本)。自定义 Tab 额外提供 pattern / 类型选择 / 单色 / 分组色串四个输入。

5.3 错误边界

ok:false 时错误原文进状态行(红字 #B91C1C),结果区清空;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行并落 hilog,页面永不崩溃。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoshighlight)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName × 1 + MiniJson + addOrMerge 移植
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.highlight、versionName 2.3.0-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/libohoshighlight_api.h
        ├── src/main/cpp/types/libhighlight_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 五 Tab 验收页面
6.2 构建、签名和安装
./gradlew :highlight:core:jvmTest                                          # 7 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

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

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。

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.highlight -m entry

四、完整代码对照

4.1 整体架构

com.neoutils.highlight.core/**(上游引擎原样编译)
    │ Highlight / Scheme / Matcher / UiColor / range 合并
com.neoutils.xregex/**(vendor,原样编译)
    │ XRegex / XMatch / findAll expect→actual
Bridge.kt + MiniJson
    │ HighlightRender:kind 分发 + addOrMerge 移植 + segments 切分
libhighlight_napi.so
    │ highlightRender
Index.ets
    │ JSON parse + ForEach Span
    ▼
ArkUI 真机页面(五 Tab)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_highlight、compose/view/上游 example 移出
gradle/libs.versions.toml只留 Kotlin 2.2.21-1.0.0 + KMP 插件别名
build.gradle.kts(根)版本 2.3.0-ohos.1、全局禁用 Ohos*Test* 任务
highlight/core/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64 + commonTest
highlight/core/.../xregex/**(9 文件)vendor 的 xregex common 源码,逐字保留
highlight/core/src/{jvm,ohos}Main/.../xregex/(各 3 文件)3 个 expect 的 actual(kotlin.text.Regex 实现,flags i/m/s/x 映射)
highlight/core/src/commonTest/.../HighlightTest.kt新增 7 项交叉验证
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt1 个 C ABI、五种 kind、MiniJson、addOrMerge 移植、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 五 Tab 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(22 节)

4.3 关键 API 对照

层次API作用
引擎Highlight { textColor { ... } }DSL 建高亮器(HighlightScope / TextColorScope)
引擎TextColorScheme / BackgroundColorScheme前景 / 背景着色 scheme(Scheme<UiColor> 实现)
引擎Matcher.fully(v) / Matcher.groups(...)整体命中一色 / 按捕获组索引着色
引擎Scheme.range匹配限定在文本区间(IntRange.merge 交集)
引擎UiColor.Hex / Rgb / Integer颜色三态,桥内统一转 #RRGGBB
vendorXRegex.findAll(text, range)正则命中(ohos actual 基于 kotlin.text.Regex)
移植addOrMerge(Span)重叠碰撞切分 + 非空覆盖合并(对应上游 SpanStyle.merge)
NativeHighlightRenderC ABI JSON 导出(唯一符号)
N-APIhighlightRender(request)向 ArkTS 暴露能力
ArkTSapplyHighlight(kind)组请求、刷新 segments / 状态行

4.4 ArkTS 与 Kotlin 的边界

const raw = highlightRender(JSON.stringify({
  kind: 'custom', text: '"name" = "Irineu"',
  pattern: '("\\w+")\\s*=\\s*("\\w+")', type: 'textColor',
  colors: '#0000FF,#008000'
}));
// res.ok === true;segments = ["\"name\""(蓝), " = ", "\"Irineu\""(绿)]
val scheme = TextColorScheme(
    regex = pattern.toXRegex(),
    matcher = Matcher.groups(UiColor.Hex("#0000FF"), UiColor.Hex("#008000")),
)
for (result in scheme.regex.findAll(text, text.range)) {
    for (group in result.groups) {
        val color = scheme.matcher.matches[group.index] ?: continue
        spans.addOrMerge(Span(style(color), group.range.first, group.range.last + 1))
    }
}

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或 SpanStyle。

五、关键决策说明

决策 1:vendor xregex,而不是改 API 或等制品

"换线不换版"的前提是 ohos 制品存在;xregex 的结论是不存在且上游不活跃。改公开签名(XRegex → Regex)会让 6 个引擎文件进 diff,违背"引擎零改动"的审核底线;vendor 让 90 行同 MIT 源码逐字进仓、差异集中在一个新增目录,3 个 expect 的 actual 是唯一的新代码——jvm 与 ohos 同构,还顺带保住了 jvm 交叉验证。

决策 2:富文本合并算法移植进桥,而不是硬编 Compose 或让 ArkTS 重写

toAnnotatedString / addOrMerge 是纯区间运算,与 Compose 的耦合只在 SpanStyle 类型上。把 SpanStyle 换成 (color, background) 可空对、merge 换成非空覆盖,算法逐行保留——ArkTS 拿到的 segments 已经是"碰撞处理完"的结果,页面零合并逻辑。反过来若在 ArkTS 重写合并,同一个库就会出现两份算法,漂移只是时间问题。

决策 3:工具链沿用 2.2.21-1.0.0 已审线,Gradle 8.14.1

KStore 因 coroutines 制品线被迫升 2.4.20;Highlight 引擎只依赖 kotlin.text,没有外部逼升因素,沿用 qrose / haze 已审的 2.2.21-1.0.0 线并把 Gradle 对齐 8.14.1。先盘点依赖矩阵,再选工具链线——这是两条适配唯一通用的规则。

决策 4:compose / view-based 模块留盘,UI 语义由桥 + ArkTS 承接

highlight-compose 站在上游 CMP 1.8.2 上、highlight-view-based 站在 Android Spannable 上,前者待社区 CMP 支持后可接回,后者在鸿蒙没有对应物。留盘不删除:源码就是"接回条件满足时"的现成参考,也是改动面审计的对照物。

决策 5:上游无测试,自建 7 项 commonTest 交叉验证

KStore 有上游 51 项测试可零改动复用;Highlight 上游没有任何测试。引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码——新增 7 项测试(整词命中、捕获组、range 限定、flags 映射、DSL 等价、区间合并、findAll 稳定性)钉住 vendor 层与引擎的接缝,配合 llvm-nm 符号验收,构成"内核没漂"的闸门。

六、测试与验证

6.1 测试环境

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

6.2 JVM 交叉验证

./gradlew :highlight:core:jvmTest

7 项用例全部通过(本仓新增):

用例钉住什么
matchesFullWord\b 整词命中与 range 下标(11…15)
matchesGroups捕获组文本与顺序("name" / "Irineu")
respectsSchemeRangeScheme.range 区间限定:跳过首次命中
flagsMapToRegexOptionsvendor flags i → RegexOption.IGNORE_CASE 映射
builderProducesSameSchemesDSL builder 与直接构造产出等价 scheme(vendor 转换无损)
rangesMergeByIntersectionIntRange.merge 交集语义
xRegexFindAllIsStablevendor findAll 多命中顺序与 group 0 一致性

6.3 构建与符号验证

:highlight:core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:highlight:core:jvmTest: 7 项全部通过
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: KN so + libc++_shared.so 回填 entry/libs/<abi>/
llvm-nm --defined-only: HighlightRender / libohoshighlight_symbols 均为全局 T 符号(双 ABI)
hvigor assembleHap: finished
install bundle successfully / start ability successfully

debugShared 产物:KN so arm64 约 3.8 MB、x86_64 约 3.4 MB(含 Kotlin/Native 运行时与正则引擎),libc++_shared.so 约 1.9 MB / 1.7 MB;HAP 内双 ABI 各三件套(libhighlight_napi.so / libohoshighlight.so / libc++_shared.so)。

6.4 功能用例

  1. 启动进入五 Tab 页面,标题行显示 Highlight · OpenHarmony Demo(libhighlight_napi.so → libohoshighlight.so)——桥链路一次可见;
  2. 文本颜色 Tab:color 一词整体变橙(#E66123),状态行回显高亮段数与命中次数;
  3. 背景色 Tab:color 同时被背景(蓝)与前景(白)两个 scheme 命中,合并渲染为蓝底白字一段——addOrMerge 合并语义的真机证据(见 6.5 效果图);
  4. 捕获组 Tab:"name" 蓝、"Irineu" 绿,中间 = 保持原色——分组着色正确;
  5. 代码高亮 Tab:fun 橙、函数名深青、参数高亮蓝、@Composable 黄绿、字符串绿——四条 scheme 组合互不干扰;
  6. 自定义 Tab:改 pattern / 类型 / 颜色后点应用,实时重新着色;
  7. 自定义 Tab 输入非法正则(如 [unclosed),状态行显示"失败: …",应用不崩溃;
  8. 编辑任意 Tab 的文本再点应用,高亮随文本实时更新。

6.5 验证结论

匹配引擎、vendor 的 xregex、合并算法移植、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,五个 Tab 覆盖全部桥接入口(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

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

八、遗留问题与改进方向

8.1 踩坑复盘

  1. kotlin.text.MatchGroup 没有 index 属性——xregex 的 XMatch.Group(index, ...) 需要组号,映射时必须用 mapIndexed 拿下标,直接 group.index 编译不过(jvm / ohos 两套 actual 各踩一次);
  2. Scheme<*> 星号投影下 matcher.matches 的元素类型退化为 Any——toArgb(color) 直接报类型不匹配,解法是在 when (scheme) 的 smart-cast 分支里访问 scheme.matcher;
  3. Matcher 泛型经 if 双分支 + spread 展开推断退化为 Any——val matcher: Matcher<UiColor> = ... 显式标注;
  4. commonMain 里唯一的 Android 符号是 @ColorInt,不剥掉会把 androidx.annotation 拖进 ohos 编译——上游注解剥离是"零依赖内核"的前置动作(cmp_qrose 阶段 2 同款);
  5. xregex 在 Maven Central 只有 jvm / js 变体,.module 元数据先验证再定路线,别等编译期才炸 Could not resolve;
  6. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  7. 2.2.21 旧线要求 Gradle 8.x,上游 8.13 升 8.14.1 与已审仓对齐;命令行跑 gradlew 还需 JAVA_HOME 指向 JDK 21(org.gradle.java.home 只管 daemon,不管启动器);
  8. 上游 .gitignore 的 /build 是根锚定,盖不住 example/nativeApp/build/——必须改成非锚定 build,否则 git add -A 会把 KLIB、so 等构建产物卷进提交;
  9. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI + KN + libc++)缺一不可;
  10. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  11. IMPORTED 的 KN so 要 IMPORTED_NO_SONAME TRUE + $ORIGIN rpath,否则 Windows 构建机的绝对路径会写进 RUNPATH;
  12. 链接期 CAdapter 生成的 api.cpp:448 [-Wreturn-type] 警告是已知噪音(qrose 仓同款),不影响符号导出;
  13. DevEco 打开 example/harmonyApp 子目录而非仓库根,否则 hvigor 找不到模块边界;签名材料由 IDE 自动写入 build-profile.json5,证书绝不进入仓库。

8.2 已知问题

  • SpanStyleScheme(fontStyle / fontWeight 等富文本属性)未进桥——当前 segments 只有 color / background 两个通道,与 Span 的能力对齐即可覆盖主路径;
  • custom kind 限单 scheme、分组色仅接受 #RRGGBB Hex 形式;
  • highlight-compose / highlight-view-based 留盘不编译:前者待社区 CMP 支持后接回,后者(Android Spannable / TextWatcher)在鸿蒙无对应物;
  • android / js 目标剥离(源文件留在树里),发布、convention plugin 一并剥离,本 fork 不发 Maven Central;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.highlight 匹配的签名。

8.3 未来优化方向

  • 桥增加 fontStyle / fontWeight / fontFamily 通道,对齐 SpanStyleScheme 全语义;
  • custom kind 支持多 scheme 数组与 scheme.range 区间参数;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)验收矩阵与 uitest 自动化截图。

九、总结

9.1 核心难点回顾

上游 highlight-core(15 个 commonMain 文件)
    → 验证 xregex 制品线(结论:无 ohos 变体)→ vendor 90 行 + 3 expect × 双平台 actual
    → 剥 @ColorInt、根构建替换(cmp_qrose 模式)
    → 工具链对齐 Kotlin 2.2.21-1.0.0 / Gradle 8.14.1(已审线)
    → ohosArm64 / ohosX64 KLIB
    → C ABI(@CName × 1)+ addOrMerge 纯颜色移植 → segments
    → C++ N-API(DisposeString 纪律)
    → ArkTS ForEach Span 渲染
    → ArkUI 真机五 Tab 验收

9.2 封装层次

  • 引擎层:上游原版 Highlight / Scheme / Matcher / UiColor 与 DSL scope;
  • 正则层:vendor 的 XRegex common 源码 + jvm / ohos actual(kotlin.text.Regex);
  • 桥层:kind 分发、addOrMerge 移植、segments 切分、错误通道;
  • N-API 层:参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:五 Tab 状态管理、JSON 组包、ForEach Span 渲染与状态行;
  • DevEco 层:CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 制品线验证的结论决定路线:同样是"依赖挡路",KStore 等到了 ohos 制品线就走"换线不换版",Highlight 等不到就走"vendor + 补 actual"——两条路线都以 .module 元数据的当天可达性为准,而不是历史结论或对上游的乐观预期;
  2. 算法移植优于语义重写:富文本合并这类纯逻辑,逐行移植成平台无关形态(纯颜色 Style),比在 ArkTS 重写一份可靠得多——上游算法是经过 Compose 三个平台 exercised 的实现,diff 面就是可信度;
  3. 上游没有测试时,交叉验证要自建:引擎在 commonMain、JVM 与 ohos 同码,7 项针对 vendor 接缝与核心语义的测试加上 llvm-nm 符号验收,就是"内核没漂"的最小闸门。

9.4 适配成果

当前适配已完成:

  • Highlight 2.3.0 真实引擎的双 ABI Kotlin/Native 编译(上游 commonMain 零重写,仅剥一个注解);
  • xregex 1.0.0-rc1 的 vendor 内联(9 文件逐字 + 6 个 actual),ohos 侧正则能力与 jvm 完全同源;
  • addOrMerge 碰撞合并算法的纯颜色移植与 1 个 C ABI 符号、C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 五 Tab 验收页面(文本色、背景色合并、捕获组、代码高亮、自定义正则、非法输入不崩溃);
  • 7 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
  • AtomGit 项目文档、OpenHarmony README(中英双份)和本地效果图。

适配过程 FAQ

以下问题来自 ohosArm64 目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。

Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?

  • 确认 DevEco Studio 版本与工程 build-profile.json5 的 compatibleSdkVersion 一致;
  • 在 File > Settings > Build, Execution, Deployment > Ohpm 配置可用的 ohpm 仓库镜像或代理;
  • 删除 oh_modules、oh-package-lock.json5 与 .hvigor 缓存后重新 Sync;
  • 公司网络需要代理时,同时给 ohpm 与 hvigor 配置 http.proxy。

Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?

  • 先在 example 目录执行 ./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用 linkDebugSharedOhosX64);
  • 确认 libxxx.so 与 libc++_shared.so 已复制到 ohos/entry/libs/<abi>/;
  • 检查 entry/build-profile.json5 的 abiFilters 与设备 ABI 是否一致(真机 arm64-v8a,模拟器 x86_64);
  • 清理 entry/.cxx 与 entry/build 后重试。

Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?

  • 确认 CMake 的 add_library(<target> SHARED ...) 名称与 import ... from 'libxxx_napi.so' 完全一致;
  • 解包 HAP,确认 libs/<abi>/libxxx_napi.so 已打包;
  • 删除 entry/build、entry/.cxx 与 oh_modules 后重新构建;
  • 检查 module.json5 的 srcEntry 与页面路径是否正确。

Q4:真机安装报 signature verification failed 或 install failed?

  • 在 DevEco Studio 的 Signing Configs 配置证书、Profile 与密码;
  • bundleName 必须与签名 profile 中的 bundle name 一致;
  • 设备需加入 profile 的 deviceIds,调试证书有效期需覆盖当前时间;
  • 用 hdc list targets 确认设备在线,再执行 hdc install -r xxx-signed.hap。

Q5:应用启动白屏、闪退或 NAPI 调用崩溃?

  • 用 hdc hilog | grep -i "napi\|kotlin" 查看原生日志;
  • 确认 libc++_shared.so 与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致;
  • 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用 XxxFree 释放;
  • 在 DevEco 的 Native 调试配置中附加 lldb,定位空指针与越界访问。

Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?

  • 使用鸿蒙发行版 Kotlin(如 2.2.21-1.0.0),并在 settings.gradle.kts 配置 eazytec 仓库;
  • Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
  • 确认 kotlin { ohosArm64() } 已应用,且 local.properties 的 SDK 路径有效;
  • 清理 ~/.gradle/caches 中对应版本的 Kotlin/Native 缓存后重试。

Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?

  • 对齐 DevEco Studio、HarmonyOS SDK 与 build-profile.json5 中的 compatibleSdkVersion;
  • 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免 modelVersion 与 IDE 不匹配;
  • 命令行构建时设置 DEVECO_SDK_HOME 指向 DevEco 的 sdk 目录。

Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?

  • 使用 DevEco 自带的 node 与 hvigor:
    "$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...;
  • 设置 JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME;
  • 加上 --no-daemon 排除守护进程缓存影响,并清理 entry/build 与 .hvigor。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐