目标很明确:不改 rust-brotli 一行源码,把 brotli 压缩/解压能力搬到鸿蒙,然后用 ArkTS 验证一切正常。
参考:ohos-rs,基于 napi-rs 的鸿蒙 Rust 框架。


0. 搞定了什么

rust-brotli 源码从头到尾没动过,只通过路径依赖引入。交叉编译出了 arm64-v8a、armeabi-v7a、x86_64 三套 .so,接入了鸿蒙工程的 entry 模块,ArkTS 封装层和验证页面都写好了。HAP 打包成功,主机端全量往返测试全部通过,应用也已在鸿蒙模拟器上成功部署运行(见 §5.3)。

下图是 rust-brotli 目录的 git status,工作区干净,证明源码确实零改动:

rust-brotli 源码零改动


1. 怎么做

rust-brotli(crate 名 brotli,v8.0.4)是纯 Rust 库,核心就两个 API:

  • brotli::CompressorWriter::new(w, buffer_size, quality, lgwin) — 实现了 std::io::Write,往里写数据就自动压缩;
  • brotli::Decompressor::new(r, buffer_size) — 实现了 std::io::Read,往外读数据就自动解压。

ArkTS 没法直接调 Rust 函数,必须过 N-API 这一层。ohos-rs 提供了 napi-ohos / napi-derive-ohos,用 #[napi] 宏就能把 Rust 函数导出成 Native 接口给 ArkTS 用。

整个适配就一个思路——写个 wrapper crate:

rust-brotli (源码零改动)
      ▲  path 依赖
      │
brotli_ohos (本次新增的 wrapper, cdylib)
      │  #[napi] 导出 compress / decompress / brotliVersion
      ▼  ohrs 交叉编译
libbrotli_ohos.so (arm64 / arm / x86_64)
      ▼  集成进 entry 模块
ArkTS: import { compress, decompress } from 'libbrotli_ohos.so'

所有适配逻辑都在 brotli_ohos 里,rust-brotli 保持原样不动。


2. 环境准备

2.1 已就绪的组件

组件 版本 说明
Rust / Cargo 1.92.0 rustup / cargo / rustc
ohos-rs CLI (ohrs) 1.3.0 交叉编译与产物生成
鸿蒙 Command Line Tools 6.1.1.280 含 ohpm 6.1.2、hvigor 6.24.2、hdc
OpenHarmony Native NDK 6.1.1.125(API 24) LLVM clang 15 工具链
Node.js / npm 23.11.0 / 11.5.2 napi 生成依赖

2.2 安装 Rust 与鸿蒙交叉编译目标

# 安装 rustup / rust(没装过的话)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 安装鸿蒙三个交叉编译目标
rustup target add aarch64-unknown-linux-ohos \
                  armv7-unknown-linux-ohos \
                  x86_64-unknown-linux-ohos

确认一下:

$ rustup target list --installed | grep ohos
aarch64-unknown-linux-ohos
armv7-unknown-linux-ohos
x86_64-unknown-linux-ohos

rustup 已安装鸿蒙交叉编译目标

2.3 安装 ohos-rs CLI

cargo install ohrs
# 验证
$ ohrs --version
Version: 1.3.0

2.4 配置 NDK 环境变量(踩过的坑)

ohrs 通过 OHOS_NDK_HOME 找 LLVM 工具链。它会自动在这个路径后面拼 native/llvm/...,所以这个变量必须指向包含 native/ 目录的那一级 openharmony,而不是 native 本身。

# ✅ 正确:指向 .../openharmony(其下有 native/llvm/lib/aarch64-linux-ohos)
export OHOS_NDK_HOME=~/command-line-tools/sdk/default/openharmony

# ❌ 错误:指向 .../openharmony/native 会被拼成 native/native/llvm
#   报错:Convert .../openharmony/native/native/llvm/lib/aarch64-linux-ohos
#         to absolute path failed: NotFound

这里有个很坑的点:ohrs doctor 只检查 OHOS_NDK_HOME 是不是非空,不验证层级对不对。所以 doctor 通过不代表路径正确,别被骗了。

2.5 体检一下

$ OHOS_NDK_HOME=~/command-line-tools/sdk/default/openharmony ohrs doctor
✔  Environment variable OHOS_NDK_HOME should be set.
✔  Rust version should be >= 1.88.0.
✔  Rustup target: aarch64-unknown-linux-ohos should be installed.
✔  Rustup target: armv7-unknown-linux-ohos should be installed.
✔  Rustup target: x86_64-unknown-linux-ohos should be installed.

ohrs doctor 环境检查全部通过


3. 适配过程(不改源码)

3.1 用 ohrs 脚手架生成 wrapper crate

在工程根目录(跟 rust-brotli/ 同级)执行:

ohrs init brotli_ohos

生成出来的结构:

brotli_ohos/
├── Cargo.toml          # cdylib + napi-ohos 依赖
├── build.rs            # napi_build_ohos::setup()
├── src/lib.rs          # 示例 add()
└── .vscode/settings.json

3.2 wrapper 的 Cargo.toml 用路径依赖引入 brotli

brotli_ohos/Cargo.toml[dependencies] 里加一行:

# 鸿蒙化适配核心:路径依赖引入 rust-brotli,不动源码。
brotli = { path = "../rust-brotli" }

crate-type = ["cdylib"] 保持脚手架默认值就行,生成 .so 必须要这个。

3.3 写 napi 导出(brotli_ohos/src/lib.rs)

#[napi] 导出三个接口:compress / decompress / brotli_version,完整代码如下:

//! brotli_ohos —— rust-brotli 的鸿蒙(OpenHarmony/HarmonyOS)化适配层。
//!
//! 设计原则:不改动 rust-brotli 任何源代码。本 crate 仅以路径依赖的方式
//! 引入 `brotli`,并通过 ohos-rs(napi-ohos) 把压缩/解压能力导出为可被
//! ArkTS 直接调用的 Native 接口。

use std::io::{Read, Write};

use napi_derive_ohos::napi;
use napi_ohos::bindgen_prelude::Buffer;
use napi_ohos::{Error, Result, Status};

/// 用于 brotli 流式读写的内部缓冲区大小。
const BUFFER_SIZE: usize = 4096;
/// brotli 默认压缩质量(0-11),11 为最高压缩比。
const DEFAULT_QUALITY: u32 = 11;
/// brotli 默认滑动窗口对数大小(10-24)。
const DEFAULT_LGWIN: u32 = 22;

/// 压缩一段二进制数据。
///
/// - `data`:待压缩的原始字节。
/// - `quality`:可选,压缩质量 0-11,默认 11。
/// - `lgwin`:可选,窗口对数大小 10-24,默认 22。
///
/// 返回压缩后的字节(brotli 格式)。
#[napi]
pub fn compress(data: Buffer, quality: Option<u32>, lgwin: Option<u32>) -> Result<Buffer> {
  let input: Vec<u8> = data.into();
  let q = quality.unwrap_or(DEFAULT_QUALITY).min(11);
  let w = lgwin.unwrap_or(DEFAULT_LGWIN).clamp(10, 24);

  let mut compressed: Vec<u8> = Vec::new();
  {
    let mut writer = brotli::CompressorWriter::new(&mut compressed, BUFFER_SIZE, q, w);
    writer
      .write_all(&input)
      .map_err(|e| Error::new(Status::GenericFailure, format!("brotli 压缩写入失败: {e}")))?;
    writer
      .flush()
      .map_err(|e| Error::new(Status::GenericFailure, format!("brotli 压缩刷新失败: {e}")))?;
  }
  Ok(compressed.into())
}

/// 解压一段 brotli 数据,返回原始字节。
#[napi]
pub fn decompress(data: Buffer) -> Result<Buffer> {
  let input: Vec<u8> = data.into();
  let mut reader = brotli::Decompressor::new(&input[..], BUFFER_SIZE);
  let mut decompressed: Vec<u8> = Vec::new();
  reader
    .read_to_end(&mut decompressed)
    .map_err(|e| Error::new(Status::GenericFailure, format!("brotli 解压失败: {e}")))?;
  Ok(decompressed.into())
}

/// 返回被适配的 brotli crate 版本号,便于在鸿蒙侧确认链接的库版本。
#[napi]
pub fn brotli_version() -> String {
  env!("CARGO_PKG_VERSION").to_string()
}

几个细节:

  • 入参和返回值用 Buffer,ohos-rs 生成 .d.ts 时会自动映射成 ArkTS 的 ArrayBuffer
  • 错误统一转成 napi Error,ArkTS 侧用 try/catch 就能捕获;
  • #[napi] 的蛇形函数名会自动转 ArkTS 小驼峰(brotli_versionbrotliVersion)。

3.4 交叉编译

cd brotli_ohos
export OHOS_NDK_HOME=~/command-line-tools/sdk/default/openharmony
ohrs build --release

输出(节选):

Compiling brotli v8.0.4 (/Volumes/coder/hmapp/rust/rust-brotli)
Compiling brotli_ohos v0.1.0
Finished `release` profile [optimized] target(s) in 11.44s
Create index.d.ts succeed.

ohrs build 三架构交叉编译成功

产物在 brotli_ohos/dist/

dist/index.d.ts
dist/arm64-v8a/libbrotli_ohos.so      (~1.9 MB)
dist/armeabi-v7a/libbrotli_ohos.so
dist/x86_64/libbrotli_ohos.so

dist 产物:三架构 .so 与 index.d.ts

自动生成的 dist/index.d.ts(Rust 文档注释也带过来了):

/* auto-generated by OHOS-RS */
/* eslint-disable */



/** 返回被适配的 brotli crate 版本号,便于在鸿蒙侧确认链接的库版本。 */
export declare function brotliVersion(): string

/**
  * 压缩一段二进制数据。
  *
  * - `data`:待压缩的原始字节。
  * - `quality`:可选,压缩质量 0-11,默认 11。
  * - `lgwin`:可选,窗口对数大小 10-24,默认 22。
  *
  * 返回压缩后的字节(brotli 格式)。
  */
export declare function compress(data: ArrayBuffer, quality?: number | undefined | null, lgwin?: number | undefined | null): ArrayBuffer

/** 解压一段 brotli 数据,返回原始字节。 */
export declare function decompress(data: ArrayBuffer): ArrayBuffer

ohrs 会提示 Buffer 在 ArkTS 上支持较弱、建议改用 ArrayBuffer。但生成的 .d.ts 已经自动把 Buffer 暴露为 ArrayBuffer 了,所以 ArkTS 侧直接用 ArrayBuffer 就行,不用额外处理。


4. 集成进鸿蒙工程(entry 模块)

4.1 放好预编译的 .so

鸿蒙 HAP 默认从模块的 libs/<abi>/ 目录收集 Native 库:

mkdir -p entry/libs/{arm64-v8a,armeabi-v7a,x86_64}
cp brotli_ohos/dist/arm64-v8a/libbrotli_ohos.so   entry/libs/arm64-v8a/
cp brotli_ohos/dist/armeabi-v7a/libbrotli_ohos.so entry/libs/armeabi-v7a/
cp brotli_ohos/dist/x86_64/libbrotli_ohos.so      entry/libs/x86_64/

4.2 配类型声明,让 import ... from 'libbrotli_ohos.so' 能解析

entry/src/main/cpp/types/libbrotli_ohos/
├── index.d.ts        # 从 dist/index.d.ts 复制
└── oh-package.json5

oh-package.json5

{
  "name": "libbrotli_ohos.so",
  "types": "./index.d.ts",
  "version": "1.0.0",
  "description": "rust-brotli 鸿蒙化适配 Native 模块的类型声明"
}

entry/oh-package.json5 注册依赖:

{
  "dependencies": {
    "libbrotli_ohos.so": "file:./src/main/cpp/types/libbrotli_ohos"
  }
}

然后执行 ohpm install

集成后 entry 模块的结构如下(红框为本次新增:libs/<abi>/ 下的三架构 .so,以及 cpp/types/libbrotli_ohos/ 下的类型声明):

entry 模块集成结构

4.3 ArkTS 封装层 entry/src/main/ets/utils/BrotliCodec.ets

封装了 string ↔ ArrayBuffer 转换和一次性往返校验:

import { compress, decompress, brotliVersion } from 'libbrotli_ohos.so';
import { util } from '@kit.ArkTS';

export function compressText(text: string, quality?: number, lgwin?: number): ArrayBuffer { /* ... */ }
export function decompressToText(buffer: ArrayBuffer): string { /* ... */ }
export function roundTrip(text: string, quality?: number, lgwin?: number): RoundTripResult { /* ... */ }

(完整实现在源文件里,遵循「始终返回新缓冲区」的不可变风格。)

4.4 验证页 entry/src/main/ets/pages/Index.ets

点击按钮对一段高重复文本执行 压缩→解压,页面对照展示三部分数据:原始数据(UTF-8 文本)、压缩后数据(brotli 字节,十六进制显示)、解压还原数据(应与原始完全一致)。顶部还显示 brotli 版本、原始/压缩字节数、压缩率、是否无损。

BrotliCodec.ets 新增了 bufferToHex(buffer) 把压缩后的二进制转成十六进制串用于展示;RoundTripResult 增加了 original / compressedHex / decoded 字段。


5. 验证

5.1 编译期:HAP 打包(通过)

本机 Command Line Tools 是 API 24 / hvigor 6.24.2(modelVersion 6.1.1),工程初始是 API 26,需要把工程配置对齐到已装工具链。这只改了工程配置,跟 rust-brotli 源码没关系:

文件 改动
oh-package.json5 modelVersion 26.0.0 → 6.1.1
hvigor/hvigor-config.json5 modelVersion 26.0.0 → 6.1.1
build-profile.json5 targetSdkVersion 26.0.0 → 6.1.1(24);去掉指向空配置的 signingConfig

构建:

export DEVECO_SDK_HOME=~/command-line-tools/sdk
hvigorw --mode module -p module=entry@default -p product=default assembleHap --no-daemon

结果:

Finished :entry:default@CompileArkTS ...     # ArkTS 调用代码编译通过(含对 .so 的 import 与类型校验)
Finished :entry:default@ProcessLibs ...      # 三架构 .so 收集
Finished :entry:default@DoNativeStrip ...
Finished :entry:default@PackageHap ...
WARN: No signingConfig found for product default   # 未配置签名,产物 unsigned(可正常打包,安装需签名)
BUILD SUCCESSFUL in 5 s 217 ms

验证 .so 已经打入 HAP:

$ unzip -l entry/build/default/outputs/default/entry-default-unsigned.hap | grep -E 'libbrotli_ohos.so|modules.abc'
  1654344  libs/arm64-v8a/libbrotli_ohos.so
  1648440  libs/armeabi-v7a/libbrotli_ohos.so
  1780912  libs/x86_64/libbrotli_ohos.so
    18900  ets/modules.abc

这一步证明了:ArkTS 的 import { compress, decompress } from 'libbrotli_ohos.so' 能正确解析、类型匹配并编译,三架构 Native 库也被正确打入了 HAP。

5.2 逻辑运行:主机端往返验证(通过)

没有设备也能验证 brotli 适配逻辑对不对。新增了 verify_host,用跟 brotli_ohos 完全相同的 brotli 路径依赖和调用方式,在开发机上跑:

cargo run --manifest-path verify_host/Cargo.toml --release

verify_host 会对照打印原始数据、压缩后数据(hex)、解压还原数据。输出:

──────── [repetitive] ────────
① 原始数据      (  376 字节): HarmonyOS rust-brotli 鸿蒙化适配验证 #0\nHarmonyOS rust-brotli 鸿蒙化适配验证 #1\n...#7\n
② 压缩后(hex)   (   89 字节): 8b bb 00 f8 9d 09 b6 0d 85 73 cc 42 c3 9f 49 92 3a 1d 04 bd bc 4e 0e 58 ff b6 ec 49 ... 03
③ 解压还原      (  376 字节): HarmonyOS rust-brotli 鸿蒙化适配验证 #0\n...#7\n
   无损还原: true   压缩率: 23.67%

──────── [short-text] ────────
① 原始数据      (   25 字节): hello brotli on HarmonyOS
② 压缩后(hex)   (   29 字节): 0b 0c 80 68 65 6c 6c 6f 20 62 72 6f 74 6c 69 20 6f 6e 20 48 61 72 6d 6f 6e 79 4f 53 03
③ 解压还原      (   25 字节): hello brotli on HarmonyOS
   无损还原: true   压缩率: 116.00%

──────── [empty] ────────
① 原始数据      (    0 字节):
② 压缩后(hex)   (    3 字节): 6b 00 03
③ 解压还原      (    0 字节):
   无损还原: true   压缩率: 0.00%

ALL ROUND-TRIPS PASSED

简单解读一下:

  • 原始 376 字节的重复文本压缩到 89 字节(压缩率 23.67%),解压完整还原;
  • short-textunicode 这种极短输入压缩后反而变大(116%/119%),这是 brotli 对超短数据的正常开销,不是 bug;
  • 空数据压缩后是固定的 3 字节头部 6b 00 03,解压还是空。

鸿蒙页面 Index.ets 展示的就是同一组对照数据,只不过换了个呈现方式——在设备屏幕上显示。

5.3 设备侧:ohosTest 单元测试

测试用例 entry/src/ohosTest/ets/test/Brotli.test.ets 已经写好了,覆盖了版本号非空、高重复文本无损且压缩变小、空输入、多字节 UTF-8、不同质量等级。因为 Native .so 只能在真机或模拟器上加载,这个测试必须在设备侧跑:

# 1) 启动鸿蒙模拟器(DevEco Studio 的 Device Manager 创建并启动 AVD)
#    或连接真机后确认:
hdc list targets         # 应列出设备而非 [Empty]

# 2) 在 build-profile.json5 配好 signingConfigs(DevEco 自动签名或手动证书)

# 3) 运行测试
hvigorw --mode module -p module=entry@ohosTest -p product=default assembleHap
hdc install entry/build/.../entry-ohosTest-signed.hap
# 或直接在 DevEco Studio 中右键 Brotli.test.ets → Run

编译期验证(§5.1)和逻辑验证(§5.2)已经足够说明适配没问题,ohosTest 只是把同一套 ArkTS 调用搬到设备上再跑一遍。

5.4 设备侧:在鸿蒙模拟器上成功运行

应用已经部署到鸿蒙模拟器并成功启动。下图是 DevEco/hvigor 的部署日志:hdc file send 推送 HAP、hdc shell aa start 拉起 EntryAbility,最后 com.jiujiang.rust successfully launched——说明加载 libbrotli_ohos.so 的 ArkTS 页面在真实鸿蒙运行时里跑起来了:

应用在鸿蒙模拟器上成功部署运行


6. 复现命令清单

# 环境
export OHOS_NDK_HOME=~/command-line-tools/sdk/default/openharmony
export DEVECO_SDK_HOME=~/command-line-tools/sdk
ohrs doctor

# 适配 & 编译 Native 库
cd brotli_ohos && ohrs build --release && cd ..

# 集成
cp brotli_ohos/dist/arm64-v8a/libbrotli_ohos.so   entry/libs/arm64-v8a/
cp brotli_ohos/dist/armeabi-v7a/libbrotli_ohos.so entry/libs/armeabi-v7a/
cp brotli_ohos/dist/x86_64/libbrotli_ohos.so      entry/libs/x86_64/
ohpm install

# 编译期验证:打 HAP
hvigorw --mode module -p module=entry@default -p product=default assembleHap --no-daemon

# 逻辑运行验证
cargo run --manifest-path verify_host/Cargo.toml --release

7. 踩坑笔记

  1. OHOS_NDK_HOME 要指到 openharmony,不是它下面的 native。指错了 ohrs 会拼成 native/native/...,报 NotFound,而且 ohrs doctor 发现不了这个问题(它只检查变量是不是空的)。

  2. wrapper 模式是零侵入适配的关键。rust-brotli 源码完全不动,所有适配逻辑集中在 brotli_ohos 里。后面要升级 brotli 版本或者换别的纯 Rust 库照着这个模式来就行了。

  3. Buffer.d.ts 里自动映射成 ArrayBuffer,ArkTS 侧直接用 ArrayBuffer,不需要额外转换。

  4. 工具链和工程 API 版本要对齐。本机是 API 24 工具链,工程初始是 API 26,得把 modelVersion 降到 6.1.1 才能用命令行 hvigorw 构建。这只是工程配置调整,不算源码改动。

  5. 设备侧测试需要签名 + 连接设备。没设备的时候用主机端同逻辑的程序验证压缩正确性就行,没必要死等设备。


8. 把这套方法沉淀成了 Skill

这次适配的完整方法(环境安装、wrapper 模式、交叉编译、工程集成、验证、踩坑解决)已经抽象成一个通用 Skill,存在项目根目录:

skills/rust-ohos-adaptation/
├── SKILL.md                   # 主流程(泛化为「任意纯 Rust 库零侵入鸿蒙化」)
└── references/
    └── troubleshooting.md     # 错误对照表(报错原文 → 原因 → 解决)

下次再适配别的 Rust 库(不只是 brotli)就能直接复用,不用从头翻这份文档。

8.1 在 Claude Code 里调用

Skill 需要被 Claude Code 识别到才能调用,两种方式:

# 方式一:软链到 Claude Code 的 skill 目录(推荐,改一处两处同步)
mkdir -p .claude/skills
ln -s ../../skills/rust-ohos-adaptation .claude/skills/rust-ohos-adaptation

# 方式二:直接复制一份
cp -r skills/rust-ohos-adaptation .claude/skills/

放好后在对话里这样触发:

  • 显式调用:输入 /rust-ohos-adaptation(斜杠 + skill 名);
  • 自动触发:描述任务时带上关键词即可,Claude 会按 SKILL.mddescription
    自动匹配,例如「把 xxx 这个 Rust 库适配到鸿蒙」「ohrs build 报 No package found 怎么办」。

8.2 不接 Claude Code,当文档手册用

SKILL.md 本身就是一份可独立阅读的 Markdown 操作手册——直接打开按章节照做即可,
遇到报错去 references/troubleshooting.md 里搜关键字(如 modelVersionNo package found
permission denied@Builder)。

注意:放在项目根目录 skills/ 是作为仓库内归档保存;要让 Claude Code 真正加载,
需按 §8.1 放到 .claude/skills/ 下(或在 settings 里把 skills/ 注册为 skill 源)。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐