为Rust
原生gui
编程搭建win32
开发环境
演艺圈有“演而优则导”的人才跨界成长模式。技术圈何尝没有类似的人才养成计划呢。在熟练掌握了面向WEB
的Rust + WASM
开发之后,趁势向相关技术领域伸展技能树便是很自然的进阶诉求。而我的选择方向就是:桌面原生GUI
应用程序开发。谈及 GUI
应用,第一件需要操心的事就是GUI Framework
的选型(即,选择合适的【渲染引擎】与【组件库】)。
技术选型
我曾经比较仔细地考查过如下三个技术方向:
- Rust GUI Framework
-
其最大的好处就是不用费脑筋于:
- 【动态链接】- 因为所有的执行程序都会被打包入一个
.exe
文件里,根本不需要运行时链接到其它库。 - 【(交叉)编译】- 因为有极致精简的
cargo + rustc toolchain
。相比之下,make
,cmake
,gcc
,msvc
,vcpkg
,minGW
,msys
,cygwin
(还有...)也太太太复杂了吧?
- 【动态链接】- 因为所有的执行程序都会被打包入一个
-
其最大的不足就是:虽然开源的
Framework
产品很多,但都“太年轻”,并且- 可用控件十分贫瘠,
- 也没有刻意挖掘
GPU
性能 --- 我分析多半还是顾不上。看他们roadmap
更多提及的还是:“再做这个控件,和计划实现那个控件,之类的事”。
一句话概括之,They are too young, too simple, too naive.
-
因为我还是想做点冲门面的东西的,所以调研结果:不推荐。
OS
自带的GUI Framework
- 这条技术路线的特点就是:借助某个
Glue-like Crate
将Rust
程序直接桥接至操作系统渲染引擎与组件库。比如,NWG就属于这一类。 - 其最大的好处就是完全不用为【交叉编译】费神了,因为根本就不存在【可移植性
portable
】。不要说不同类型的操作系统之间,就算是同一款操作系统的不同版本之间,其绑定接口与调用套路都高概率地不一样。【可移植】还是省省吧。 - 因为支持多平台还是我想要的,所以调研结果:也不推荐。
- 这条技术路线的特点就是:借助某个
- 独立
Cpp GUI Framework
- 这条技术路线算是前两种技术方案的折中了。
- 其不足之处
- 因为市面上免费的
Cpp GUI Framework
多原生于Linux
系平台,所以Win32
开发环境真心地不好搭。好心人都去搞 Linux 去了,留在 Windows 平台上的都是“唯利是图”营商精英。 - 除了 Rust 自身的【交叉编译】之外,咱们还得额外地准备一份针对
Cpp GUI Framework
的【交叉编译】解决方案。当然,若能找到针对目标平台的【动态链接库】预编译包,那就太省心。不是没有,可以碰碰运气。
- 因为市面上免费的
- 虽然困难重重,但调研结果:推荐。再想到
Flutter
也是将Dart
绑定Skia
。这套路和我的选择很像呀!俺就充满了信心。
不确定你是否已经注意到了我在上文的一个措词【可移植性
portable
】而没有使用另一个技术“热词”【跨平台cross-platform
】?这是因为没有VM
何来跨平台,而咱们恰恰没有VM
。这就正如咱们果决地抛弃了GC
一样。
Cpp GUI Framework
的选择
在口碑榜上名列前茅的Cpp GUI Framework
还是有不少的。我考查过的有GTK3,FLTK,和QT。我选中的是GTK3 GUI Framework和gtk-rs的组合。
GTK
的光辉历史不用我再多讲了吧?虽然眼下QT
的崛起很强势(据说新版的Ubuntu
桌面系统要选用QT
打造),但由GTK
实现的GNOME
已经占据了Linux
桌面系统的大半江山。
开发环境搭建
上面讲了这么多铺垫的段子,现在开始聊正文。给原生于Linux
的GTK GUI Framework
搭建一套基于Windows 10
开发环境是一件既烧脑又充满成就感的事。
-
首先,下载与安装msys2。
MSYS2
是一款服务于Cpp
的【软件构建与分发平台】(形似Maven2
)。它的功能非常全面 (粗讲都足够独立开一期分享了)。咱们这里只使用它的两个功能:- 编译出针对当前
Windows
系统的GTK
动态链接库。若匹配你操作系统的GTK
版本在云端仓库有预编译包,它也会直接下载使用(聪明!)。 - 当作
cmd
或PowerShell
来用,因为- 它在
Windows
系统上模拟出了一个伪Linux
环境。你之前背过的大部分Linux
指令在MSYS2
终端都可以大展拳脚了。 - 它更容易集成
cmake
,make
,gcc
工具链,方便一站式地解决rustc + gcc
的编译问题。
- 它在
- 编译出针对当前
-
然后,双击安装目录下的
mingw64.exe
,打开终端。如果你的机器是32
位的话,那就需要双击mingw32.exe
文件。 -
接着,在命令行内,依次执行下面罗列的指令
# 同步【本地】与【云端】的仓库数据库 pacman -Syu # 安装工具链,gcc/make/cmake 等工具一次性安装完成 pacman -S base-devel mingw-w64-x86_64-toolchain --needed # 安装 GTK。一般来说,都会有预编译包可用。若你中奖了,那就从源码来编译吧。也耽误不了多少时间。 pacman -S mingw-w64-x86_64-gtk3 mingw-w64-x86_64-glade --needed
-
接下来,向环境变量
PATH
内添加rust toolchain
路径。之前不是提及过MSYS2
模拟出了一个伪Linux
环境了吗!若你还没忘的话,此时此刻,千万别犹豫,用上vi + ~/.bashrc
呀!vim ~/.bashrc # cargo 目录 export PATH=/c/Users/<账号名>/.cargo/bin:$PATH # 安装后的 GTK 动态链接库,就在这个目录。 export PATH=/mingw64/bin:$PATH # 这个也加上吧,rust 程序崩溃时,至少有个完整的调用栈日志可看。 export RUST_BACKTRACE=full
由此,馁馁的满足感袭来一波。
-
【可选步骤】将
MSYS2
命令行终端与VSCode
集成终端关联起来。我是懒人一枚且非常不情愿在多个窗口之间Ctrl + TAB
地来回切换。-
给
Windows 10
操作系统,添加一个新环境变量CHERE_INVOKING
,其值为1
-
给
VSCode
安装一个新的扩展插件Shell launcher
-
打开
VSCode
的【用户-设置】界面,并直接跳转至settings.json
文件 -
添加/更新配置项
"shellLauncher.shells.windows": [{ "shell": "<MSYS2 安装目录>\\usr\\bin\\bash.exe", "args": ["--login", "-i"], "label": "msys2" }],
-
敲击键盘
F1
键,输入命令Shell Launcher: Launch
回车。 -
MSYS2
命令行终端就会在VSCode
集成终端内被打开了,并已经贴心地定位到当前工程的根目录。
又一波满足感馁馁地袭来!
-
-
再然后,执行
cargo new --bin app-demo1
命令。创建一个Binary App
类型的空工程。-
在
Cargo.toml
文件内,添加GTK
依赖[dependencies] gtk = {version = "0.9.0", features = ["v3_16"]} gio = {version = "0", features = ["v2_44"]}
-
在
src/main.rs
文件内,添加少量的Hello World
代码。// 强烈推荐在 path 前冠以 :: 符, // 其表示这个包来自于第三方,而不是本地模块。 use ::gtk::prelude::*; use ::gio::prelude::*; use ::gtk::{Application, ApplicationWindow, Button}; fn main() { let application = Application::new( Some("my.demo1"), Default::default(), ).expect("GTK 绑定失败"); application.connect_activate(|app| { let window = ApplicationWindow::new(app); window.set_title("Hello World 窗体标题"); window.set_default_size(350, 70); let button = Button::with_label("点击【Hello World】!"); button.connect_clicked(|_| println!("点了!")); window.add(&button); window.show_all(); }); application.run(&[]); }
-
执行
cargo check
,检查是否存在- 语法错误
- 反【借入规则】与【所有权规则】的代码
-
执行
cargo build
,检查rust
是否能够与GTK
绑定得上。- 这一波操作对咱们业务开发者是透明的。它是由
gtk-rs
胶水层crate
私下来做的。
- 这一波操作对咱们业务开发者是透明的。它是由
-
执行
cargo run
运行被编译生成的.exe
文件。-
检查
GTK DLL
文件在运行时是否都能被正确地链接上。 -
不出意外的话,此时你会遇到程序崩溃和一条莫名其妙的报错消息
exit code: 0xc0000139, STATUS_ENTRYPOINT_NOT_FOUND
这条错误消息是告诉你:“
DLL
链接失败,很可能是不正确版本的DLL
文件被关联”。
-
-
不要慌,此坑我趟过。这个报错的原因不神秘。简单地讲,就是
System32
系统DLL
与你的应用程序所依赖的某个本地DLL
有命名冲突。- 同时,你的应用程序优先加载与链接了版本不匹配的
System32
系统DLL
。
这不是你程序的错,而是Windows
操作系统从XP SP2
版本以来,就刻意这么设计的。这款设计学名叫Dynamic-Link Library Search Order。和Unix
系OS
不同,微软为了应付来自黑客精英们的重点关照不得不采纳了更为严格的【DLL
搜索规则】来遏制日益猖獗的【DLL
劫持攻击】。哎,大家出来混,其实谁都不容易呀! 所以,PATH
环境变量对DLL
搜索的加权贡献是最低的。于是,一款应用程序搜索其依赖DLL
位置(由高往低)依次为:
.exe
文件的同级目录system32
系统目录- 启动指令的执行目录
- 最后才是
PATH
环境变量罗列的目录或文件清单。
关于从浩瀚的DLL
海洋里捞出那个与系统DLL
重名的mingw64/bin/zlib1.dll
文件,这里跳过1000
个字。我仅只感慨大学没白上。 既然知晓了重名DLL
文件,那我解决问题的思路就相当简单粗暴了。即,在.exe
文件的同级目录(也就是target/debug
或target/release
)内,创建一个指向正确DLL
文件的符号链接。于是,版本匹配的DLL
文件便会优先于System32
系统DLL
被加载与关联。
这当然不是我彪乎乎地徒手执行mklink /D
指令,来创建符号链接。请别把我想得那么蠢萌蠢萌的。 像这样简单且重复性高的劳动太应该分配给rust toolchain
去完成了。简单地讲,就是利用cargo hook
(类似于,git hook
与 npm hook
),让cargo
在每次开始编译前执行一段脚本代码给mingw64/bin/zlib1.dll
文件创建符号链接文件。
-
第一步,在工程根目录,创建
build.rs
文件,并添加如下代码use ::std::{env, fs, os, path::Path, process}; fn main() { let out_dir = env::var("OUT_DIR") .expect("失败:环境变量`OUT_DIR`未提供"); println!("调试:OUT_DIR={}", out_dir); let exe_dir = Path::new(&out_dir[..]).join("../../..").canonicalize() .expect(&format!("失败:不能从 {} 推断出 exe 目录", out_dir)[..]); symbolic_link_zlib1(&exe_dir); } #[cfg(windows)] fn symbolic_link_zlib1(exe_dir: &PathBuf) { let msys2_home = match env::var("MSYS2_HOME") { Ok(value) => value, Err(_) => { println!("cargo:warning=环境变量`MSYS2_HOME`没有提供,没有链接操作会被执行"); return; } }; println!("调试:MSYS2_HOME={}", msys2_home); println!("调试:EXE_DIR={}", exe_dir.display()); if !exe_dir.is_dir() { println!("cargo:warning={} 不是一个目录", exe_dir.display()); process::exit(1); } let zlib1_symbol = exe_dir.join("zlib1.dll"); println!("调试:ZLIB1_EXE={}", zlib1_symbol.display()); if zlib1_symbol.exists() { fs::remove_file(zlib1_symbol.clone()) .expect(&format!("失败:不能删除原来的 {} 符号链接文件", zlib1_symbol.display())[..]); } let bits = if cfg!(target_pointer_width = "32") { 32usize } else { 64usize }; let bin_dir = Path::new(&msys2_home[..]).join(&format!("mingw{}", bits)[..]).join("bin"); let bin_dir = bin_dir.canonicalize() .expect(&format!("失败:不能从 {} 推断出 mingw**/bin 目录", bin_dir.display())[..]); println!("调试:BIN_DIR={}", bin_dir.display()); if !bin_dir.is_dir() { println!("cargo:warning={} 不是一个目录", bin_dir.display()); process::exit(1); } let zlib1_origin = bin_dir.join("zlib1.dll"); println!("调试:ZLIB1_FILE={}", zlib1_origin.display()); if !zlib1_origin.is_file() { println!("cargo:warning={} 不是一个文件", zlib1_origin.display()); process::exit(1); } os::windows::fs::symlink_file(zlib1_origin.clone(), zlib1_symbol.clone()) .expect(&format!("失败:不能创建文件链接 {} 指向 {}", zlib1_symbol.display(), zlib1_origin.display())[..]); println!("成功:能创建文件链接 {} 指向 {}", zlib1_symbol.display(), zlib1_origin.display()); } #[cfg(not(windows))] fn symbolic_link_zlib1(_: &PathBuf) {}
- 借助【条件编译】元属性,这段代码将仅在
Windows
下才会被编译与执行 - 借助【条件编译】宏,这段代码能够自动识别当前
OS
的最大指针宽度,和指向正确的mingw**
子目录。
- 借助【条件编译】元属性,这段代码将仅在
-
第二步,在
~/.bashrc
里,添加一个新环境变量MSYS2_HOME
。其值就是MSYS2
的安装目录。 -
第三步,重新执行
cargo run
。于是,GTK
的Hello World
界面就会出现了。完美!!!
至此,我这次要和大家分享的内容就结束了。哎!这我就足足准备了一周。至于【交叉编译】的坑,咱们下次再填吧(实在太深)。今天,我先立一个Flag
。
突然,想感慨:“谁人心里没有‘星辰大海’?或许咱们前端人的‘诗和远方’就是走出【浏览器】,迈向【原生桌面】,再涉足【IoT
嵌入式开发】吧。最终,做到技术链上游。”。我依旧是那位“水平不高,能力有限,但初心犹在”的前行者。
评论区
写评论顶一个。
这是要逆天啊
好评一个~
666
非常赞!!!