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

在鸿蒙 PC(2in1)上运行的 2048

上面这张图就是我们要做的东西:一个跑在鸿蒙 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 模块一样被 ArkTS import

整体架构是一条清晰的分层:

整体架构:ArkTS 负责界面,Rust 负责逻辑

  • 逻辑层:纯 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);
  • 内嵌 hdcark 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

ark-cli 统一运行时与 Rust 鸿蒙 target


二、逻辑层:纯 Rust 实现 2048

ohrs init rust2048 生成一个 napi 项目。为了能脱离 napi 单测
把纯逻辑放在 game_core.rs(不依赖 napi),lib.rs 只做 napi 绑定。

坑②:模块不要命名为 core,会与 Rust 标准库 core crate 冲突,改名 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 驱动:

一次移动的两个阶段:由 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.json5deviceTypes 必须包含目标设备类型,
否则该类型设备拒绝安装。本项目设为 ["phone", "tablet", "2in1"]

然后一条命令构建 → 安装 → 启动:

$ ark run
  ✅ 构建完成!
  ✅ 安装成功!
  🚀 应用已启动: com.example.game2048

因为 2in1 也是 x86_64,直接复用 libs/x86_64/librust2048.so,Rust 侧零改动
最终就是开头那张图——一个跑在鸿蒙桌面上的窗口化 2048:

鸿蒙 PC 上的 2048 游戏过程

在 PC 形态下,应用以独立窗口运行,标题栏、窗口控件、任务栏一应俱全;
游戏照常用方向手势(触控板/鼠标拖拽)滑动合并,分数实时更新,数字块平滑滑动。


总结:一套逻辑,多端形态

维度 选择
逻辑层 Rust(cdylib + napi-ohos),6 个单测覆盖
桥接/构建 ohos.rsohrs,交叉编译 + 自动生成 .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,也能顺畅落地。

踩坑清单(给后来者省时间):

  1. 国内必配 rsproxy.cn 镜像,否则 cargo install ohrs 大概率超时;
  2. Rust 模块别叫 core;纯逻辑拆出来才能在宿主机单测;
  3. OHOS_NDK_HOME 指到 openharmony 这一层;
  4. ArkUI ForEach 的 key 必须含变化数据,要做滑动动画则改用 @Observed+@ObjectLink+绝对定位;
  5. ArkTS 严格模式:new Map<K,V>()forEach((x: T) => …)
  6. 上 2in1 记得在 module.json5deviceTypes 里加 "2in1"

Logo

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

更多推荐