Rust

Rust 区域下的其他文章:

介绍

Rust 是一门系统编程语言,专注于安全,尤其是并发安全,支持函数式和命令式以及泛型等编程范式的多范式语言。Rust 在语法上和 C++类似,但是设计者想要在保证性能的同时提供更好的内存安全。——百度百科

rust 掀起了一股 RIIR (Rewrite it in Rust) 的热潮。

Awesome Alternatives in Rust | A curated list of command-line utilities written in Rust 收录了一些 rust 优秀应用。主要是 linux cli 工具。

为什么推荐

优点

缺点

如何学习

官网有详细的 QA 与你所需要的一切。资料方面,rust 的学习资料非常多,列举几个我读过的:external - book

其他资料:

开发环境

安装 rust

rust 的安装与配置并不难。在 windows 上可以使用官方脚本一行安装 rustup 及 rust。linux 也可以选择用包管理器,详见 Archwiki;但是最为推荐的还是 rustup,毕竟写 rust 会经常换工具链。

我目前日常使用 nightly,仅在必要的时候使用 stable。修改全局默认工具链为 rustup default nightly,在项目内更改当前项目默认工具链为 rustup override set nightly。

开发

然后我使用 vscode 作为 IDE。安装插件:

还有一些能够优化开发体验的选项:

  1. 使用 clippy 作为 check 指令。
  2. 安装额外的 cargo 组件
  3. 切换 vscode rust-analyzer 插件为预发布版本 算了,预发布经常出 bug。
  4. 设置 cargo fmt(我常用的)
    • 开启注释自动换行。由于其默认不开启且还停留在 unstable(五年啊!),因此需要自行写个 rustfmt.toml 开启。查看 cargo 小节获取更多 rustfmt 信息。
    • 自动处理与合并 use 句

语言基础

循环

Rust 的 for 循环需要跟可迭代对象,例如:

rs
for i in 0..100 {} // i in [0,99]
for i in 0..=100.step_by(2) {} // i in { 0,2,4,6...,98,100 }

而类似 C++ 的 do while 循环可以写成:

rs
loop {
  // do something
  if condition { break; }
}

loop 循环还可以 break 出一个值。

输出

dbg!() 宏可以在 stderr 中输出调试信息,会消耗所有权。dbg! 返回值就是输入。dbg! 在 release 下也会输出。

ln 代表结束空行。常用的就 print(ln)! eprint(ln)!,没了。print 系列宏不消耗所有权。print! 底层是 write!。

输入

输入需要使用标准库中的 std::io(或者其他非标准库),输入是各行的字符串,需要手动处理。

rust
use std::io;
let mut s = String::new();
io::stdin().read_line(&mut s).expect("failed to read");
let num: i32 = s.trim().parse().unwrap(); // 转换类型过程

基础数据结构

这里可能会有一些帮助。

不得不说手写数据结构确实是一个学习 rust 的好方式,自己写一遍,什么 *Cell 什么 Weak 全部都能吃透。

container

monad

此处特指 Option 与 Result 两种。后面的方法可以不记,实际写到再查(IDE 下拉列表看函数签名)。

原生字符串

Rust 的字符串所包含的问题实际上很多,此处只是冰山一角。

最主要的就是 &str 和 String 两种了,前者没有所有权,后者有。

字符串修改

在 Rust 语言中,字符串采用 utf-8 编码,字符长度不一,因此 Rust 不提供下标查找字符串的方法。这让字符串的修改需要一点点的技巧。

  1. 转换为 Vec<char> 后修改 C++程序员认为这种方式非常亲切。之后若有需要,还可将 Vec<char> 重新转换为字符串。注意,Rust 中的 char 为 4 字节,转为 Vec 后,可进行 O(1) 查找。
    rust
    let s1:String = String::from("Hello我是绝对值_x");
    let mut a : Vec<char> = s1.chars().collect();
    a[5] = '你';
    let s2 = a.iter().collect::<String>();
    assert_eq!(s2,"Hello你是绝对值_x");
  2. replace_range 函数
    rust
    let mut s1:String = String::from("Hello我是绝对值_x");
    s1.replace_range(5..=7,"你");
    assert_eq!(s1,"Hello你是绝对值_x");
    请注意,若替换范围不在 utf-8 字符的分割点上将会导致程序抛出 panic,因此不适用于变字节数的未知字符串的替换。
  3. as_bytes_mut 方法(unsafe)
    rust
    let mut s1:String = String::from("Hello我是绝对值_x");
    unsafe {
      let s1_bytes: &mut [u8] = s1.as_bytes_mut();
      let s2_bytes: &[u8] = "你".as_bytes();
      for i in 0..3{
        s1_bytes[i + 5] = s2_bytes[i]
      }
    }
    assert_eq!(s1,"Hello你是绝对值_x");
    该方法异常繁琐,同样也不适用于变字节数的未知字符串的替换,但是若替换范围不在 utf-8 字符的分割点上并不会触发 panic. 例如,将第 6 行代码改为 s1_bytes[i + 6] = s2_bytes[i] 的运行结果:
    rust:no-line-numbers
    Hello�你��绝对值_x

语法糖

问号

——多用问号 人生会轻松很多

问号用于提前返回错误。do_something_that_might_fail()? 等价于

rs
match do_something_that_might_fail() {
  Ok(v) => v,
  Err(e) => return Err(e),
}

问号不能在正常签名的闭包中使用,例如 for_each,map 等的参数。可以用 try_for_each,try_map 等(如果有的话)。

或者等他娘的 try_blocks 稳定。

impl Trait

匿名泛型,可以让你少写点东西。

rs
fn print(a: impl IntoIterator<Item = impl fmt::Display>) {
    a.into_iter().for_each(|s| println!("{}", s));
}
// equals to:
fn print<T>(a: T)
where
    T: IntoIterator,
    T::Item: fmt::Display,
{
    a.into_iter().for_each(|s| println!("{}", s));
}

并发

如果你对 async/await 模型没有明确概念,可以看看这篇文章入门。
rust 提供 async/await 模型和线程模型。

Future

每个 async 函数返回的都是一个 Future<Output = ...>。Rust 的 Future 不像其他语言那样创建即执行,而是需要通过 poll 执行并推进。

Send/Sync

关于 Send/Sync 可以看这里 或者 external articles 5.。

关于 Wrappers,看这里即可(我想大家应该都看得懂):

Struct Trait
Box<T> Send(T) -> Send, Sync(T) -> Sync
Arc<T> (Send + Sync)(T) -> (Send + Sync)
Mutex<T> Send(T) -> (Send + Sync)
Rc !Send + !Sync
Cell<T>, RefCell<T> Send(T) -> Send, !Sync
RwLock<T> (Send + Sync)(T) -> (Send + Sync), Send(T) -> Send

此处暂不考虑 allocator.

将这些类型列在一起,可以发现,标准库没有任何包装可以将 !Send 转为 Send。(貌似有一个 crate send_wrapper 可以做到)

tokio

说到并发,目前广泛使用的异步运行时是 tokio。一般 features = ["macros", "rt", "rt-multi-thread"] 是必加的。

关于 tokio 可以看入门秘籍 13 章。

简单批处理

在实际并发中经常碰到需要等待一批 Future 结束并获取返回值的情况。join! 不能 join 任意数量;tokio 有一个 JoinSet,但返回值是乱序的,并且 api 设计也不够易用。所以我们如何获取顺序的并行 Future 返回值呢?

答:用 futures / futures_util crate 的 futures::stream::FuturesUnOrdered。具体使用方法可以参考用例。一般就是将每一个 task spawn,然后将 handle collect 到 FuturesUnOrdered 里再 while let Some(x) = container.next().await 即可。

mod

rust 的 mod 确实会让初学者摸不着头脑。建议先搜几篇文章看看,例如 Rust 模块和文件 - [译],也可以问 AI。多写几次就完全掌握了。

  1. 每一个 .rs 文件、mod 块 和 带有 mod.rs 的文件夹 都是模块。
  2. lib.rs(如果不是 lib target 则为 main.rs)是顶层模块(crate),其他模块层级即为文件目录层级。
  3. 整个模块结构是一颗树。
    • 初始时,只有 lib.rs 在模块树内,其他文件都在树外。
    • 我们需要使用 mod xxx 将模块添加到模块树内。只有在模块树内的模块才会参与编译。
    • 添加到模块树后,在某个模块使用另一个模块的定义需要用 use xxx。这里的 xxx 可以是模块树中的“绝对路径”(也就是从顶层模块开始查找,crate::sub1::xxx),也可以是“相对路径”(从当前模块开始查找,super::sub2::xxx)。

其他

语言进阶

trait

trait 可谓是 rust 核心,不是 OOP 胜似 OOP(?),rust 学习的一大难点也是掌握 trait 的用法。

dyn object

一个特殊的对象是 Trait Object,表示实现了某个 trait 的任意对象,代码里使用 dyn object 表示。

很多地方代码要求 Sized,所以一般需要用 Box 包起来:Box<dyn object>。如果在 struct 内用,经常还需要加生命周期。看起来要求比较多,不过如果泛型长了,用 dyn object 写起来还是更简单点。

dyn object 其实也是 Rust DST (dynamically-sized types) 特性的一部分,有兴趣的可以读读 DSTs Are Just Polymorphically Compiled Generics。

宏

宏是很好用的东西,分为过程宏和声明宏。声明宏简单,常见用来写不定参数的函数/减少重复代码。过程宏就是纯 Token 处理,非常复杂,可以写装饰器。

学习宏,直接去看如何学习中提到的小宏书。讲的很好。

调试宏可以用 cargo-expand,不需要通过编译就能展开宏。注意!cargo-expand 不保证展开后的运行结果与原代码一致!

生命周期

待续

杂谈

嵌入外部资源

有时候我们要将外部文件/可执行程序嵌入代码二进制中。rust-embed 比较麻烦而且文档不太行,我决定用我自己的方式,带有 zstd 压缩(因为 zstd 解压快,可以尽可能减小运行时开销)。

引入一个 build-dependencies:

toml
[build-dependencies]
zstd = "0.13"

然后写 build.rs,编译时将这些文件压缩为 zstd 包:

在程序中使用宏引入(因为 include_bytes! 必须接受 literal,所以不能用 fn 传入 path):

rs
/// decompress the prebuilt zst file and write to a temp file.
macro_rules! write_prebuilt_zstd {
    ($zst_filename:expr, $output_path:expr) => {{
        let compressed_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/", $zst_filename));
        let file = std::fs::File::create(&$output_path).expect("create temp file failed");
        let mut decoder = zstd::stream::Decoder::new(std::io::Cursor::new(compressed_bytes))
            .expect("zstd decoder create failed");
        let mut writer = std::io::BufWriter::new(file);
        std::io::copy(&mut decoder, &mut writer).map(|_| $output_path)
    }};
}
write_prebuilt_zstd!("dwarfs.exe.zst", my_path)?;

这样就可以把嵌入进二进制里的内容解出来了。

当然也有现成的库 include_assets 可以做到这一点。不过这个库基本不再维护,且 pull request 区也有指示其 bug,建议谨慎使用。

Cargo

rust 唯一官方指定包管理器:cargo,而且在一众语言包管理中是顶级的。

cargo envs

常用 cargo 指令

太常用的就不说了。

我的配置

创建 ~/.cargo/config.toml,参考我的配置。

然后由于现在大家都用 CI release,因此 [profile.release] 要写在项目里而不能写全局。

一些解释:

fmt

在 rustfmt.toml 里写代码的格式化选项。我一般会开这些:

toml
group_imports       = "StdExternalCrate"
imports_granularity = "Crate"
merge_derives       = true
unstable_features   = true
wrap_comments       = true

懒的话也可以直接抄前辈的。

构建

cargo build 在全局获取包与依赖的源码,并编译到 target 里。rust 的包构建体积膨胀非常厉害,而且同一份源码的编译产物可能不同[3],因此没有全局缓存,还是需要为每个仓库都编出中间产物。

不过可以试试 sccache 全局缓存,可以减少重复构建。

扩展

cargo 扩展跟 git 扩展很像,只要是名为 cargo-xxx 的可执行文件都能视作 cargo 扩展。这些扩展与 cargo 一起组成了整个优秀的 rust 工具链生态。以下列举一些常用的 cargo 扩展应用。

名字 简介
miri rustup +nightly component add miri,用于更严格的测试,检测内存泄漏与不安全,死锁等
cargo-binstall 安装 binary,减少从源码编译
cargo-bloated 用于优化产物体积。群友作品,据说比 cargo-bloat 更好用
cargo-expand 展开宏
cargo-msrv Find the minimum supported Rust version (MSRV) for your project
cargo-wizard 提供编译模板以配置为最大性能、快速编译时间或最小二进制大小。感觉一般,对于新手还行,老手不都有自己的模板吗。
flamegraph benchmark 火焰图
cargo-bisect-rustc 二分查找哪个 rustc nightly 版本引入了错误
cargo-shear / cargo-machete Remove unused Rust dependencies
cargo-nextest 好用的 test 工具,有超时失败,log 筛选等特性
cargo-audit 查依赖漏洞
cargo-hakari 加速构建的黑科技
cargo-selector TUI 快速选择运行目标
cargo-sweep 部分清理编译产物
cargo-depgraph 看依赖关系图。这一个工具的依赖有点多,不能直接出图,感觉不太好用。
cargo-semver-checks 检查 API 是否遵循 semver 规范
cargo-feat 在命令行直接看 crate 的 feature。警告:该工具为 vibe coding 产物,质量一般,不建议使用。vscode 装个 tombi 就能有 feature 提示了。
cargo-fuzz 模糊测试工具,详见 fuzz

三方库评价

由于本文太长了,所以拆了一篇文章,详见 crates 评价。

打包

说到打包就不得不提万恶的 openssl,我已经喷了无数次,无数次,无数次[4]…。很多库会提供 rustls feature 来绕过 openssl,例如 reqwest;但是也有库根本不提供,例如 rusqlite。所以 openssl 的问题还是得去解决。

profile

我习惯设置为这样。

toml
[profile.release]
lto = true
opt-level = "s"
panic = "abort"
strip = true

opt-level = "s" 是综合考虑性能与二进制大小的等级。一般来说性能还是比二进制大小重要的。如果要最大化性能请设为 3;如果要最小化二进制大小请设为 "z",并且看看 min-sized-rust 文章。

交叉编译

sh
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl

我也写过 rust release CI,深知交叉编译在 link 阶段很容易出问题。解法有两个,一个是用工具链对应的链接器,还有一个就是 cargo-zigbuild,蛮好用的。不过注意,windows 和 macos 不能用 cargo-zigbuild;然后这玩意也经常出问题,要做好心理预期,我也骂了很多次。

release

说到 release,我首推我自己写的 rust simple release,优势是配置非常简单 + hack openssl,可以专注代码而无需折腾 CI。(结果折腾 CI 的变成我了,天天跟 cargo-zigbuild 打)

再说到我之前用的 taiki-e/upload-rust-binary-action,我也用了很久,说句实话还行,但是它内部用的 cross docker container,如果有除了 rust 的其他依赖,或者遇到傻逼 openssl 的问题就没辙了。

cross action:使用 docker 容器进行 build,但是不提供压缩产物。

发布

将包发布到 crates.io 上也是极其方便的。直接 cargo publish 即可。

我不建议使用 CI 进行 publish (具体原因)。反正 publish 是低频操作。

测试

assert 有 assert!() 和 debug_assert!() 之分,前者在 release 下仍然会进行 assert,而后者不会。

assert2 是一个全兼容 assert 的更好看的第三方库,是 pretty_assertions 进化版。

关于测试看这一篇就够了。小总结/补充:

benchmark

cargo bench 是 rust 自带的 benchmark,但是还在 nightly 阶段。学习可以参考这篇文章,讲的不错。

criterion 是 rust 界最知名的第三方 benchmark 库,它可以在 stable 下使用,并且有更多的功能,例如其默认自带 3s 的 warming up,随机抽样和稳定的耗时,比 cargo bench 好用。不过也有些缺点,例如不能写在文件内部做 unit bench。一个 criterion 的例子:

rust
use criterion::{Criterion, black_box, criterion_group, criterion_main};
use sha2::{Digest, Sha256};
fn bench_md5(c: &mut Criterion) {
    let data = b"hello world";
    c.bench_function("md5", |b| {
        b.iter(|| {
            black_box(md5::compute(data));
        })
    });
}
fn bench_sha256(c: &mut Criterion) {...}
criterion_group!(benches, bench_md5, bench_sha256);
criterion_main!(benches);

用户界面

GUI

GUI 是 rust 日经问题了。

一些 GUI 框架:

这里有一些 issue/star 数对比。Are we GUI Yet?是更多 GUI 框架简介。

我早期尝试过一下 iced,用不明白,不用了。反正自从我开始写前端后我就不再写其他非前端 GUI 了,实在是太 naive 太难受了。

他们之中有哪个能达到 electron 80% 的可用程度,称为可用。

——阿卡琳

TUI

比起 GUI,rust 重心还是在 CLI 和 TUI 上。

ratatui

一个广泛使用的 TUI 框架,教程还不错。

我读源码花了挺久时间。如果只想快速上手,建议狠狠抄这个 example。

用着发现个 bug,顺带 pr 了几行[5]。

然后 tui-realm 是基于其做的一个高层 TUI 框架,或许可以一试。

ORM

(拳打柴油机,脚踢大海洋)

我其实用 sqlx 习惯了,感觉也不一定需要 ORM 辅助开发,SQL 本身就是一个相对完备的转译兼容层,sqlx 也有编译期 SQL syntax check。

不过如果有看起来不错的 ORM,我也有一些意愿尝试。我个人还是倾向于基于 proc macro 的 ORM,结构即描述,看着比较干净一些。目前的记录:

音频处理

音频处理无论在哪个语言都是天坑之一。

Rust 虽然有万能解码器 Symphonia(Symphonia 的 bug 和兼容性也有待提高),但是编码器这块缺的可太多了,除了 wav 这种简单格式有 pure rust 的 hound,大部分编码器都还只有 bindings。

另外还有一些标榜自己是 pure rust 的音频库,但是实际上质量并不咋地,例如 opus-rs,详见评价。

嵌入向量数据库

我需要 rust 侧的轻量嵌入式向量数据库解决方案,要求是 10,000,000 个向量内查最近邻。(每个向量还有附带额外信息)

简单看了一下。我不希望将所有数据先加载到内存,最好的方案应该是 DiskANN,不过这玩意目前还没有 rust 的实现。 微软给重写到 Rust 了,牛逼。

r18n

去 luoxu 随便一搜,发现 i18n 模型是个自古以来的难题。

我也找了一些看,包括 rust-i18n, r18, i18n-embed, fluent-rs,最后还是感觉 rust-i18n 文档清晰,模型简单,比较适合我的项目。

奇技淫巧

nightly on stable

古语云:async 就像海洋,只有打开 nightly 的人才能到达彼岸。——Sherlock Holo

Rust nightly 有许多好用的东西,然而 nightly 工具链只有日期,没有版本,导致想要下载一个特定版本 nightly 工具链还需要去了解 rust release 流程,十分不友好。fuk

但是有一扇窗为 nightly 打开,那就是 RUSTC_BOOTSTRAP=1。只要开了这个环境变量,就可以不需要重装任何工具链,纵享丝滑 unstable featureref。

worktree 协作

如果你希望开多个 git worktree 并行推进任务(AI 时代非常常见),则可以考虑共享编译缓存,因为 Rust 的编译产物实在是太大了。比如一些大型项目本体构建就数十 GB,则多个 worktree 可以分分钟把你的磁盘干爆。

首先共享编译缓存是只有 NTFS user 或者 ext4 user 需要考虑的东西,其他的 fs 可以直接用 CoW 复用 target。四舍五入编译产物问题基本上只烦 windows 用户。

sccache 虽然可以复用缓存内容,节省 CPU,但是并不能帮你节省磁盘空间。因为匹配到构建内容后,sccache 是把缓存的构建产物 copy 到新 target 里的,没有 reflink,磁盘占用其实是双倍。

可以使用 CARGO_TARGET_DIR env 来让所有 worktree 共享一个 target folder。虽然会牺牲一点并行度(共享 target 后两个 worktree 无法同时构建),但是构建时长本来也只占开发中较小的一部分,因此可以放心使用。

external

books:

  1. Rust 语言圣经:圣经,文风上乘,质量高。
  2. tour of rust:交互授课式,基础入门。
  3. rust by example:注重例子。
  4. 小宏书:专门介绍 rust macro
  5. Rust Atomics and Locks:底层并发原理入门
  6. Rust 入门秘籍:一本既简洁又深入的书,非常值得一看(特别是 tokio 相关章节)。
  7. Rust 编程第一课 - 陈天:比较进阶的书,写得很好,着重讲述了难点和实战
  8. Rust magic patterns:针对某些狭小的知识点的深入分析

articles:

  1. Rust Learning Smart Pointers
  2. Rust 中的闭包递归与 Y 组合子
  3. 随机 Rust Quiz - dtolnay / Rust Quiz - boxyuwu:想成为语言律师吗?
  4. 为什么 Rust 需要 Pin, Unpin ?(中文翻译)
  5. 如何理解 rust 中的 Sync、Send?
  6. Rust 的 Pin 与 Unpin
  7. static, const, let 声明变量有什么区别?
  8. An introduction to advanced Rust traits and generics
  9. [翻译] async: 什么是 blocking
  10. Rust Runtime 设计与实现-科普篇 及后续系列文章
  11. Using Rust Macros for Custom VTables:如何创建一个 runtime object
  12. 金枪鱼之夜:基于完成的 Rust 异步:compio 项目及其经验
  13. 用 Rust 搞科研的两年
  14. The missing parts in Cargo
  15. Fast Rust Builds
  16. 幽灵索引类型与匿名结构体
  17. Rust 中常见的有关生命周期的误解
  18. Inside Rust's std and parking_lot mutexes - who wins?
  19. Making a const version of Rust's array::from_fn - How hard can it be?,图码文并茂
  20. Apache Iggy's migration journey to thread-per-core architecture powered by io_uring
  21. Interpreting near native speeds with CEL and Rust,了解一下 CEL 及其优化。
  22. Bugs Rust Won't Catch
  23. A definitive guide to sealed traits in Rust
  24. A 2026 Survey of Rust GUI Libraries,作为互联网喷子,我非常认同这篇文章,作者做了大量的工作来支撑自己的观点,并且文风语气都比较幽默。

  1. 由于 rust 的 I/O 较为麻烦,leetcode (比起 洛谷 等)能免去 I/O 之苦。 ↩︎

  2. https://t.me/QC_Grove/734 ↩︎

  3. 哪怕同一个编译器同一个包 rust 的编译是有副作用的,比如 env 宏 build script 乃至 proc macro,都是能任意副作用的 (ref) ↩︎

  4. openssl 不仅编译的工具链垃圾,性能也垃圾,打不过 pure rust 的 rustls,打不过手写的 simd。这种垃圾还有什么存在的必要吗? ↩︎

  5. review 还挺严格的,但是 member 说话又好听 ↩︎