【社区投稿】Rust登陆华为鸿蒙操作系统之Native模块开发
Rust登陆【华为鸿蒙】操作系统之Native模块开发名词解释【鸿蒙操作系统】的英文全名是Open Harmony Operation System。正文将以其首字母缩写词ohos引用该词条。【鸿蒙软件开发工具包】的英文全名是Open Harmony Software Development Kit。正文也将以它的首字母缩写词ohsdk引用该词条。DevEco Studio IDE是【华为】为鸿蒙
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
!
更多推荐
所有评论(0)