在鸿蒙 PC 上用 Rust 写一个 2048:ArkTS 负责界面,Rust 负责灵魂
欢迎加入开源鸿蒙PC社区: https://harmonypc.csdn.net/
一次完整的「Rust 逻辑 + ArkTS 渲染」鸿蒙化实践,以 HarmonyOS PC(2in1)为目标设备:
同一套代码,最终在鸿蒙桌面上以一个原生窗口应用的形态跑起来。

上面这张图就是我们要做的东西:一个跑在鸿蒙 PC(2in1 形态)上的 2048。
它有标题栏、有最小化/最大化/关闭按钮、待在任务栏里——是一个桌面窗口应用,
而不是手机 App 被拉伸到大屏。但有意思的是:它的全部游戏规则,一行都没写在 ArkTS 里,
而是用 Rust 实现、通过 ohos.rs 编译成鸿蒙原生 .so 模块,再由 ArkTS 调用。
这篇文章记录从零环境到在鸿蒙 PC 上跑起来的完整过程,包括踩过的坑。
为什么是「Rust 逻辑 + ArkTS 界面」
鸿蒙应用的主力语言是 ArkTS(声明式 UI,类 TypeScript)。但很多场景下,
我们希望把核心逻辑用一门更适合写算法、可跨端复用、性能可控的语言来写——Rust 是理想选择:
- 游戏规则、状态机这类纯逻辑,用 Rust 写既安全又好测,还能脱离 UI 单独跑单元测试;
- 同一份 Rust 逻辑未来可复用到其它平台;
- 鸿蒙提供 N-API(Node-API)桥接,ohos.rs 把 napi-rs 适配到了鸿蒙,
让 Rust 函数可以像 C++ native 模块一样被 ArkTSimport。
整体架构是一条清晰的分层:

- 逻辑层:纯 Rust,编译为
cdylib,导出一个Game类。 - 桥接:
napi-ohos/napi-derive-ohos把 Rust 导出为 N-API 模块,并自动生成index.d.ts。 - UI 层:ArkTS 调用
.so,负责画面、触控/鼠标手势与动画。 - 目标设备:HarmonyOS 2in1(PC),API 24。
一、环境:不装 DevEco Studio,用 ark-cli 一条命令搞定
本次在 Windows 11 上开发。已有 Rust 1.96 与 Node,但没有鸿蒙 SDK/NDK。
这里没有装庞大的 DevEco Studio,而是用轻量命令行工具 ark-cli(二进制 ark,内嵌 hdc)管理工具链。
ark-cli 是什么
ark-cli 是 坚果派(nutpi) 开源的轻量级 HarmonyOS/OpenHarmony 命令行脚手架(用 Rust 编写),
把鸿蒙开发常用的几件事收进了一个 ark 命令里:
- 统一运行时:一条命令下载并管理 SDK / NDK / hvigor / ohpm / node / Emulator,全部装到
~/.ark-cli/runtime/,不必装动辄数 GB 的 DevEco Studio; - 应用构建运行:
ark build/ark run一条龙构建 → 安装 → 启动 HAP; - 模拟器管理:拉取系统镜像、创建/启动/停止模拟器实例(手机、平板、2in1 PC);
- 内嵌 hdc:
ark hdc <args>完全透传 hdc,设备调试、日志、文件收发都能用。
对「在 Windows 上、面向鸿蒙 PC、还要交叉编译 Rust」这种场景特别合适——环境干净、可脚本化、命令行就能跑通全流程。
仓库地址:https://atomgit.com/nutpi/ark-cli (含完整文档与 Release 预编译包)
安装 ark-cli
仓库自带安装脚本,克隆后一键编译装入 PATH(需要本机有 Rust 工具链;没有可先 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh):
$ git clone https://atomgit.com/nutpi/ark-cli.git
$ cd ark-cli
$ bash scripts/install.sh # 编译 release 并装入 PATH,创建 ark 软链
$ ark --version # 验证本体就绪
不想自己编译,也可以直接从 Releases 下载对应平台的预编译包,
解压后把ark-cli放进 PATH 即可。
本体装好后,关键的一步是再装「统一运行时」(SDK/NDK/hvigor/ohpm/node/Emulator),否则 build/run/emulator 都没法工作:
$ ark runtime install # 下载 SDK + NDK + hvigor + ohpm + node + Emulator 到 ~/.ark-cli/runtime
$ ark runtime status
✅ SDK: ...\runtime\sdk\default\openharmony (API 24, 6.1.1.125)
✅ Emulator / hvigorw / ohpm / node / hdc 均就绪
NDK 就在 SDK 里:...\openharmony\native,自带三平台 clang
(aarch64/armv7/x86_64-unknown-linux-ohos-clang)。
接着装 Rust 的鸿蒙 target 和 ohos.rs 的构建工具 ohrs:
$ rustup target add aarch64-unknown-linux-ohos armv7-unknown-linux-ohos x86_64-unknown-linux-ohos
$ cargo install ohrs
坑①(网络):
cargo install ohrs多次因 crates.io 下载超时失败。
配置rsproxy.cn镜像(~/.cargo/config.toml用 sparse 协议)后,40 秒装好。
国内开发鸿蒙 + Rust,几乎必配镜像。
| target | 鸿蒙 ABI | 用途 |
|---|---|---|
aarch64-unknown-linux-ohos |
arm64-v8a |
arm64 真机 |
armv7-unknown-linux-ohos |
armeabi-v7a |
32 位真机 |
x86_64-unknown-linux-ohos |
x86_64 |
模拟器 / 2in1 PC |
注意最后一行:鸿蒙 PC(2in1)模拟器是 x86_64,所以我们重点关心 x86_64 那份 .so。

二、逻辑层:纯 Rust 实现 2048
用 ohrs init rust2048 生成一个 napi 项目。为了能脱离 napi 单测,
把纯逻辑放在 game_core.rs(不依赖 napi),lib.rs 只做 napi 绑定。
坑②:模块不要命名为
core,会与 Rust 标准库corecrate 冲突,改名game_core。
2048 的核心技巧:四个方向的移动都能归约为「把一条线向起点滑动」——压缩去空格、相邻相等合并、再补零。
四个方向通过不同的「展平下标顺序」复用同一套行处理逻辑。
关键设计:move 不只返回「是否移动」,而是返回一份移动方案 MovePlan,
精确描述每个数字块 from → to、是否合并、本步新生成哪一格。
这是后面能做出「滑动动画」的前提——UI 不需要自己推断每个块怎么动,Rust 直接告诉它。
#[napi(object)]
pub struct TileMove { pub from: u32, pub to: u32, pub value: u32, pub merged: bool }
#[napi(object)]
pub struct MovePlan {
pub moved: bool,
pub moves: Vec<TileMove>, // 每个块的移动
pub spawn_index: i32, // 新格下标,-1 表示无
pub spawn_value: u32,
pub score: u32,
pub won: bool,
pub over: bool,
}
#[napi]
impl Game {
#[napi(js_name = "move")]
pub fn make_move(&mut self, dir: u32) -> MovePlan { /* ... */ }
}
纯逻辑用 rustc --test 直接在宿主机验证(napi crate 因缺 libnode.dll 无法在 Windows 本机编译,
所以逻辑必须拆出来才能测):
$ rustc --test --edition 2021 src/game_core.rs -o core_test.exe && ./core_test.exe
test result: ok. 6 passed; 0 failed
三、交叉编译为鸿蒙 .so
ohrs 调用 OHOS NDK 的 clang,一次产出三种 ABI 的 .so 和 TS 声明:
$ export OHOS_NDK_HOME="C:/Users/mangge/.ark-cli/runtime/sdk/default/openharmony"
$ ohrs build --release
Finished `release` profile [optimized]
Create index.d.ts succeed.
坑③:
OHOS_NDK_HOME要指向.../openharmony(SDK 这一层),
不是.../openharmony/native——ohrs 会自己拼native/build/cmake/ohos.toolchain.cmake。
产物:
dist/
├─ arm64-v8a/librust2048.so
├─ armeabi-v7a/librust2048.so
├─ x86_64/librust2048.so ← 鸿蒙 PC / 模拟器用这份
└─ index.d.ts ← 自动生成,含 Game / MovePlan / TileMove
接入鸿蒙工程:.so 放进 entry/libs/<abi>/,index.d.ts + 一个 oh-package.json5
放进 entry/src/main/cpp/types/librust2048/,并在 entry/oh-package.json5 声明依赖。
ArkTS 端即可:
import { Game, MovePlan, TileMove } from 'librust2048.so';
const game = new Game();
四、UI 层:让数字块「滑」起来
最难的一关在 UI。第一版用 ForEach 渲染棋盘,结果遇到了 ArkUI 的经典坑:
坑④(只有分数变,格子不动):
ForEach只在 key 变化时才重建子组件。
最初 key 用纯位置${r}-${c},数值变了 key 不变,格子永远不刷新;分数因为是直接绑定@State才会动。
把数值编进 key 能让格子刷新,但那只是「原地切换」,没有滑动。要做出数字块滑过格子的动画,
必须换架构——这也是本项目最关键的一招:
用 @Observed 类 + 子组件 @ObjectLink,让组件身份稳定、属性原地可变。
ForEach按稳定id复用组件(不重建 → 不会丢失位置);@ObjectLink直接观察对象属性的原地修改,绕过ForEach的「key 不变就不更新」;- 每个数字块用绝对坐标
.position()摆放,改index→ 在animateTo中触发position平滑过渡 = 滑动。
@Observed class TileVM { id: number; value: number; index: number; willMerge: boolean }
@Component struct TileView {
@ObjectLink tile: TileVM;
build() {
Stack() { Text(this.tile.value.toString())/* ... */ }
.position({ x: cellX(this.tile.index), y: cellY(this.tile.index) }) // index 变即位移
.transition(TransitionEffect.OPACITY.combine(TransitionEffect.scale({ x: 0.3, y: 0.3 }))
.animation({ duration: 140, curve: Curve.EaseOut })) // 出生/消亡弹入弹出
}
}
一次移动分两阶段,靠 Rust 给的 MovePlan 驱动:

const plan = this.game.move(dir);
if (!plan.moved) return;
// 阶段一:滑动——animateTo 里把每个块的 index 改到目标格,position 平滑过渡
animateTo({ duration: 110, curve: Curve.EaseInOut, onFinish: () => this.afterSlide(plan) }, () => {
this.tiles.forEach(t => { const m = moveByFrom.get(t.index); if (m) { t.willMerge = m.merged; t.index = m.to; } });
this.score = plan.score;
});
// 阶段二(onFinish):移除被合并块、目标格数值翻倍、push 新生成块(淡入弹出)
效果:两个相同数字块滑到同一格叠在一起,其一淡出、另一翻倍,新格弹入。
坑⑤(ArkTS 严格语法):
CompileArkTS比普通 TS 严格——new Map()/new Set()必须带泛型;对从.so导入的Array<TileMove>用for...of
元素会被推成any而报错,改用arr.forEach((m: TileMove) => …)显式标注即可。
五、跑到鸿蒙 PC(2in1)上
重点来了。手机模拟器只是顺带,本项目的目标是鸿蒙 PC(2in1)形态。
2in1 镜像默认没下载,先拉镜像、建实例、启动:
# 镜像约 2.35 GB,注意 (24) 必须带引号,否则 shell 会把括号当通配符
$ ark emulator image-install --device-type "2in1" --os-version "HarmonyOS 6.1.1(24)"
$ ark emulator create --device-type "2in1" --os-version "HarmonyOS 6.1.1(24)" pc2in1
$ ark emulator start pc2in1
$ ark devices # 🟢 已连接
坑⑥(装不上 2in1):
module.json5的deviceTypes必须包含目标设备类型,
否则该类型设备拒绝安装。本项目设为["phone", "tablet", "2in1"]。
然后一条命令构建 → 安装 → 启动:
$ ark run
✅ 构建完成!
✅ 安装成功!
🚀 应用已启动: com.example.game2048
因为 2in1 也是 x86_64,直接复用 libs/x86_64/librust2048.so,Rust 侧零改动。
最终就是开头那张图——一个跑在鸿蒙桌面上的窗口化 2048:

在 PC 形态下,应用以独立窗口运行,标题栏、窗口控件、任务栏一应俱全;
游戏照常用方向手势(触控板/鼠标拖拽)滑动合并,分数实时更新,数字块平滑滑动。
总结:一套逻辑,多端形态
| 维度 | 选择 |
|---|---|
| 逻辑层 | Rust(cdylib + napi-ohos),6 个单测覆盖 |
| 桥接/构建 | ohos.rs 的 ohrs,交叉编译 + 自动生成 .d.ts |
| UI 层 | ArkTS:@Observed/@ObjectLink + 绝对定位 + animateTo 滑动动画 |
| 工具链 | ark-cli(SDK/NDK/hvigor/ohpm/Emulator/hdc 一体,免 DevEco) |
| 目标设备 | HarmonyOS 2in1(PC),API 24(同一 HAP 也兼容手机) |
整条链路验证下来,结论很清晰:逻辑用 Rust、界面用 ArkTS 在鸿蒙上是完全可行的工程方案,
而 ohos.rs + ark-cli 让它在 Windows 上、面向鸿蒙 PC,也能顺畅落地。
踩坑清单(给后来者省时间):
- 国内必配
rsproxy.cn镜像,否则cargo install ohrs大概率超时; - Rust 模块别叫
core;纯逻辑拆出来才能在宿主机单测; OHOS_NDK_HOME指到openharmony这一层;- ArkUI
ForEach的 key 必须含变化数据,要做滑动动画则改用@Observed+@ObjectLink+绝对定位; - ArkTS 严格模式:
new Map<K,V>()、forEach((x: T) => …); - 上 2in1 记得在
module.json5的deviceTypes里加"2in1"。
更多推荐




所有评论(0)