Rust
登陆【华为鸿蒙】操作系统之Native
模块开发
名词解释
【鸿蒙操作系统】的英文全名是
Open Harmony Operation System
。正文将以其首字母缩写词ohos
引用该词条。【鸿蒙软件开发工具包】的英文全名是
Open Harmony Software Development Kit
。正文也将以它的首字母缩写词ohsdk
引用该词条。DevEco Studio IDE
是【华为】为鸿蒙应用程序开发免费提供的集成开发环境。它的最新稳定版内置了ohsdk 3.1.0 (API v9)
。【
Native
模块】是指由遵循了ArkTs NAPI
接口规范的C/Cpp/Rust
程序经交叉编译输出的链接库.so
文件。
前言
到写文章时止,虽然华为技术团队既未将rustup
工具链无缝集成入DevEco Studio IDE
也未提供ArkTs + Rust
的“一站式”混合编程体验,但Rust
登陆ohos
依旧势不可挡,因为相较于Rust
带来的生产效率收益(参照c / cpp
),搭建交叉编译环境的人工成本真的微不足道。甚至,求助于【操作系统镜像】或Docker
技术,@Rustacean 还能避免这类重复性劳动的再次发生。
为了填补DevEco Studio IDE
与rustup
工具链之间的“窄沟”,仅有两步操作需被执行:
搭建面向
ohos
的交叉编译环境。
限于作者
dev box
是Windows 11
,所以本篇文章仅分享从Windows
至ohos
的交叉编译环境搭建心得。
将交叉编译输出的.so
文件注入DevEco Studio
工作流。
搭建Windows
➞ ohos
交叉编译环境
鉴于华为硬件产品的三款主流CPU
架构,@Rustacean 需同时准备三套交叉编译方案,分别是:
面向
64
位ARM CPU
的aarch64-unknown-linux-ohos
方案。面向
32
位ARM CPU
的armv7-unknown-linux-ohos
方案。面向
64
位AMD / Intel CPU
的x86_64-unknown-linux-ohos
方案。
前两套方案是为【真机】设备提供动态链接库/Native
模块;而后一套方案则是服务于手机模拟器(虚拟机)的。
上表中Triple
的信息描述格式统一是:
<CPU架构><CPU子架构>-<厂商>-<操作系统>-<应用程序二进制接口格式>
于是,armv7-unknown-linux-ohos
应被读作
【厂商】栏的unkown
是Mozilla
公司的“锅”,而不是我定的。就我本意,这一栏馁馁的是汉语拼音HuaWei
。
下面上干货了...
第一步,给ohsdk
补装native
组件
DevEco Studio IDE
的内置ohsdk
位于%LocalAppData%\Huawei\Sdk\openharmony\<API 版本号>
目录下,但其初始安装却缺失了native
组件(— 可能是因为这个模块太大了,超过2GB
)。所以,@Rustacean 需要
补装
native
组件记住
ohsdk
对应的【API
版本号】,因为后续配置得用。
具体步骤
打开
DevEco Studio IDE
若出现的是【欢迎界面】,就从菜单
Configure
➞Settings
,打开Settings
对话框若出现的是【工程界面】,就从菜单
File
➞Settings
,打开Settings
对话框从对话框左侧选择
SDK
;从右侧查看Platform
选项卡下面的内容寻找并记忆被勾选的【
SDK
版本号 (API
版本号)】。比如,下图中的3.1.0 (API 9)
。勾选
native
复选框点击
OK
按钮等待
native
组件安装完成 — 耐心点儿,等待时间可不短
待上述操作都正常完成之后,便可见如下所示的新目录结构
第二步,重新编译Rust
标准库
之所以把事情搞这么大是因为Mozilla
厂方并没有为ohos
提供预编译的【标准库】二进制文件。于是,尽管ohos
已被纳入了rustc
交叉编译支持清单(请见下图)
,但直接执行交叉编译指令
cargo build --release --target=aarch64-unknown-linux-ohos
还是会遭遇失败和看到E0463
号错误
技术方案选型
编译【标准库】源码有两条技术路径
重新编译整条
rustup
工具链,捎带着也就编译出【标准库】了 — 难!我没搞定将【标准库】作为普通依赖
crate
和Cargo (Lib) Package
工程的业务代码一起编译(— 注:这个解释并不精确,因为细究起来主crate
与依赖crates
是搅和在一起的各自独立编译,而不是绝对意义上的“一锅烩”)。下图中被红框圈定的crates
就都出自于【标准库】
我选择了第二条技术路线。虽然后一条技术路线拖长了程序编译的总用时,但它仅会影响首次编译操作。从那以后,借助sccache编译缓存技术,由【标准库】引入的额外延时几乎可以忽略不计。更重要的是,该技术路线不会阻塞 @Rustacean 对rustup
工具链的后续升级。咱们随时都可以rustup update
。
采用【方案二】的准备工作与先决条件
给
rustup
工具链,补装【标准库】源码(即,rust-src
组件)。从命令行,立即执行且仅执行一次:
rustup component add rust-src
启用
nigtly
工具链,因为工具链的stable
版本还尚不支持“裹挟【标准库】共同编译”的新功能。从命令行,立即执行且仅执行一次:
rustup default nightly
采用
ohsdk
内置的llvm - clang
作为rustc
链接器(下一节将详细介绍)向交叉编译指令添加新命令行参数
-Zbuild-std
。cargo
会透传该参数给rustc
并指示编译器不是寻找现成的【标准库】链接文件而是现场编译【标准库】源码。编译指令也将变为
cargo +nightly build -Zbuild-std --release --target=aarch64-unknown-linux-ohos
如何把ohsdk
内置的llvm - clang
作为rustc
链接器
第一步,回忆之前记下的【鸿蒙API
版本号】数字和新建环境变量OHOS_API_V
。【推荐】从Cargo
全局配置文件%UserProfile%\.cargo\config.toml
新建OHOS_API_V
环境变量,因为
一方面,这可最小化对系统环境的“污染” — 该变量仅对
Rust
交叉编译有用,没有必要系统级全局可见。另一方面,它随时可被【会话级】同名环境变量短暂复写,方便以后临时变更做试验。
打开%UserProfile%\.cargo\config.toml
配置文件和添加配置表
[env]
OHOS_API_V = "9"
【注意】伴随今后ohsdk
的自动升级,该环境变量的值须被同步地手动更新,以避免编译失败。
第二步,将ohsdk
目录下的LLVM
前端编译器llvm\bin\clang.exe
包装为rustc
的【鸿蒙链接器】。敲黑板,重点来了!@Rustacean 需分别构建三个链接器,以服务三套交叉编译方案,和向华为的三类硬件设备提供.so
文件。于是,有
【链接器1】面向
64
位ARM CPU
真机的aarch64-unknown-linux-ohos
交叉编译方案。在%UserProfile%
目录下,新建cmd
文件aarch64-unknown-linux-ohos-clang.cmd
,并添加如下代码%LocalAppData%\Huawei\Sdk\openharmony\%OHOS_API_V%\native\llvm\bin\clang.exe ^ -target aarch64-linux-ohos ^ --sysroot=%LocalAppData%\Huawei\Sdk\openharmony\%OHOS_API_V%\native\sysroot ^ -D__MUSL__ %*
【链接器2】面向
32
位ARM CPU
真机的armv7-unknown-linux-ohos
交叉编译方案。在%UserProfile%
目录下,新建cmd
文件armv7-unknown-linux-ohos-clang.cmd
,并添加如下代码%LocalAppData%\Huawei\Sdk\openharmony\%OHOS_API_V%\native\llvm\bin\clang.exe ^ -target arm-linux-ohos ^ --sysroot=%LocalAppData%\Huawei\Sdk\openharmony\%OHOS_API_V%\native\sysroot ^ -D__MUSL__ ^ -march=armv7-a ^ -mfloat-abi=softfp ^ -mtune=generic-armv7-a ^ -mthumb %*
【链接器3】面向
64
位AMD / Intel CPU
模拟器的x86_64-unknown-linux-ohos
交叉编译方案。在%UserProfile%
目录下,新建cmd
文件x86_64-unknown-linux-ohos-clang.cmd
,并添加如下代码%LocalAppData%\Huawei\Sdk\openharmony\%OHOS_API_V%\native\llvm\bin\clang.exe ^ -target x86_64-linux-ohos ^ --sysroot=%LocalAppData%\Huawei\Sdk\openharmony\%OHOS_API_V%\native\sysroot ^ -D__MUSL__ %*
第三步,全局且有条件地向rustc
装配【鸿蒙链接器】。其中,
【全局】意味着修改
Cargo
全局配置文件%UserProfile%\.cargo\config.toml
和作用于所有Cargo Package
工程。【有条件】意味着采用条件编译语法
target.<triple>.linker
限定该【链接器】仅生效于面向ohos
的交叉编译操作。
具体作法,打开%UserProfile%\.cargo\config.toml
配置文件和添加配置表
[target.aarch64-unknown-linux-ohos]
linker = "./aarch64-unknown-linux-ohos-clang.cmd"
[target.armv7-unknown-linux-ohos]
linker = "./armv7-unknown-linux-ohos-clang.cmd"
[target.x86_64-unknown-linux-ohos]
linker = "./x86_64-unknown-linux-ohos-clang.cmd"
[profile.dev.package.compiler_builtins]
opt-level = 2
再对前面配置片段补充两点解释:
配置项
linker
以相对路径引用链接器文件的背后逻辑是cargo
总是以config.toml
的父文件夹(.cargo)所处目录为起点开始解析相对路径(,而不是以config.toml
的同级目录为起点)。所以,本例中的./
路径前缀对应的就是登录账号的根目录%UserProfile%
。配置项
opt-level
,借助【Profile
重写(i.e. Override)】配置表头[profile.dev.package.compiler_builtins]
,仅将【开发编译】模式下【标准库】内compiler_builtins crate
的代码优化级别强制锚定于2
。否则,cargo build -Zbuild-std --target=aarch64-unknown-linux-ohos
指令(注意:没有--release
参数)会概率性地失败于exit code: 0xc0000005, STATUS_ACCESS_VIOLATION
错误。
第四步,给冗长的交叉编译指令约定(短)别名。
还是打开%UserProfile%\.cargo\config.toml
配置文件和增补如下配置表
[alias]
ohos-build = ["build", "-Zbuild-std", "--target=aarch64-unknown-linux-ohos", "--target=armv7-unknown-linux-ohos", "--target=x86_64-unknown-linux-ohos"]
于是,只要执行一条cargo ohos-build
指令就相当于连续执行下面三条编译指令:
cargo build -Zbuild-std --target=aarch64-unknown-linux-ohos
cargo build -Zbuild-std --target=armv7-unknown-linux-ohos
cargo build -Zbuild-std --target=x86_64-unknown-linux-ohos
总结交叉编译环境的搭建成果
以后每次在Cargo (Lib) Package
工程根目录下执行
cargo ohos-build --release
,编译器都会立即
唤起
ohsdk
内置的LLVM
前端编译器llvm - clang
作为rustc
链接器将【标准库】源码作为普通依赖
crate
与主crate
业务程序一起编译并行启动三个
JOB
进程对同一套Rust
源码同时执行三组交叉编译操作交叉编译输出三个文件名相同但
ABI
格式不同的动态链接库.so
文件
新建Cargo (Library) Package
工程,验证交叉编译环境
首先,克隆stuartZhang/socket2至本地,并将代码分支切至v0.4.x
。
git clone git@github.com:stuartZhang/socket2.git
cd socket2
git checkout -q v0.4.x
关于这一步操作的必要性,我已经详细地阐述于ohos-node-bindgen还不能被直接使用章节了。简单地讲,这是为了绕过socket2 crate对华为鸿蒙操作系统的不兼容缺陷。
然后,从命令行,新建Cargo (Library) Package
工程
cd ..
cargo new --lib calculator
code calculator
其次,在VSCode
内,打开Cargo.toml
文件,和追加如下内容
[lib]
crate-type = ["dylib"][dependencies]
ohos-node-bindgen = "6.0.3"
socket2 = "0.4.10"[patch.crates-io]
socket2 = { path = "../socket2" }
前面配置片段内的【依赖图重写】配置表[patch.crates-io]
指示Cargo
包管理器使用本地的stuartZhang/socket2 crate
山寨货替换crates.io
上的正品,因为正品不兼容华为操作系统。
接着,从VSCode
打开src/lib.rs
文件,和增补如下Demo
代码。这是一段简单的整数加运算程序。请把注意力聚焦在【派生宏】的使用上。
use ::ohos_node_bindgen::derive::ohos_node_bindgen;
#[ohos_node_bindgen]
fn add(first: i32, second: i32) -> i32 {first + second
}
再次,执行交叉编译
cargo ohos-build --release
最后,从【资源管理器】查看编译输出结果
Cargo (Library) Package 工程根目录
├── Cargo.toml
├── src — Rust 源码目录
├── target
│ ├── aarch64-unknown-linux-ohos
│ │ └── release
│ │ └── libcalculator.so
│ ├── armv7-unknown-linux-ohos
│ │ └── release
│ │ └── libcalculator.so
│ ├── x86_64-unknown-linux-ohos
│ │ └── release
│ │ └── libcalculator.so
值得注意的是,编译输出的链接库文件名是有lib
前缀的。所以,Native
模块的文件名是lib<包名>.so
,而不是<包名>.so
。
将Native
模块注入普通的DevEco Studio
工程
Native
模块就是由前面交叉编译输出的ArkTs N-API
链接库.so
文件。
首先,从DevEco Studio IDE
新建/打开普通Empty Ability
工程。
然后,修改模块级的build-profile.json5
文件(比如,entry/build-profile.json5
),和添加如下配置项至buildOption
节点
"externalNativeOptions": {"abiFilters": ["arm64-v8a","armeabi-v7a","x86_64"]
}
其次,在模块根目录下,创建下面三个子文件夹
libs/arm64-v8a
libs/armeabi-v7a
libs/x86_64
接着,依次向它们复制入编译好的链接库文件。例如,
最后,在ArkTs
业务代码内(比如,entry/src/main/ets/pages/Index.ets
),以ES Module
语法,导入Native
模块,和调用其成员方法
import calculator from 'libcalculator.so';
const result = calculator.add(2, 3);
总的来讲,调用端的ets
代码就这么简单!但还是有三处优化可做以改善开发体验:
优化DevEco Studio
工程目录结构
将Cargo (Lib) Package
与DevEco Studio Project
合并为一个工程更有利于提高Rust + ArkTs
的混合编程生产力。所以,如下DevEco Studio
工程目录结构是被强力推荐的:
DevEco Studio 工程根目录
├── entry — 模块根目录
│ ├── libs — 交叉编译输出的 .so 文件都被复制到下面的子文件夹内
│ │ ├── arm64-v8a
│ │ ├── armeabi-v7a
│ │ └── x86_64
│ ├── src
│ │ ├── main
│ │ │ ├── resources
│ │ │ ├── cpp — *旧有*的 Cpp(ArkTs N-API) 工程目录
│ │ │ ├── ets — *旧有*的 ArkTs 源码目录
│ │ │ ├── rust — *新建*的 Rust(ArkTs N-API) 工程目录
│ │ │ │ ├── Cargo.toml
│ │ │ │ ├── src — Rust 源码目录
│ │ │ │ ├── target
│ │ │ │ │ ├── aarch64-unknown-linux-ohos
│ │ │ │ │ │ └── release
│ │ │ │ │ ├── armv7-unknown-linux-ohos
│ │ │ │ │ │ └── release
│ │ │ │ │ ├── x86_64-unknown-linux-ohos
│ │ │ │ │ │ └── release
将Cargo (Lib) Package
降级为DevEco Studio Project
内某个特定模块下的子工程有两个好处:
同一个
DevEco Studio
工程内可同时包含多个Native
子工程。每个
Native
子工程既可独占一个模块以达成与主模块业务代码有限隔离的目的,也能与ets
程序“混住”耦合于相同模块内。
友情提示
在移动Cargo (Lib) Package
工程位置后,千万别忘了同步修改Cargo.toml
配置文件中【依赖图重写】配置表[patch.crates-io]
对本地stuartZhang/socket2 crate
的引用路径。否则,会编译失败!
自动化链接库.so
文件的复制操作
在每次执行cargo ohos-build --release
指令之后都徒手复制三个.so
文件至不同的文件夹是非常低效的,所以 @Rustacean 有必要给Cargo
编写build.rs
与post_build.rs
构建程序,以扩展包管理器在编译前与编译后的处理行为,并自动完成文件复制操作。其中,
build.rs作为【前置处理】程序
从环境变量,收集
.so
文件的位置信息生成
[CMD] COPY /Y
或[Shell] cp -f
文件复制指令将【文件复制】指令尾追加至同一个
.cmd / .sh
脚本文件
post_build.rs作为【后置处理】程序
执行被写入【文件复制】指令的程序文件,并
删除该程序文件
【打广告】
build.rs
与post_build.rs
皆未对上下文做任何的假设。所以,它们可被零成本地复用于其它同类工程中。
还是看图吧,一图抵千词
设计很完美但现实很骨感,因为Mozilla
厂方的rustup
工具链尚不支持【后置处理】。所以,@Rustacean 需
额外安装功能增补包cargo-post
cargo install cargo-post
修改
Cargo
全局配置文件%UserProfile%\.cargo\config.toml
中的ohos-build
别名设置,以使cargo-post
生效[alias] ohos-build = ["post", "build", "-Zbuild-std", "--target=aarch64-unknown-linux-ohos", "--target=armv7-unknown-linux-ohos", "--target=x86_64-unknown-linux-ohos"]
【注意】在"build"左侧新添加了"post"数组项
给Native
模块导出接口,添加.d.ts
类型提示
DevEco Studio IDE
并没有集成类似于DLL Export Viewer的【动态链接库外部接口反射工具】。所以需要
@Rustacean 在输出
.so
文件的同时也提供一份接口类型说明的.d.ts
文件(— 其功能几乎等效于C
头文件),并将该类型说明文件注入
DevEco Studio
工作流
接下来,我沿着前面Rust + ArkTs
混合编程的新目录结构,描述操作步骤:
在模块
entry
的根目录下,创建src/main/rust/types/libcalculator
子目录。注意:路径末端的文件夹名libcalculator
是链接库文件的basename
。在新建文件夹内,再新建文件
index.d.ts
和添入Native
模块导出函数的函数签名export const add: (frist: number, second: number) => number;
接着新建文件
oh-package.json5
和添入Native
模块的摘要信息。{"name": "libcalculator.so","types": "./index.d.ts","version": "0.1.0","description": "ArkTs NAPI 原生模块示例" }
其中,
name
字段就是链接库的文件名(含扩展名)。types
字段是指向类型说明文件的相对路径。version
字段是Native
模块版本号。【推荐】该字段值与Cargo (Lib) Package
子工程中Cargo.toml
配置文件内[package]
配置表下version
配置项的值保持一致 — 这又是一处纯人工同步点。description
字段是Native
模块描述信息。
打开
entry
模块的oh-package.json5
文件,并添加对Native
模块的依赖项条目。"dependencies": {"libcalculator.so": "file:src/main/rust/types/libcalculator" }
在依赖项条目中,左侧是链接库的文件名;而右侧是指向了类型说明文件所处文件夹的相对目录。
最后,从
DevEco Studio IDE
依次点击菜单项Build
➞Rebuild Project
重新构建整个工程和使配置项修改生效。
于是,鸿蒙应用软件开发程序员就能在ets
与ts
代码编辑器内获得针对Native
模块API
的丰富类型提示了。
线上例程
我已将上述全部文字描述内容都例程化到github
工程Arkts-NAPI-Rust-Demo内了。线下运行该工程可加强对文章繁杂内容的理解。
运行例程工程的环境要求
rustc 1.75.0-nightly
VSCode 1.86
ohsdk 3.1.0(API v9)
DevEco Studio 3.1.1 Release
运行例程工程的具体步骤
克隆
git@github.com:stuartZhang/Arkts-NAPI-Rust-Demo.git
在
VSCode
内,打开
entry/src/main/rust
目录敲击
Alt + T + R
键。依次点击菜单项
build
➞ohos-build
➞--release
观察控制台输出日志,等待交叉编译结束。
在
DevEco Studio IDE
内,image 打开工程根目录
启动手机模拟器
敲击
Shift + F10
键,运行移动端程序
结束语与扩展阅读
搞定【交叉编译】难关仅只是鸿蒙Rust
原生开发万里征程的第一步。加深对ArkTs - NAPI
接口定义的理解才是【形成生产力】的核心任务。好消息是
ArkTs - NAPI
与nodejs N-API
高度相似。至少截至目前,它们的相似度还>= 95%
。所以,已熟悉nodejs
原生模块编程的“老司机”们上手鸿蒙ArkTs - NAPI
应该不难。另外,我在春节假期期间贡献的ohos-node-bindgen crate更可大幅降低
ArkTs - NAPI
原生开发的复杂度。请对比下图左右侧的代码量
所以,ohos-node-bindgen crate值得大家点
star
呀!也请大家给Arkts-NAPI-Rust-Demo点star
!