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。

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 说话又好听 ↩︎