【开源软件移植】把 RustDesk 的 Rust 核心搬到 HarmonyOS PC:一次 Native HAR 迁移实战记录
【开源软件移植】把 RustDesk 的 Rust 核心搬到 HarmonyOS PC:一次 Native HAR 迁移实战记录
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
本文仅聚焦 HarmonyOS PC 场景,不展开手机、平板等其他设备类型。
本文基于实际迁移结果编写,宿主机环境为 Arch Linux,构建链路采用 Linux 命令行方式。
本文重点记录 Rust / Native 迁移适配过程,ArkTS UI 层只作为消费侧验证入口,不展开页面设计。
三方库仓库:https://atomgit.com/FrankHan2004/RustDeskHar
鸿蒙App仓库:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_rustdesk
文中后续文件路径均以
RustDeskHar仓库根目录为基准,也就是下文提到的<repo-root>。如果你参考本文迁移 EasyTier 或其他 Rust / Native 项目,
<repo-root>就替换成对应项目在本机上的实际存储路径。Rust 基础环境配置参考官方安装页:
https://rust-lang.org/zh-CN/tools/install/ohos-rs / ohrs 快速开始文档:
https://ohos.rs/docs/basic/quick-start.htmlHarmonyOS command-line-tools 下载地址:
https://developer.huawei.com/consumer/cn/download/command-line-tools-for-hmos
写在前面
这次做的事情,简单说就是:把 RustDesk 里已经很成熟的 Rust 远控核心,搬到 HarmonyOS PC 上跑起来,然后封装成一个 Native HAR 给鸿蒙应用用。
一开始我也以为这件事会比较“直线”:
拉 RustDesk 源码
↓
改一下 target
↓
cargo build
↓
打成 HAR
↓
鸿蒙 PC 上跑起来
但真正开始做以后就发现,事情没有这么简单。RustDesk 不是一个小库,它是一个完整的远程桌面项目,里面有桌面端逻辑、Flutter 逻辑、编解码逻辑、输入逻辑、平台相关逻辑,还有一大堆三方依赖。把它搬到 HarmonyOS PC 上,不是简单“换个编译目标”就完事。
这篇文章我不想写成那种特别像文档的“第一步、第二步、第三步”说明书。因为单看命令其实没什么难的,真正有价值的是中间的判断过程:
- 为什么不直接在 ArkTS 里重写远控逻辑;
- 为什么最终选择 Native HAR 作为交付形态;
- 为什么 OHOS 不能简单当成普通 Linux;
- 哪些 RustDesk 能力应该保留,哪些应该先隔离;
- 视频解码为什么要单独做 OHOS 适配;
- 为什么不能只看到画面出来就觉得迁移完成了。
所以这篇更像一篇迁移手记:把我这次适配 RustDesk 到 HarmonyOS PC 的过程、取舍和踩坑都整理出来。命令会给,但重点不是复制命令,而是理解这条迁移路线为什么这么走。
源码开源仓库:https://atomgit.com/FrankHan2004/RustDeskHar
下面就按这次真实迁移的顺序展开:先讲目标和架构,再讲 OHOS 条件编译、条件依赖、视频链路、HAR 封装,最后再做一轮踩坑复盘。希望后面做其他 Rust / Native 项目鸿蒙 PC 迁移时,也能直接拿这套思路做参考。
这里也先说明一下边界:本文不是说 RustDesk 原有桌面端功能已经 100% 完整迁移到了 HarmonyOS PC。当前重点是 RustDesk Rust 核心远控链路、OHOS 条件编译、视频解码、Surface 输出、输入调用和 Native HAR 封装这些关键部分。像完整桌面端 UI、托盘、所有设置项、全部平台能力和完整产品体验,还需要后续继续补。
如果你时间比较紧,可以按下面的方式读:
| 你的目的 | 建议优先看 |
|---|---|
| 想知道这次到底迁了什么 | 第一、二、三节 |
| 想参考方法迁移其他 Rust 项目 | 第四节和第十一节 |
| 正在处理 OHOS 条件编译和依赖问题 | 4.2 到 4.7 |
| 只想复现构建产物 | 第九节 |
| 已经跑起来但画面或输入不对 | 第六、八、十节 |
这样读会轻松一点,不需要一开始就被所有命令和路径淹没。
先放最终效果图,至少说明这条路是跑通了的:

一、先想清楚:我们到底要迁移什么
RustDesk 本身已经有成熟的 Rust 核心:会话管理、认证流程、视频协议、输入事件、编解码协商等都在上游工程里存在。如果为了适配鸿蒙 PC 重新在 ArkTS 侧实现这些逻辑,成本高,也很难持续跟随上游。
这里其实有一个很现实的问题:做开源软件移植时,到底是“重写一个像它的应用”,还是“尽量把原项目的核心搬过来”。
如果只是做一个 Demo,当然可以在 ArkTS 里写几个页面,再模拟一些状态,看起来也能像一个远控应用。但 RustDesk 这种项目最值钱的地方不在 UI,而在它后面的远控协议、连接流程、认证流程、编解码协商、输入事件处理这些东西。把这些全部重写一遍,不仅工作量大,而且以后 RustDesk 上游一更新,我们这边基本没法跟。
所以我一开始就把目标定得比较明确:不要重新发明一个 RustDesk,而是尽量把 RustDesk 现有的 Native/Rust 核心搬到 HarmonyOS PC 上。
本次迁移的目标定得比较克制:
- 保留 RustDesk 上游 Rust 核心实现;
- 让核心代码能通过
aarch64-unknown-linux-ohos目标编译; - 为 HarmonyOS PC 补齐必要的平台分支;
- 打通会话、登录、视频解码、Surface 绑定和输入调用链;
- 把 Native 能力封装为标准 HAR,让鸿蒙 PC 工程通过
ohpm install接入。
也就是说,这次迁移更像是先把“发动机”和“传动链路”搬过来,而不是一次性把原 RustDesk 桌面端所有功能、所有界面、所有平台能力都搬完。这个边界要先讲清楚,不然后面很容易误解成“RustDesk 全功能鸿蒙 PC 版已经完整完成”。
这里有一个关键判断:迁移的第一阶段不追求完整桌面端功能对齐,而是优先让远控核心链路闭环。
远控软件的核心闭环是:
创建会话
↓
连接远端
↓
完成认证
↓
协商视频 codec
↓
解码并输出到 Surface
↓
发送键鼠输入
↓
持续观察分辨率、帧率、质量和连接状态
只要这条链路成立,后续 UI、交互、配置项都可以逐步补齐。反过来,如果 Native 核心没有跑通,页面做得再完整也只是空壳。
这也是我这次适配里反复提醒自己的一点:不要被“页面上看起来像不像 RustDesk”带偏。远控类项目只看页面没意义,真正要看的,是连接是不是真的建立了,认证是不是真的过了,画面是不是从远端解出来的,输入是不是确实发回远端了。
换句话说,第一阶段的目标不是“做一个漂亮客户端”,而是先把下面这条链路打穿:
RustDesk 核心能编译
↓
RustDesk session 能创建
↓
远端能连上
↓
认证能通过
↓
远端画面能解码
↓
Surface 能显示
↓
输入事件能回传
这条线只要断一处,迁移就还没有真正完成。
二、最终跑通的结果
当前仓库已经能够生成以下产物:
dist/arm64-v8a/librustdesk_native_har.sopackage/libs/arm64-v8a/librustdesk_native_har.sopackage/libs/index.d.tspackage.har
这说明 RustDesk 的 Native 核心已经完成 HarmonyOS 动态库构建,并被打包为可供鸿蒙 PC 工程引用的 HAR 包。
这里多说一句,为什么我一直强调 package.har,而不是强调某个 .so。
在 Native 迁移里,单独编出一个 .so 其实只是中间结果。如果后续鸿蒙应用每次都要手工复制 .so、手工处理声明文件、手工替换工程里的 native 库,那这个项目很快就会变得很难维护。尤其是 RustDesk 这种项目,后面一定会继续改 Native 代码,继续调 codec、输入、分辨率、性能参数。如果每次更新都靠手工复制文件,迟早会出现“我以为装的是新版本,其实应用里还是旧库”的问题。
所以这次迁移从一开始就把最终形态定成 HAR:Native 层负责构建和封装,鸿蒙应用工程只通过 ohpm install package.har 来消费它。这样后续升级也比较清楚:重新构建 HAR,重新安装 HAR,再重新构建 HAP。



这里的重点不是“生成了一个 .so”,而是形成了一条可复用的工程链路:
RustDesk 上游 Rust 核心
↓
OHOS 条件编译与平台补丁
↓
HarmonyOS codec / Surface / input 适配
↓
NAPI 导出层
↓
Native HAR 打包
↓
鸿蒙 PC 应用工程接入
从结果上看,最终产物只有几项;但从迁移过程看,中间其实解决了不少问题:Rust target、依赖补丁、平台分支、codec 能力检测、Surface 输出、NAPI 导出、HAR 打包和真机验证。后面会按这个顺序展开。
三、整体架构与迁移思路
迁移过程中最重要的设计取舍,是不要把所有代码都堆进应用工程,也不要直接把 RustDesk 内部对象暴露给 ArkTS。
这一点是边做边越来越明确的。刚开始很容易有一种冲动:哪里编不过就在哪里改,哪里需要一个接口就直接从 RustDesk 内部拿。短期看这样确实快,但过几天项目就会变成一团乱麻:ArkTS 里知道 RustDesk 内部结构,Native 层又知道应用页面状态,third_party 里还混着一堆临时逻辑。
所以后面我把结构重新想了一遍:RustDesk 是上游核心,OHOS 是平台适配,HAR 是交付边界。三者要分开。
最终采用三层结构:
RustDeskHar/
├── third_party/rustdesk/ # RustDesk 上游源码快照和 OHOS 适配补丁
├── src/ # Native HAR 的 NAPI 导出入口
├── package/ # HAR 打包目录
├── dist/ # Native 构建产物
├── build.rs # 构建期标记与 NAPI 初始化
└── Cargo.toml # cdylib、依赖和 patch 配置
对应关系如下:
┌──────────────────────────────┐
│ RustDesk 上游源码快照 │
│ third_party/rustdesk/ │
└──────────────┬───────────────┘
│ 条件编译 / 依赖修补 / 平台裁剪
▼
┌──────────────────────────────┐
│ OHOS 平台适配层 │
│ scrap/common/ohos/ │
│ avcodec / vpxcodec / aom │
└──────────────┬───────────────┘
│ session / decoder / surface / input
▼
┌──────────────────────────────┐
│ Native HAR 封装层 │
│ src/lib.rs + package/ │
└──────────────┬───────────────┘
│ NAPI / HAR
▼
┌──────────────────────────────┐
│ HarmonyOS PC 应用工程 │
│ ArkTS 只消费稳定接口 │
└──────────────────────────────┘
这个结构解决了三个问题:
- 上游 RustDesk 代码可以作为
third_party快照持续维护; - OHOS 相关改动集中在平台分支和 Native 导出层,避免污染应用工程;
- 消费侧只依赖 HAR,不需要理解 RustDesk 内部复杂对象。
迁移类项目很容易陷入“哪里能改就在哪里改”的状态。这里强制把边界划清楚,是为了后续可维护性:上游更新时,优先更新 third_party/rustdesk,消费侧应用只替换 HAR。
换成更口语的说法就是:
- RustDesk 该干 RustDesk 的事,负责远控核心;
- OHOS 适配层该干平台的事,负责 codec、Surface、条件编译这些差异;
- HAR 该干封装的事,给外面一个相对稳定的调用入口;
- 鸿蒙应用工程就别再钻进 RustDesk 内部了,只负责调用接口和展示结果。
这样看起来多了一层封装,但后面调试时会轻松很多。比如输入有问题,就看 ArkTS 到 NAPI,再看 NAPI 到 RustDesk session;codec 有问题,就看 OHOS codec capability 和 decoder;HAR 更新有问题,就看 ohrs artifact 和 ohpm install。问题边界会清楚很多。
3.1 实际迁移关键路径
本次适配不是一步到位,而是按下面这条关键路径推进:
确认交付形态:Native HAR,而不是散装 .so
↓
固定 OHOS NDK、linker、Rust target
↓
让 RustDesk 先通过 aarch64-unknown-linux-ohos 编译
↓
修补或 vendor 关键 Rust 依赖
↓
用 target_env = "ohos" 隔离桌面端平台能力
↓
补齐 HarmonyOS codec capability 和 decoder 创建逻辑
↓
把视频输出收敛到 Surface 绑定路径
↓
设计 NAPI session API,隐藏 RustDesk 内部对象
↓
用 ohrs 生成 package.har
↓
在真实鸿蒙 PC 工程里验证连接、画面和输入
这条路径里最容易被低估的是中间几步:依赖补丁、平台分流和视频输出。它们都不是“接入 HAR”阶段能补救的问题,必须在 Native 核心迁移时就处理好。
如果用一句话概括这条路,就是:先让 RustDesk 核心在 OHOS 环境下站住,再考虑怎么被鸿蒙应用舒服地使用。
我一开始也尝试过从“应用接入”角度倒推,比如先写 ArkTS 页面,先把 HAR 装进去,先做几个按钮。但很快就会发现,按钮点下去以后真正的问题还在 Native 里:session 有没有建起来?RustDesk 的 IO loop 有没有跑?codec 能力是不是正确?Surface id 有没有传到底层?这些不解决,上层页面改多少都没用。
所以后面就把顺序调整成了“底层优先”:
先 cargo check
再 ohrs build
再 ohrs artifact
再 ohpm install
最后真机连远端测完整链路
这个顺序虽然慢一点,但每一步都能明确告诉你问题在哪一层。
这一节先记住一个结论就够了:RustDeskHar 不是把 RustDesk 整个桌面端硬塞进鸿蒙工程,而是把 RustDesk 的 Rust 核心、OHOS 平台适配和 HAR 封装拆成三层来做。后面的工具链、条件编译、codec 和 NAPI,都是围绕这个结构展开的。
四、工具链先站稳:让 RustDesk 认识 OHOS target
4.1 工具链基准
本文实测环境如下:
| 项目 | 实际值 |
|---|---|
| 宿主机 | Arch Linux |
| Rust | rustc 1.94.0 |
| cargo | 1.94.0 |
| ohrs | 1.3.1 |
| ohpm | 6.1.1.816 |
| HarmonyOS SDK 根目录 | <command-line-tools-root>/sdk/default/openharmony |
| Rust target | aarch64-unknown-linux-ohos |
| 目标架构 | arm64-v8a |
| 目标场景 | HarmonyOS PC |
如果你的 Rust 基础环境还没搭好,建议先看 Rust 官方安装页。本文不重复讲 rustup 的完整安装过程:
https://rust-lang.org/zh-CN/tools/install/
如果你之前没有用过 ohos-rs / ohrs,也建议先看官方快速开始文档。里面讲了 Rust target、HarmonyOS NDK、OHOS_NDK_HOME 和 ohrs 的基本使用:
https://ohos.rs/docs/basic/quick-start.html
HarmonyOS command-line-tools 可以从华为开发者官网下载:
https://developer.huawei.com/consumer/cn/download/command-line-tools-for-hmos
下载完成后,把它解压到你希望存放 SDK 的目录。这个解压后的目录就是本文里的 <command-line-tools-root>。后文不要直接复制 <command-line-tools-root> 这个字符串,而是替换成你机器上的实际路径。
至少先准备好这些东西:
- Rust / cargo;
- HarmonyOS command-line-tools;
- OHOS SDK / NDK;
ohpm;hdc;- 能正常构建和安装的鸿蒙 PC 应用工程。
这里再解释几个后文会反复出现的路径占位符。
| 名称 | 在本文中的含义 | 举例说明 |
|---|---|---|
<repo-root> |
当前 RustDeskHar 仓库根目录 | git clone 后进入的 RustDeskHar 目录 |
<command-line-tools-root> |
command-line-tools 解压后的实际目录 | 下载后你自己选择的 SDK 存放目录 |
OHOS_NDK_HOME |
Rust/clang 实际使用的 OHOS NDK 根目录 | <command-line-tools-root>/sdk/default/openharmony |
<harmony-app-root> |
消费 package.har 的鸿蒙应用工程根目录 |
你的 ArkTS/HAP 工程目录 |
这里最容易混的是 <command-line-tools-root> 和 OHOS_NDK_HOME。前者是 command-line-tools 的解压目录,后者才是 Rust/clang 真正使用的 NDK 根目录:
export OHOS_NDK_HOME="<command-line-tools-root>/sdk/default/openharmony"
<repo-root> 在本文里就是 RustDeskHar 仓库根目录,也就是执行下面命令后进入的目录:
git clone https://atomgit.com/FrankHan2004/RustDeskHar.git
cd RustDeskHar
如果你参考本文迁移 EasyTier 或其他项目,<repo-root> 就换成对应项目在你机器上的实际根目录。
再说一下 ohrs。本文里用到的 ohrs,可以理解为 Rust 到 HarmonyOS HAR 的构建和打包工具。它负责把 Rust Native 工程编译成 HarmonyOS 可用的原生库,并通过 ohrs artifact 生成最终给 ArkTS 工程安装的 package.har。
如果本机还没有安装 ohrs,可以先通过 cargo 安装:
cargo install ohrs
如果安装后提示找不到 ohrs 命令,先确认 cargo 的 bin 目录已经在 PATH 里:
export PATH="$HOME/.cargo/bin:$PATH"
安装后确认版本:
ohrs --version
本文实测使用的是 ohrs 1.3.1。后文真正用到的核心命令只有两条:
ohrs build --release -a aarch
ohrs artifact
第一条负责构建 Rust Native 产物,第二条负责生成最终的 package.har。这也是为什么我后面一直强调:不要只编 .so,也不要手工复制 .so,每次改完 Rust 代码都应该完整跑 ohrs build 和 ohrs artifact。
再显式指定 linker:
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_OHOS_LINKER="$OHOS_NDK_HOME/native/llvm/bin/aarch64-unknown-linux-ohos-clang"
export CARGO_TARGET_AARCH64_LINUX_OHOS_LINKER="$OHOS_NDK_HOME/native/llvm/bin/aarch64-unknown-linux-ohos-clang"
export RUSTFLAGS="-Clinker=$OHOS_NDK_HOME/native/llvm/bin/aarch64-unknown-linux-ohos-clang"
这里的经验是:不要只相信工具自动探测,Native 迁移阶段要把 NDK、sysroot、linker 都显式固定住。
这个地方我踩过一个很典型的坑:看到目录里有 command-line-tools,就下意识觉得它可以直接作为 NDK 根目录用。但真正给 Rust、clang、bindgen 用的路径并不是这一层,而是下面的 sdk/default/openharmony。
如果这里配错,后面的错误会非常绕:有时是 linker 找不到,有时是 sysroot 不对,有时是某个 C/C++ 依赖编译失败。表面看像是 Rust crate 的问题,其实根子是工具链路径没固定好。
所以迁移这种 Native 项目,我现在的习惯是先把三件事确认清楚:
NDK 根目录是哪一个
clang linker 是哪一个
sysroot 是哪一个
只要这三件事不含糊,后面定位问题会少很多噪音。
4.2 target_os 和 target_env 的差异
Rust 的 OHOS target 有一个容易误判的点:
target_os = "linux"
target_env = "ohos"
如果只按 target_os = "linux" 判断,很多桌面 Linux 逻辑会被错误编进 OHOS 产物里。RustDesk 原本包含大量桌面端能力,例如窗口、托盘、输入捕获、平台服务等,这些能力在 HarmonyOS PC Native HAR 场景下不能直接复用。
因此本次适配大量使用:
#[cfg(target_env = "ohos")]
而不是简单把 OHOS 当成普通 Linux。
这是第一类迁移思考:同一个 target triple 里,linux 只说明底层 ABI 接近,不能说明平台能力等价。
这个点很容易被忽略。因为 aarch64-unknown-linux-ohos 里确实有 linux,很多条件编译如果只写 target_os = "linux",OHOS 也会被带进去。问题是 RustDesk 里的 Linux 桌面端逻辑,很多是默认跑在传统 Linux 发行版上的,比如 X11、Wayland、桌面输入、托盘、系统服务等。
HarmonyOS PC 虽然底层 ABI 和 Linux 有关系,但应用模型、窗口模型、输入模型、媒体能力都不是传统 Linux 桌面那套。所以这里不能偷懒,必须认真区分:
target_os = "linux" 只能说明底层目标像 Linux
target_env = "ohos" 才能说明当前是在 OHOS 环境
后面很多“莫名其妙编进了桌面依赖”的问题,本质上都是这个判断没分清。
4.3 先做最小编译闭环
仓库拉取后,先验证 RustDesk 核心能否进入 OHOS 编译链路:
git clone https://atomgit.com/FrankHan2004/RustDeskHar.git
cd <repo-root> # 本文中就是刚克隆出来的 RustDeskHar 目录
export OHOS_NDK_HOME="<command-line-tools-root>/sdk/default/openharmony"
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_OHOS_LINKER="$OHOS_NDK_HOME/native/llvm/bin/aarch64-unknown-linux-ohos-clang"
export CARGO_TARGET_AARCH64_LINUX_OHOS_LINKER="$OHOS_NDK_HOME/native/llvm/bin/aarch64-unknown-linux-ohos-clang"
export RUSTFLAGS="-Clinker=$OHOS_NDK_HOME/native/llvm/bin/aarch64-unknown-linux-ohos-clang"
cargo check --target aarch64-unknown-linux-ohos --lib


这个阶段的目标不是跑通所有功能,而是先证明:
- RustDesk 作为依赖能被当前 HAR crate 拉起来;
- OHOS linker 和 sysroot 能正常工作;
- 不适合 OHOS 的桌面分支已经被条件编译隔离;
- 后续可以进入真实功能适配。
这里建议不要一上来就跑完整 HAR 构建。完整构建链路更长,报错也可能混在一起,不利于定位。
我更推荐先用 cargo check 做一个轻量体检。它能比较快地告诉你:RustDesk 在当前 target 下到底能不能被编译器接受。如果这一步都过不了,就先不要管 HAR、ohpm、ArkTS 页面那些东西,因为问题还在 Rust Native 层。
等 cargo check 能过以后,再进入 ohrs build。这样每一层的问题都比较清楚:
cargo check 失败 多半是 Rust 条件编译或依赖问题
ohrs build 失败 多半是 Native HAR 构建或链接问题
ohrs artifact 失败 多半是 HAR 产物组织问题
ohpm install 失败 多半是消费侧包管理或缓存问题
真机运行失败 再看 session、codec、surface、输入和日志
这种拆法看起来麻烦,但比把所有步骤一次跑完再看一大坨报错要省时间。
4.4 上游依赖不能只等 crates.io 原版
RustDesk 依赖链里有一些 crate 在 OHOS target 下需要本地补丁,例如:
libsodium-sysmagnum-opusrdev
这些依赖如果直接回退到 crates.io 或上游默认版本,容易出现 ohos target 不识别、构建脚本不兼容、桌面输入能力误编译等问题。
因此 HAR crate 通过 [patch] 把关键依赖收敛到本地 vendor 版本,避免构建时绕回未适配版本。
顶层 Cargo.toml 里保留类似这样的补丁入口:
[patch.crates-io]
libsodium-sys = { path = "third_party/rustdesk/vendor/libsodium-sys" }
[patch."https://github.com/rustdesk-org/magnum-opus"]
magnum-opus = { path = "third_party/rustdesk/vendor/magnum-opus" }
[patch."https://github.com/rustdesk-org/rdev"]
rdev = { path = "third_party/rustdesk/vendor/rdev" }
这一步的经验是:迁移复杂 Rust 项目时,不能只看顶层 crate 是否能改通,还要固定住关键 transitive dependency 的来源。
这个坑比较隐蔽,因为有时候你在 third_party/rustdesk 目录里单独改好了,单独构建也像是能过;但回到 HAR 根目录一构建,Cargo 解析依赖时又可能走另一条来源,最后还是把未适配 OHOS 的版本拉进来。
所以这里不是“哪里报错改哪里”就够了,而是要站在最终构建入口看问题:最终产物是从 RustDeskHar 根目录构建的,那顶层 Cargo.toml 就必须把关键依赖来源固定住。这样后面不管是 cargo check、ohrs build,还是 CI/其他机器复现,吃到的都是同一套依赖。
4.5 OHOS 条件编译:不要把平台差异写成到处都是的 if
这一节想多展开一点,因为它对迁移其他 Rust / Native 项目也很有参考价值。
做 OHOS 迁移时,最容易犯的错误是:哪里编不过,就在哪里加一个判断;哪里调用不了,就临时 if ohos 绕一下。短期看能推进,长期会很痛苦,因为平台差异会散落到项目各处,后面谁也不知道哪段代码是通用逻辑,哪段是 OHOS 特殊逻辑。
我更推荐把条件编译当成“分层工具”,而不是“补丁胶水”。大概可以按下面这个顺序拆:
先判断这段能力是不是平台相关
↓
如果平台无关,尽量保持通用代码不动
↓
如果平台相关,抽出统一函数签名
↓
在不同平台下提供不同实现
↓
让上层调用同一个接口,不关心平台细节
比如 RustDesk 里的视频解码就是典型平台相关能力。上层只应该关心“创建 decoder、喂入视频帧、输出画面”,不应该在业务逻辑里到处判断当前是不是 OHOS。
比较好的写法是把平台相关实现收敛到模块里:
#[cfg(target_env = "ohos")]
mod ohos;
#[cfg(not(target_env = "ohos"))]
mod desktop;
或者在同一个抽象下提供不同实现:
#[cfg(target_env = "ohos")]
fn create_platform_decoder(surface_id: u64, width: u32, height: u32) -> Result<Decoder> {
// OHOS 下走系统 AVCodec + Surface 输出
create_ohos_decoder(surface_id, width, height)
}
#[cfg(not(target_env = "ohos"))]
fn create_platform_decoder(surface_id: u64, width: u32, height: u32) -> Result<Decoder> {
// 其他平台继续走原来的桌面端或软件解码路径
create_default_decoder(width, height)
}
这样上层代码只需要调用:
let decoder = create_platform_decoder(surface_id, width, height)?;
它不需要知道 OHOS 下面是 OH_AVCodec_GetCapability,也不需要知道其他平台是 VAAPI、MediaCodec、软件解码,或者 RustDesk 原来的路径。
这就是条件编译最重要的作用:把平台差异挡在边界里面,让通用业务逻辑尽量保持干净。
在 RustDesk 这次迁移里,类似思路用在了几类地方:
- codec 能力检测:OHOS 下查询系统
AVCodec,其他平台继续走原有能力判断; - decoder 创建:OHOS 下按 codec capability 创建系统 decoder,其他平台不受影响;
- Surface 绑定:OHOS 下需要从 ArkTS/XComponent 拿到 surface id,再传给 Native;
- 输入发送:上层仍然是鼠标、键盘、文本输入,底层按 RustDesk session bridge 进入核心;
- 桌面能力裁剪:托盘、桌面输入捕获、窗口管理等不适合 HAR 阶段的能力,不进入 OHOS 构建路径。
迁移其他项目时也可以套这个思路。比如一个项目里有文件系统、窗口、音视频、网络、输入这些能力,不要一上来全局搜索 linux 然后硬改。更稳的方式是先分类:
纯算法 / 纯协议 / 纯数据结构 尽量不改
文件路径 / 配置目录 / 沙箱访问 平台分支
窗口 / Surface / 图形输出 平台分支
音视频编解码 平台分支
输入 / 剪贴板 / 系统托盘 平台分支或先裁剪
构建脚本 / bindgen / cc crate 条件依赖或构建脚本适配
这样做的好处是,迁移不是靠“碰到报错就修”,而是有一张平台差异清单。后面报错时,你能更快判断它属于哪一类。
4.6 条件依赖:该隔离的依赖不要让它进入 OHOS 构建
条件编译解决的是“代码路径”的问题,条件依赖解决的是“哪些 crate 会被拉进来”的问题。
这两个要分开看。很多项目迁移失败,不是因为自己的代码写错,而是因为某个依赖在 OHOS target 下编不过。比如依赖里用了传统 Linux 桌面库,或者 build.rs 里不认识 ohos,或者 native sys crate 默认找错了系统库。
所以迁移时要问一个问题:这个依赖在 OHOS 产物里真的需要吗?
如果不需要,就不要让它进入 OHOS 构建。其他项目可以参考下面这种写法,具体 crate 名和版本号按实际项目填写:
[target.'cfg(not(target_env = "ohos"))'.dependencies]
tray-icon = "x.y.z"
desktop-input-crate = "x.y.z"
[target.'cfg(target_env = "ohos")'.dependencies]
napi-ohos = { version = "1.0", default-features = false, features = ["napi8", "async"] }
napi-derive-ohos = "1.0"
这样做的意思是:
- 桌面端要用的托盘、桌面输入依赖,不要在 OHOS 下构建;
- OHOS HAR 要用的 NAPI 依赖,只在 OHOS 目标下出现;
- 上层业务代码通过条件编译选择不同入口,而不是让所有平台依赖一起进来。
如果依赖本身是必须的,但是它的上游版本还不支持 OHOS,那就不是简单“条件依赖”能解决的,而是要做依赖补丁。RustDesk 这次就属于这种情况,所以顶层用了 [patch]:
[patch.crates-io]
libsodium-sys = { path = "third_party/rustdesk/vendor/libsodium-sys" }
[patch."https://github.com/rustdesk-org/magnum-opus"]
magnum-opus = { path = "third_party/rustdesk/vendor/magnum-opus" }
[patch."https://github.com/rustdesk-org/rdev"]
rdev = { path = "third_party/rustdesk/vendor/rdev" }
这里要注意一个区别:
target-specific dependencies 用来决定某个平台拉不拉某个依赖
[patch] 用来替换某个依赖来源
不要把这两个混在一起。
如果一个依赖在 OHOS 下根本不需要,最好用 target-specific dependency 把它排除掉;如果一个依赖在 OHOS 下必须使用,但原版不支持 OHOS,那就 vendor 一份或 fork 一份,再在最终构建入口用 [patch] 固定。
这也解释了为什么本文一直强调“最终构建入口”。因为 [patch] 必须放在最终参与解析依赖的 workspace / crate 顶层才可靠。如果你只在某个子目录里改,最后从 HAR 根目录构建时,Cargo 不一定会用到你以为的那份依赖。
4.7 判断一个依赖该保留、裁剪还是 patch
迁移时可以用下面这张简单决策表:
| 情况 | 推荐处理方式 |
|---|---|
| 纯 Rust、平台无关、OHOS 可直接编译 | 保留 |
| 只在桌面端需要,OHOS HAR 不需要 | 用 target-specific dependency 排除 |
OHOS 需要,但上游 crate 不认识 ohos |
vendor / fork 后 [patch] |
| build.rs 里写死 Linux/Android/iOS 判断 | 修改 build.rs,补 target_env = "ohos" 分支 |
| sys crate 需要链接系统库 | 明确 OHOS linker、sysroot、lib 路径 |
| 功能重要但平台实现差异很大 | 抽象统一接口,OHOS 单独实现 |
| 功能不是第一阶段关键链路 | 先裁剪,后续再补 |
这张表看起来简单,但实际很有用。它能避免迁移时陷入两个极端:
- 一个极端是“什么都想保留”,导致桌面端依赖全进 OHOS,编译问题爆炸;
- 另一个极端是“能删就删”,最后项目虽然编过了,但核心功能也被删没了。
更合理的做法是分清楚:哪些是项目核心,哪些是平台外围能力,哪些可以后补。
放到 RustDesk 这次迁移里,对应关系大概是:
远控协议 / session / 登录认证 核心,保留
视频 codec / Surface 输出 核心,但需要 OHOS 实现
键鼠输入发送 核心,通过 session bridge 暴露
系统托盘 / 桌面窗口管理 非第一阶段核心,OHOS 下隔离
部分 native sys 依赖 需要 vendor / patch
HAR / NAPI OHOS 交付形态,需要新增
这样一拆,后面的工作就不再是“面对整个 RustDesk 发愁”,而是变成一组相对明确的迁移任务。
这一节的核心其实就两句话:代码路径靠条件编译隔离,依赖来源靠 target dependency 和 [patch] 收敛。如果后续迁移其他 Rust 项目,先按这两条线拆,问题会比直接改报错清楚很多。
五、源码裁剪:从桌面项目里拆出鸿蒙 PC 可用核心
RustDesk 不是一个单纯的 SDK,它有完整桌面端行为。鸿蒙 PC 迁移时需要先判断哪些能力属于“必须迁移”,哪些属于“当前阶段应当隔离”。
这个阶段其实挺像“拆机器”。RustDesk 这台机器原本是给桌面端、移动端、Flutter 端等多个场景服务的。现在我们要把它放到 HarmonyOS PC Native HAR 里,就不能一股脑把所有东西都搬过来。
比如远程连接、登录、视频解码、输入发送,这些是必须要的;但传统桌面端的系统托盘、某些平台输入捕获、窗口管理逻辑,就不一定适合放进当前 HAR 里。不是说这些功能不重要,而是第一阶段它们不是远控闭环的关键路径。
如果什么都想一次性迁过来,结果很可能是:编译问题一大堆,平台差异一大堆,最后真正的远控链路反而迟迟跑不通。
本次保留的核心能力包括:
- 会话创建与启动;
- peer id 解析和连接参数;
- rendezvous、direct、relay 等连接流程;
- 密码登录和 2FA 流程;
- 远端显示信息、分辨率切换和刷新;
- 视频 codec 协商;
- 键盘、鼠标、文本输入发送。
当前阶段不直接迁入的能力包括:
- 传统桌面窗口管理;
- 系统托盘;
- 桌面端全局输入捕获;
- 与当前远控闭环无关的平台服务。
这种裁剪不是删除功能,而是通过 OHOS 条件编译让它们不进入当前 Native HAR 构建路径。
RustDesk 原本 Flutter 侧已经有相对清晰的 session bridge。Native HAR 没有重新设计一套远控协议,而是尽量复用 RustDesk 现有 Flutter session 入口:
ArkTS
↓ NAPI
src/lib.rs
↓ flutter_ffi / flutter session bridge
third_party/rustdesk/src/flutter_ffi.rs
third_party/rustdesk/src/flutter.rs
↓
client / io_loop / protocol
这个选择很关键。直接暴露 RustDesk 内部 Client 或 IO loop 看似灵活,但会让 ArkTS 侧强依赖上游内部结构。复用 Flutter session bridge,则可以把 HarmonyOS PC 看成 RustDesk 的一个新消费端。
这里还有一个实际考虑:RustDesk 已经有 Flutter 侧的会话模型,说明上游本来就需要把 Rust 核心暴露给非传统桌面 UI 使用。既然有这条路,就没必要重新绕一条完全不同的桥。
我这次的思路是:HarmonyOS PC 这边虽然不是 Flutter,但它同样需要一个“UI 层调用 RustDesk 核心”的通道。那就尽量贴近 RustDesk 已经存在的 session bridge,用 NAPI 把它包装出来,而不是直接把内部对象裸露出来。
这样做的好处是后续更容易跟上游对齐。上游 session 逻辑变了,我们更容易找到对应位置;如果自己重写一套桥,短期也许更自由,长期维护成本会高很多。
这一节可以理解成迁移中的“取舍阶段”:不是所有桌面能力都要第一时间搬过来,但远控核心链路一定要保住。先保核心,再补外围,这样项目不会一开始就被大量平台差异拖住。
六、视频链路:不能只满足“能看到画面”
远程桌面迁移里,视频链路通常是最容易低估的一部分。
很多时候我们说“远控跑起来了”,第一反应是能不能连上、能不能看到画面。但真正做的时候会发现,“看到画面”背后其实有很多细节:远端发什么编码格式,本地支持哪些 decoder,decoder 是硬解还是软解,输出是回到内存还是直接到 Surface,尺寸变化怎么处理,刷新和分辨率怎么配合。
如果只是为了快速看到一帧画面,可以走比较粗糙的路径。但远程桌面不是普通图片展示,它要持续解码、持续渲染,还要尽量低延迟。所以视频链路不能只满足“能显示”,还要考虑后续性能和可维护性。
RustDesk 上游支持多种 codec,HarmonyOS PC 上不能假设所有格式都可用,也不能只靠软件回拷帧做长期方案。本次适配把 OHOS 视频能力集中放到:
third_party/rustdesk/libs/scrap/src/common/ohos/
├── avcodec.rs
├── aom.rs
├── vpxcodec.rs
├── convert.rs
├── record.rs
└── mod.rs
6.1 先做系统 codec 能力检测
HarmonyOS PC 上是否支持某个 decoder,必须以系统运行时结果为准。相关能力检测位于:
third_party/rustdesk/libs/scrap/src/common/ohos/avcodec.rs
核心 API 包括:
OH_AVCodec_GetCapability(...)
OH_AVCapability_GetName(...)
OH_AVCapability_IsHardware(...)
OH_AVCapability_GetCategory(...)
RustDesk 层的统一汇总入口位于:
third_party/rustdesk/libs/scrap/src/common/codec.rs
核心函数是:
Decoder::supported_decodings(...)
这样做的好处是:codec 协商仍然走 RustDesk 原有能力表,但 OHOS 下的能力来源改成系统 AVCodec 查询结果。
这里我没有写死“支持哪些 codec”。因为不同设备、不同系统版本、不同硬件能力下,实际 decoder 能力可能不同。迁移时如果直接在代码里拍脑袋写“我支持 H264/H265/VP9”,后面很容易和真实设备能力不一致。
更稳妥的方式是让系统自己告诉我们:当前 HarmonyOS PC 到底有哪些 decoder,名字是什么,是否硬件能力,然后再把这个结果转换给 RustDesk 的 codec 协商逻辑。

6.2 解码器创建逻辑统一
迁移初期最容易出现的问题,是每种 codec 各走一套创建逻辑。这样短期能跑,长期很难维护。
最终 OHOS 下统一为:
- 使用
OH_AVCodec_GetCapability(mime, false)查询系统能力; - 使用
OH_AVCapability_GetName(capability)获取推荐解码器名称; - 优先调用
OH_VideoDecoder_CreateByName(name); - 失败时回退到
OH_VideoDecoder_CreateByMime(mime)。
统一后的关键点是:H264 / H265 / VP8 / VP9 / AV1 都尽量共享同一套底层创建 helper,只在包装层区分格式差异。
这一步的迁移思考是:平台适配不应该为每个 codec 复制一份“差不多”的逻辑,而应该先沉淀出统一平台抽象,再让不同格式复用它。
这个地方如果不统一,后面调试会非常难受。比如 H264 是一种创建方式,VP9 又是另一种,AV1 再来一套;某一天发现 Surface 绑定或者 decoder fallback 有问题,就要在好几个文件里重复修。
所以这里宁愿前期多花一点时间,把“按 name 创建,失败后按 mime 回退”这套逻辑抽出来,也不要让每个 codec 都复制一份差不多但又不完全一样的代码。
6.3 Surface 输出优先于长期回拷
远控画面如果长期走 CPU 回拷,性能和延迟都会受到影响。HarmonyOS PC 适配里更合理的方向,是让 decoder 输出和应用侧 Surface 对齐。
统一后,OHOS 下多种格式都优先支持:
decode_to_surface()
并且创建 decoder 时统一带上:
decode_size
surface_id
这一步解决的是渲染路径一致性:不再让部分 codec 直出 Surface、部分 codec 走回拷,而是把 HarmonyOS PC 的视频输出收敛到同一个方向。
简单理解就是:远控画面最后要显示到鸿蒙应用的 XComponent / Surface 上,那 Native 层就应该尽量围绕这个输出目标设计,而不是先解到内存里,再让上层想办法搬来搬去。
这一步也是后面真机调画面比例、分辨率、刷新时的基础。如果 Surface 绑定和解码输出路径不清楚,后面看到黑屏、小画面、比例不对、刷新不稳定时,就很难判断到底是渲染层问题还是解码层问题。
这一节想强调的是:远控里的视频链路不是“能显示一帧图”就结束了。codec 能力、decoder 创建、Surface 输出和分辨率变化要一起考虑,否则后面调性能和体验会非常被动。
七、Native HAR 封装:给应用一个干净入口
HarmonyOS PC 应用最终消费的是 HAR,不应该直接理解 RustDesk 内部 session、client、io_loop 的生命周期。
这一层可以理解成“翻译官”。RustDesk 内部有自己的对象、状态和调用方式;ArkTS 应用侧更希望拿到的是简单的动作:初始化、创建会话、开始连接、登录、绑定 Surface、发送鼠标、发送键盘、设置帧率、关闭会话。
如果把 RustDesk 内部细节全暴露出去,ArkTS 侧代码会很快变复杂,而且一旦上游内部结构变化,应用层也会被迫跟着改。这样就失去了 HAR 封装的意义。
Native 导出层位于:
src/lib.rs
当前暴露的能力包括:
runtimeInitruntimeGetDecoderCapabilitiessessionAddsessionStartsessionLoginsessionSend2fasessionReconnectsessionClosesessionSendMousesessionInputKeysessionInputStringsessionPollEventssessionSwitchDisplaysessionSetSizesessionChangeResolutionsessionSetImageQualitysessionSetCustomFpssessionBindSurfacesessionRefresh
导出层做了三件事:
- 把 ArkTS 侧传入的字符串、JSON 和数字参数转成 RustDesk session 调用;
- 维护 HarmonyOS PC 侧需要的 session 状态、surface 绑定和错误信息;
- 用 JSON 返回结果,避免 ArkTS 侧依赖 Rust 内部类型。
其中 sessionBindSurface 是视频链路关键接口。它把鸿蒙侧的 surface_id 和 RustDesk 的 peer/display 绑定起来,后续 decoder 可以通过这个绑定找到目标输出面。
这里的迁移思考是:HAR 的边界应该稳定、窄、面向业务动作,而不是把上游项目的内部结构原样暴露出去。
举个例子,ArkTS 侧其实不需要知道 RustDesk 内部怎么维护 Client,也不需要知道 IO loop 里具体怎么处理 PunchHoleResponse 或 RelayResponse。它真正关心的是:
我要连哪个 peer
↓
现在连接到哪一步了
↓
要不要输入密码或 2FA
↓
画面应该输出到哪个 surface
↓
鼠标键盘事件怎么发给远端
所以 NAPI 层返回 JSON 字符串,看起来不如强类型对象“高级”,但在这个阶段反而比较实用。它降低了 ArkTS 和 Rust 内部结构之间的耦合,也方便调试时直接打印状态。
这一节的重点是边界:ArkTS 应用只需要知道“我要做什么动作”,不应该知道 RustDesk 内部“这个动作怎么拆成多少对象和线程”。HAR 的价值就在于把复杂度挡在 Native 层里面。
八、真机验证:画面出来不等于迁移完成
Native HAR 构建成功并不等于迁移成功。远控类项目必须在真机上验证完整链路:连接、登录、画面、输入、分辨率、帧率和异常状态。
这个阶段最容易出现“看起来成功了,但其实还有坑”的情况。
比如:
- 应用能启动,但 session 没真正连上;
- 远端能连上,但画面没有绑定到正确 Surface;
- 画面出来了,但分辨率请求没有生效;
- 画面比例看起来对了,但输入坐标偏了;
- 帧率设置了,但 RustDesk 自己的 QoS 或远端编码策略又把它限住了;
- 鼠标能移动,但单击、右键、拖动的语义不对。
所以真机验证不能只看“有没有画面”。远控软件必须把画面、输入和状态一起看。
消费侧接入方式如下:
cd <harmony-app-root>
ohpm uninstall rustdesk-ohrs
ohpm install /path/to/RustDeskHar/package.har
hvigorw assembleHap
部署到鸿蒙 PC:
hdc install <harmony-app-root>/entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -b top.frankhan.resk -a EntryAbility

验证阶段重点观察:
runtimeInit是否完成;sessionAdd / sessionStart / sessionLogin是否按顺序推进;- peer display 信息是否正确返回;
sessionBindSurface是否在 XComponent surface 创建后执行;- 远端分辨率请求是否真正生效;
- codec capability 是否和实际解码路径一致;
- 自定义 FPS、图像质量和 RustDesk QoS 是否互相影响;
- 鼠标、键盘和文本输入是否能从 ArkTS 正确进入 RustDesk session。
这一步暴露出一个常见误区:只看到画面出现,并不等于适配完成。
远程桌面需要持续看三类数据:
- 显示尺寸和远端真实分辨率;
- 视频帧率、质量、codec 和 QoS;
- 输入坐标映射、按键语义和拖拽状态。
迁移过程中,显示尺寸、远端分辨率、输入坐标基准都需要分开处理。画面适配用的是预览区域和视频比例,输入映射用的是远端实际坐标系,两者不能简单混成一个缩放因子。
这一点是实际调试中慢慢摸出来的。远控画面为了显示好看,通常会等比缩放、居中、留黑边;但输入事件必须落到远端真实坐标上。如果把显示缩放和输入坐标混在一起,就会出现“画面看着挺对,但鼠标点哪里都偏一点”的问题。
所以后面我把它拆成两件事:
显示层:关心本地预览区域、比例、居中、适配
输入层:关心远端真实宽高、坐标映射、按钮语义
远控类应用里,这两个模型必须分开。它不像普通视频播放器,只要画面比例正确就行;远控还要让用户点到正确的位置。
如果前面几节你已经理解了,大致就能知道这次迁移为什么要按“Rust 编译、HAR 构建、应用安装、真机验证”这条链路走。下面把命令集中放一遍,方便真正复现时不用在前文里来回翻。
九、复现命令:按这条链路跑
这一节只保留必要复现命令,方便读者验证迁移结果。
前面讲了很多迁移思路,这里把实际复现命令单独放出来。建议按顺序跑,不要一上来就跳到最后一步。因为每一步其实都在验证不同层:Rust 编译、Native 构建、HAR 打包、消费侧安装。
获取仓库:
git clone https://atomgit.com/FrankHan2004/RustDeskHar.git
cd <repo-root> # 本文中就是刚克隆出来的 RustDeskHar 目录
配置 OHOS 工具链:
export OHOS_NDK_HOME="<command-line-tools-root>/sdk/default/openharmony"
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_OHOS_LINKER="$OHOS_NDK_HOME/native/llvm/bin/aarch64-unknown-linux-ohos-clang"
export CARGO_TARGET_AARCH64_LINUX_OHOS_LINKER="$OHOS_NDK_HOME/native/llvm/bin/aarch64-unknown-linux-ohos-clang"
export RUSTFLAGS="-Clinker=$OHOS_NDK_HOME/native/llvm/bin/aarch64-unknown-linux-ohos-clang"
检查 Rust 编译链路:
cargo check --target aarch64-unknown-linux-ohos --lib
构建 Native HAR:
ohrs build --release -a aarch
ohrs artifact
成功后应看到:
dist/arm64-v8a/librustdesk_native_har.so
package/libs/arm64-v8a/librustdesk_native_har.so
package/libs/index.d.ts
package.har
消费侧应用工程安装 HAR:
cd <harmony-app-root>
ohpm uninstall rustdesk-ohrs
ohpm install /path/to/RustDeskHar/package.har
hvigorw assembleHap
这里建议每次更新 Native 代码后都完整执行 ohrs build、ohrs artifact、ohpm uninstall、ohpm install。不要只手工替换 .so,否则很容易出现 HAR 已旧、应用侧仍引用旧接口或旧 so 的问题。
如果你是第一次复现,建议每一步都确认一下产物:
cargo check 之后,看 Rust 是否能过 OHOS target
ohrs build 之后,看 dist/arm64-v8a/ 下有没有 .so
ohrs artifact 之后,看 package.har 是否生成
ohpm install 之后,看消费侧工程是否真的换成了新 HAR
hvigorw assembleHap 之后,再安装到真机跑
不要嫌这个过程啰嗦。Native 项目最怕的就是“我以为换了,其实没换”。尤其是 HAR、ohpm、HAP 构建缓存叠在一起时,如果不完整刷新,很容易测到旧代码。
十、踩坑记录
这一节写得稍微直白一点。下面这些坑不是理论上可能遇到,而是这次迁移过程中确实绕不开、或者很容易踩进去的点。
10.1 ohrs doctor 不是最终裁判
在 Linux 环境下,ohrs doctor 可以用于检查环境,但不能作为迁移成败的唯一依据。真正有效的判断是:
cargo check --target aarch64-unknown-linux-ohos --lib
ohrs build --release -a aarch
ohrs artifact
只要这三步完整成功,并且生成的 HAR 能被鸿蒙 PC 工程安装运行,才说明链路成立。
工具检查能帮你发现一些明显环境问题,但它不可能理解 RustDesk 这种复杂项目里的条件编译、vendor 依赖和平台补丁。最后还是要看真实构建和真实运行。
10.2 OHOS target 不能当普通 Linux 处理
aarch64-unknown-linux-ohos 里带着 linux,但它不是普通 Linux 桌面环境。RustDesk 内部很多桌面能力必须通过 target_env = "ohos" 分流。
否则会出现两类问题:
- 编译期拉入不适合 OHOS 的桌面依赖;
- 运行期调用鸿蒙 PC 不存在或语义不同的平台能力。
这个坑的表现往往不是一句“你把 OHOS 当 Linux 了”,而是某个很具体的 crate 编译失败、某段平台代码找不到符号、某个桌面 API 在 OHOS 下不成立。所以排查时要反过来看:是不是某个 cfg 条件把传统 Linux 代码带进来了。
10.3 依赖补丁要在 HAR crate 顶层收敛
如果只在 third_party/rustdesk 内部修补依赖,而顶层 HAR crate 没有对应 [patch],Cargo 解析时可能仍回到原始依赖来源。
这类问题最隐蔽,因为你在子目录里单独构建可能成功,回到 HAR 根目录构建却失败。
本次把关键依赖补丁放在 RustDeskHar/Cargo.toml 顶层,就是为了让最终 HAR 构建链路吃到同一套依赖来源。
简单说就是:最终从哪里构建,就在哪里固定依赖。不要只在子工程里修,顶层不管。
10.4 codec 不能只打通 H264
远控协商过程中,远端可能提供多种 codec。如果只修通 H264,初期测试也许能跑,但后续一旦远端策略、设备能力或配置变化,就会进入未适配分支。
因此本次把 H264 / H265 / VP8 / VP9 / AV1 的 OHOS 创建参数和 Surface 输出路径尽量统一,降低后续维护成本。
远控协商不是你想用哪个就永远用哪个。远端能力、网络状态、配置策略都可能影响最后选择的 codec。只修一个格式,很容易留下“今天能跑,换台机器就不行”的隐患。
10.5 画面坐标和输入坐标要分开建模
预览画面需要根据窗口尺寸等比缩放、居中显示;输入事件则需要映射到远端真实坐标。
如果用同一个尺寸去处理显示和输入,很容易出现画面看起来正确,但点击位置偏移的问题。
远控类项目迁移时,这一点比普通视频播放更重要。
这个坑一开始不一定明显,因为你可能先测的是画面,只要能看到远端桌面就觉得不错。但一旦开始点按钮、拖窗口、右键菜单,就会发现输入偏移比画面问题更影响使用体验。
10.6 不要过早追求“看起来完整”
迁移项目很容易被 UI 完整度带偏。比如先做连接页、设置页、按钮、状态栏,看起来会很有成就感。但 RustDesk 这种项目,真正难的地方在 Native 链路。
所以我的建议是:先把最小远控闭环跑通,再慢慢补 UI。否则很容易出现页面做了很多,但底层 session、codec、input 还没打通的情况。
10.7 日志要分阶段收窄
刚开始调试时可以多打一些日志,帮助确认链路到底走到哪里。但问题定位以后,要尽快把日志收窄到当前问题。
比如调输入时就重点看输入事件、坐标、按钮语义;调画面时就看 surface、分辨率、codec;调连接时就看 session 状态和认证流程。日志太多不一定是好事,最后会淹没真正有用的信息。
十一、复盘:关键决策和教训
这部分就不写得太“报告化”了,直接说这次做下来比较深的几个感受。
经验 1:先打通最小 Native 闭环,再补 UI
RustDesk 这种项目的难点在核心链路,不在页面。先让 session、codec、surface、input 在真机上闭环,再做交互体验,效率更高。
尤其是远控这种应用,UI 只是入口。真正决定它能不能用的,是底层连接和音视频/输入链路。先把底层跑通,后面 UI 怎么调都还有空间;反过来,UI 先做完,底层没跑通,最后还是要回头拆。
经验 2:平台分支要集中,不能散落在应用工程
OHOS 相关代码集中在 third_party/rustdesk 的平台目录和 src/lib.rs 导出层。消费侧应用只负责调用 HAR,不参与 RustDesk 内部适配。
这个原则能让项目后面好维护。否则今天在 ArkTS 里补一个平台判断,明天在 RustDesk 内部补一个临时判断,后天又在 HAR 导出层绕一下,很快就不知道问题到底属于哪一层。
经验 3:尽量复用上游已有 bridge
本次没有绕开 RustDesk Flutter session 架构另写一套远控 bridge,而是把 Native HAR 接到已有 flutter_ffi 和 session 调用上。这样更利于后续同步上游。
这点我觉得很重要。移植不是完全重写,能顺着上游已有架构走,就尽量顺着走。上游已经解决过的问题,不要轻易再造一套。
经验 4:Native HAR 是交付边界,不是临时打包方式
package.har 应该被看作最终分发产物。应用工程通过 HAR 引用 Native 能力,而不是手工复制 .so 和头文件。
这样做的好处是更新链路清楚,问题也更容易复现。别人拿到仓库以后,只要按同样的方式构建 HAR、安装 HAR,就能尽量接近同一套环境。
经验 5:真机验证要看日志和状态,不要只看画面
远程预览出现只是第一步。后续必须持续验证连接状态、分辨率、帧率、codec、QoS、输入事件和异常恢复。
我这次调试里,很多问题都是“画面已经出来了,但细节还不对”。比如分辨率不对会影响清晰度,输入坐标不对会影响操作,帧率设置不一定真正生效,QoS 还可能动态调整质量。远控软件就是这样,能看到画面只是开始。
经验 6:迁移时要接受“先不完整,但方向要对”
做这种项目很难一次性把所有功能都迁完。更现实的方式是先做一个方向正确的最小闭环,然后一层层补。
这次的方向就是:RustDesk 核心保留,OHOS 适配集中,Native HAR 交付,鸿蒙应用只消费接口。只要这个方向不乱,后面补输入体验、分辨率策略、帧率策略、更多状态展示,都可以继续往上加。
经验 7:先拆代码路径,再拆依赖来源
如果后续要参考本文迁移别的 Rust / Native 项目,我建议先做两张表。
第一张表是代码路径表:哪些模块是平台无关的,哪些模块必须为 OHOS 单独实现,哪些模块第一阶段可以先裁剪。
第二张表是依赖来源表:哪些依赖 OHOS 可以直接编,哪些依赖只应该留在桌面端,哪些依赖必须 vendor 或 fork 后用 [patch] 固定。
这两张表做完以后,再开始改代码会清楚很多。否则很容易一边改条件编译,一边改 Cargo 依赖,一边改 build.rs,最后问题混在一起,很难判断到底是哪一层出了错。
写在最后
这次 RustDesk 适配 HarmonyOS PC 的重点,不是把一个项目“编译过”这么简单,而是把一个完整桌面远控项目拆成可维护的迁移层次:
- RustDesk 上游核心继续保留在 Rust;
- OHOS 适配集中在条件编译、依赖补丁和
scrap/common/ohos/; - 视频链路优先接入 HarmonyOS codec 与 Surface;
- Native HAR 作为稳定交付边界;
- 鸿蒙 PC 应用工程只通过 HAR 消费远控能力。
对于已经拥有成熟 Native/Rust 核心的开源项目,这条路径具有较强复用价值:先保留核心,再做平台适配,最后用 HAR 收敛交付边界。这样既能降低迁移成本,也能为后续持续跟随上游留下空间。
如果用更通俗的话收个尾,这次迁移给我的感觉就是:不要急着把项目“包装成鸿蒙应用”,先想清楚原项目真正有价值的核心在哪里,再决定怎么搬。
RustDesk 的价值不在于某个按钮或者某个页面,而在它的远控核心。HarmonyOS PC 适配要做的事情,就是尽量保护这个核心,让它在新的平台上以合适的方式跑起来。
这也是为什么我最后选择了这条路线:
不重写核心
↓
不散装 .so
↓
不把上游内部对象直接暴露给 ArkTS
↓
用 OHOS 平台分支解决平台差异
↓
用 Native HAR 做交付边界
这条路不一定是最短的,但后续更稳。对于 RustDesk 这种还会持续迭代的开源项目来说,“能跑一次”不是终点,“后面还能维护、还能升级、还能继续调优”才更重要。
更多推荐




所有评论(0)