crates 评价
crates 评价
热门
有一些库几乎成为业界标准,必需掌握。
| 库名 | 简介 |
|---|---|
| anyhow | 一般用于 bin target 的错误处理,把所有 error 统一起来,可以偷懒,有需要也可以 downcast 到具体错误类型 |
| thiserror / snafu | 一般用于 lib target 的错误处理,需要让下游使用者区分错误类型并区别处理。详见 错误处理 |
| arc_swap | 高性能的读多写少并发容器,读过程无锁。详见 arc_swap |
| tokio | 异步运行时 |
| serde | 序列化与反序列化 |
| reqwest[1] | 高层次的 Http Client |
| clap / palc | 命令行工具,后者是为了减小二进制体积而使用的,详见 clap |
| tempfile | 创建自动销毁的临时文件/文件夹 |
| rayon | 易于使用的线程级并发库,针对 CPU 负载任务 |
| indicatif | progress bar |
| colored / simply-colored | 命令行颜色输出,后者更适合用于 no_std |
| rand / smallrand | 随机数,后者更适合用于 no_std |
| parking_lot | 一个解锁分配更公平的、没有 poison 的互斥锁 |
| enum_dispatch | 如果一个 tagged enum 的每个 variant 都实现了某个 trait,那么此 enum 本身可以直接实现这个 trait。(trait 不可携带 type) |
| walkdir / ignore | 递归访问文件系统。如果你要处理 gitignore 请用后者 |
推荐
另外一些库则是我用过然后觉得好用。
| 库名 | 简介 |
|---|---|
| memchr | 字符串查找 |
| assert2 / pretty_assertions | 全兼容的好看的 assert |
| tap | 函数式工具,在链式中途拿取引用操作而不影响返回值 |
| enum-tools / strum_macros | 提供 enum 的常用方法,最常用的就是字符串互转了 |
| pollster | 小而美,专注于 在同步环境运行异步函数 一件事,打破同步与异步间隔 |
| expect-test | 自动更新 test 中 assert_eq 的期望值 |
| const-hex | Vec<u8> -> hex str |
| constime | 计算编译期值,用一个非常简单易用的宏 |
| inquire | 用户命令行交互 |
| samply | profiler (support flamegraph, tutorial video) |
| binrw | 二进制内容的序列化 / 反序列化 src |
| terminal-menu | 一个 TUI 终端选择器,小巧又好用 |
| ustr | 全局去重的 &'static str 池,详见 字符串进阶 |
| flexi_logger | 如果你想在 log crate 下使用一些高级收集特性,可以尝试它,用着非常舒适,而且我想要的功能,rolling、compress、batch 等都是开箱即用。这是一个我如何使用它的样例。 |
这里还有一个常用库的列表可以参考。
拉黑
当然,也有一些避雷条目一生黑(不仅限于 lib):
| 库名 | 吐槽 |
|---|---|
| teloxide | Telegram bot 库,但是没有文档,只有一点最简单的 example;遇到各种问题没有解决方法;API 经常 break 并且设计得很丑 |
| rusqlite | 绑定了 openssl!不要用它,要玩 sqlite 请左转 sqlx |
| listeners | 有严重的性能问题 ref |
| crossbeam-channel | 该暴露的方法不暴露,该设计 trait 时不设计 trait,该实现的功能没有实现,性能还不如 crossfire (ref);PR 卡着不推;miri 测试不兼容 |
| pingora | issue 爱理不理,trait 设计糟糕,大公司开源但不是真正意义上的开源 |
| tracing 系 | 性能不如 fasttrace 一根,tracing-appender 代码写得一坨狗屎,众望所归的 feat pr 都喂到嘴边了就是不合;如果你对 tracing 本身架构没有足够了解,很容易被干到死锁 (issue),并且这是文档里没有指明的 |
| xq | 跟 jq cli 不兼容;纯纯傻逼玩具,性能垃圾,打一个 1G json,jq 和 jaq 峰值内存都用不了 6G,xq 吃了 20G 都打不出来 |
| tauri_specta | 2.0.0-rc.21 的指令是错的,cargo add 会版本冲突;有些第三方可以 serde 的类型没有实现 derive(specta::Type),例如 chrono:😗;很多第三方 error 类型也没有实现 serde/Type,没法用。建议只用 ts-rs 做 struct 结构生成,事件还是用 tauri 那一套。 |
| tauri-plugin-http | 这是它的 issue 区,低级问题太多了,不知道一个 client 为什么能做得这么屎。还容易遇到权限问题。 |
| wasm-pack | vibe coding 重灾区,全是 emoji 但是关键内容缺失的大便一样的文档;还有大便一样的代码质量和放着根本不理的 issue 区,npm postinstall script 还会卡住。请使用 wasm-bindgen-cli。 |
| faster-hex | 打不过 hex-simd。详见 #hex |
细说
clap
一般我都用 features = ["derive"],使用更方便,但是文档更难找,因为文档默认用的是动态添加成员。clap 就是得多用才能熟练,可以多去网上找找例子。
之前我用 clap derive 喜欢把 Cli 放到 static LazyLock,可以免去到处传参之苦。结果发现写测试变得非常困难,因为测试是并发的,不同测试看到的 Cli 是一致的,这样没法在测试里表达不同的 Cli 状态。所以如果 rust 有一个好用的 context 实践的话就好了。
扯远了,说回 clap。我们可能对命令行有更多自定义的验证,简单的场景可以用 serde_valid,这是个比较有意思的日本人写的库,写起校验条件还是比较符合人体工学。如果你的 validate 非常复杂,或者有各种奇奇怪怪的类型,则可以自行 impl Cli 添加自定义的 fn validate(&self),并且在 parse 后调用一次做校验。不要用 clap 自带的 value_parser,那个是一坨大便;或者可以使用某个 serde_inline_default 宏,但是代码这里不好给出,可以私聊我。
其他经验:
- clap 默认不允许
-开头的 value,如果需要,用户可以用xxx=-xxx,开发者可以考虑 allow_hyphen_values. - clap 的 Vec 默认允许 0 个元素。如果要求用户必须给出内容,可以使用
#[clap(num_args = 1..)]。 - 对于 command 的 alias 尽可能使用
visible_alias,可以在 help 中显示。可以多次使用以定义多个 alias。
如果你对构建后产物有极致要求,可以用群友开发的 palc,功能比 clap 弱一些,但是可以节省几百 KB 的产物大小。干点简单的 arg parse 也是可以用的。
不过注意,palc 默认不支持 -V / --version 打印版本号。
once_cell
创建 Lazy 或 OnceCell 的 static 变量。在 rustc 1.80.0 以前这是 unstable,但是现已 stabilized(std::sync::LazyLock),所以 once_cell 已经没人用了。
字符串进阶
Rust 生态里有着各种各样的字符串,标准库里有一堆,三方库还有一堆。
ustr:全局去重的 'static str。每个字符串只是一个引用;相同的字符串只占用一份空间;可以以极低开销拷贝。只是创建字符串的时候有一次去重对比的开销,不算大。 ustr 是我最喜欢的三方 str 之一,如果你的拷贝次数远大于构造,且不同 str 的构造次数有限(如果字符串是用户输入,容易让内存无限增长),推荐使用它,无需关心各种生命周期,clone 起来也飞快。
arcstr:Arc<str> 的高性能替代(去除了 weak ref 以提升性能)。arcstr 也可以到处 clone 而不用关心生命周期。但是要注意,在多线程下 arcstr 的引用计数的性能损耗还是比较大的。
bstr:非 UTF-8 的字符串。没怎么用,不作评价。
compact_str:在栈上存储 <= 24 bytes 的字符串。
错误处理
rust 界流传着 bin 用 anyhow,lib 用 thiserror 的谚语。它们两个的目的是完全相反的。一个是细化,一个是归一。
- anyhow 可以将所有错误归为一类往外抛,并且还有额外信息(context)支持。
- anyhow 比较“重”,会增大你的二进制大小。如果你不需要用它的一些额外特性(例如 context),也可以
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;。
- anyhow 比较“重”,会增大你的二进制大小。如果你不需要用它的一些额外特性(例如 context),也可以
- thiserror 比较轻量,用来细分自定义的 error 类型,可以理解为 derive From another error 的容器。
- 不能在两个错误类型中同时 from 同一个 Error。如果确实需要,可能要手动再分 Enum 作为 suberror。
而 snafu 是一个很有野心的挑战者,它可以同时适应类似 anyhow 抛模糊错误和 thiserror 抛精确错误的场景。但是 snafu 也是有缺点的:
- snafu 虽然通过 proc macro 生成
xxxSnafu来简化错误创建,但是本质上还是手动挡。大量的 context 会让代码复杂度变高,并且写 context 的时候如果不注意可能会造成性能问题(就像.context(xxxSnafu { path: p.to_path_buf() })这样,即使不进错误处理的分支也会多进行一次字符串拷贝)。- snafu 对标 anyhow 的部分,也就是 Whatever,并没有想象中的那么好用。即使对于自己创建的 Snafu,每个 Whatever 也都需要 .whatever_context 来抛出异常(如果写闭包的话代码又更长了)。
我的观点大概是,anyhow 对于 binary target 基本是必备,anyhow 的方便是其他库不能替代的。而对于 lib target,thiserror 当然没有问题,如果你的库不需要细分其他 Error 可以用 thiserror 自动档,如果有更细致的要求就用 snafu 手动挡。没有必要专门去用 snafu 重写当前的错误处理方案。
另外如果你对错误处理感兴趣,也可以看看 eros 等小众的错误处理 crate,每个 crate 都有其设计理念。
日志
说日志的话总共也就两套方案,一种是传统的 log 方案,另一种是 trace 方案。trace 方案基本上算是给网关和 server 用的,像我这种写垃圾小玩具的肯定是接触不到了。
log 方案
log 方案的好处就是 log crate 非常统一,而且用起来跟其他语言很像,比较简单。但是 log 只提供底层 API,而如何展示就有很多种选择了。
- 对于一般的小玩具,基本就只是支持一下颜色和时间戳输出。我之前一直在用 pretty_env_logger (based on env_logger),不过由于 env_logger 引入了 regex,这玩意对编译后二进制大小有较大影响,所以我也在寻找更符合需求的替代品。
- 而且 env_logger 是完全同步的,不适合多线程打日志。
- 对于更大一点的玩具,需要更多功能的,flexi_logger 用起来是手感较为舒适的。
- 但是这玩意问题是代码质量比较一般。
- 如果需要简单、额外功能较少的日志库,也可以使用 Rust 群群友的 spdlog-rs。比起 flexi_logger,spdlog-rs 少了各种特殊 file flush 策略和轮转后日志压缩等功能,但是这些本来也不是一个日志库的核心功能。spdlog-rs 的 benchmark 是很好看的。
- 一定要记得开
flexible-stringfeature!这个 feature 还能进一步优化性能,省掉每次打日志的堆分配开销。而且这个 feature 其实藏得比较隐蔽,它并不在 Cargo.toml 的 features 里,但是可以开。
- 一定要记得开
trace 方案
trace 方案最常见最泛用的就是 tracing 了,跟 tokio 一样,大企业都在用。但是我不太喜欢(tracing 的一些生态),详见前面的拉黑。如果你做网关 / http server,并且确实需要 trace 方案的,可以看看 fastrace,号称是最快的 trace 方案库,性能方面极具竞争力,生态也尚可(有 tracing 兼容层)。
还有比较邪道的日志库使用 log crate 的生态实现 trace 的效果。例如 context-logger,emmm 不过这个 crate 基本跟手动挡一样,感觉自己写个也不难。
channel
crossbeam-channel 被我拉黑了,大家可以选择 crossfire。它的 crates 页 还有一些跟其他 channel 库(kanal[2], flume)比的 benchmark。
queue
channel 和 queue 还是有点区别的,一般来说 channel 底层是一个 concurrent queue,只是在外层包装了线程唤醒、select 等逻辑。
crossbeam-queue 和 concurrent-queue 是并发 queue 常用的两个实现,并且它们原本就是一家(最初作者都是 stjepang,后来神秘隐退)。社区也有让这两个 queue 合并为一的声音。
但是论性能,concurrent-queue 还是打不过 crossbeam-queue 的。比如 crossbeam-queue 从很久(2018-2019)以前就引入了 crossbeam-utils::Backoff,并且在 ArrayQueue 和 SegQueue 一开始实现就采用了 Backoff 作为 CAS fail / slot stamp/block 路径,但是 concurrent-queue 并没有使用 Backoff,而是直接 busy_wait/yield_now。我这里实测在中高度争用的情况下有 Backoff 的性能可以比无 Backoff 要高出 100% 以上。也难怪 concurrent-queue 没什么人用了。
其次,concurrent-queue 引入了一个 full_fence 的 lock not hack,但是这个 hack 在 2026 年已经过时了。实测 LLVM 已把 x86 的 fence(SeqCst) 降级为单条 lock orl $0x0,-0x40(%rsp),做了这个 hack 反而有两条指令 movq $0x0,(%rsp) + lock notq (%r15),导致性能退化 ~8-10%。
而且 crossbeam-queue 还有一些 concurrent-queue 没有的小优化,比如 get_unchecked 去掉无用的越界校验、mem::needs_drop 减少 drop 操作等。因此一般不推荐使用 concurrent-queue。
另外,这类 concurrent queue 已经明确说明,从设计上就是为了跑多核的,在单核嵌入式处理器或 RTOS 上运行时,都有一些性能问题,严重的还可能死锁 ref1 ref2,使用的时候需要特别注意。
PS. concurrent-queue 在线程数少、低竞争情况下性能较好;可以尝试下 youpipe-concurrent-queue,这是为我的 youpipe 项目 fork 的版本,做了一些性能优化。
serde
除了直接 derive 外,serde 一般用得多的技巧还有:
#[serde(rename = "xx")]和#[serde(rename_all = "kebab-case")],自定义序列化的名称与格式。更多宏可以看 doc Field attributes。- 对于需要在缺失时使用 empty 的容器对象,
#[serde(default)]是个不错的选择。 - 如果有的结构需要手写 parser,可以顺带实现 serialize trait,代码不会太多。
- serde 提供了 remote derive,也就是为第三方 crate 里的 struct derive serde。但是我没用过,看起来不太好用的样子。
一些 serde 插件:
- serde_valid_derive:使用宏对某些字段进行 validate,避免写大段函数。这玩意还是比较早期的阶段,不够好用,但是出发点是好的,开发者修 issue 也修得也很快。
rayon
rayon 现在已经几乎统治了 rust CPU 负载型的并发。使用 rayon 可以非常方便地写出多线程程序,榨干你的 CPU,并且本身是同步的,无需引用任何异步运行时。并且 rayon 本体也是相当轻量。
rayon 内部有一堆线程池 + 一堆奇形怪状的锁和管道,然后通过工作窃取最大化核心利用率,是很有一套的。我之前做过一点 pipeline,想跟 rayon 碰一碰;测出来对于均衡负载的工作性能,我的 pipeline 不弱于 rayon,但是在非均衡负载下 rayon 把我按在地上摩擦。
rayon 的基础示例可以读 doc 或让 AI 给 example,不再赘述。
rayon 的生态也不错,一个常用的是 indicatif (features = ["rayon"]),它可以让 rayon 并发处理时显示易于阅读的进度条,这在一般耗时较长的 CPU 负载场景下是非常好用的。
use indicatif::{ParallelProgressIterator, ProgressBar, ProgressStyle};
let process_pb = ProgressBar::new(files.len() as u64);
process_pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}")
.expect("Internal Error: Failed to set progress bar style")
.progress_chars("#>-"),
);
files
.into_par_iter()
.progress_with(process_pb.clone())
.for_each(|entry| {...});
process_pb.finish_with_message("Processing complete!");- 对于数量巨大、每个任务开销较小的任务,我们可能希望在一个 worker 里一次处理多个任务,避免过多的上下文切换,并且内部 for 循环还可以自动向量化以提升性能。rayon 提供了 par_chunks 来做到这一点。
sqlx
如果你写 SQL 比较熟练,不需 ORM,那么 sqlx 就非常适合你。尤其是在当前 Rust 还没有任何特别好用的 ORM 的环境下,sqlx 更是一个不差的选择。
说到 sqlx 就不得不提,它是强制类型的,因此在编译时就需要获取数据库表信息,例如使用 sqlite 时用户需要为其提供一个模板 sqlite db 文件。但是(假设用户没有装 sqlite cli)创建一个 sqlite 本身就需要 sqlx,就遇到了鸡/蛋问题。而且修改 schema.sql 也有可能忘记重新构建模板 sqlite。这时候就要用一个 build.rs 在 schema 初始化或改变时自动更新模板 sqlite。这个 build.rs 我写在了这里。官方也有提供 sqlx-cli 来做这件事,如果你不喜欢滥用 build.rs 也可以用 sqlx-cli。
tauri
由于我只写 web based GUI,因此 tauri 成了我的唯一选择。网上有很多喷 tauri 的,我用得不爽也会喷,但又不是不能用,实现我的需求还是没问题的。
- 一开始就不要对 tauri 抱有太大期待,当成一个 IPC 框架用就行了。
- tauri 的很多插件比较狗屎,上面 拉黑 写了一些。很多插件是为了在 js 里操作只有 rust 能拿到的资源,这里建议不要用它们,自己手写 rust 然后暴露 command 给前端调用,这样比较可控。
- 用 tauri 插件有权限问题,很烦,有时候调试半天结果发现是没给权限。如果自己写 rust 代码就没有这些问题。tauri 这些权限并不会让系统更加安全,只会加重开发者的心理负担。
- 而且 js/ts 调某些资源还是比较危险的,先不说 nullable 语言和类型隐式转换一坨大便,光是前端响应式什么时候会重复执行,什么时候没法执行,就够开发者喝一壶了。
- 如果 vibe coding 小子那就更应该多写 rust,少写 js/ts,这都是血的教训。
- 当然有些插件也是可以用的,tauri_plugin_window_state、tauri-plugin-single-instance 这些都比较简单,没啥问题。像这两个插件,写 tauri APP 基本算是必须引入的,可以提升关键的用户体验。
- 错误处理一般是自己写 thiserror,还必须实现 Serialize,这个没法 derive(很多 from 的错误都没法 derive Serialize),只能自己写个:
impl Serialize for Error { fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> where S: Serializer, { serializer.serialize_str(self.to_string().as_ref()) } } - tauri 启动速度比较慢,也没啥能抠的性能优化(webview 已经完蛋了),如果你配置比较大,可以用
window.__INITIAL_CONFIG__抠一次 IPC:let config_json = "..."; let main_window = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into())) .title("GalgameManager") .inner_size(800.0, 600.0) .resizable(true) .center(); .drag_and_drop(true) .user_agent("github:lxl66566/GalgameManager") .initialization_script(format!("window.__INITIAL_CONFIG__ = {config_json};")) .build()?; // use in ts side: globalThis.__INITIAL_CONFIG__
image
image 库基本就是 Rust 图像生态里知名度最高的库了,image 提供了对大量图像格式的统一操作支持,用来写 server 等业务逻辑属于必备库。
然而读了一点 issue 和代码后,我感觉 image 本身的质量比较一般。可能也是缺人的缘故吧(计算机里的图像天坑),image 的迭代速度一直很慢,0.22 一年,0.23 两年,0.24 两年,所以即使 image 处于 0.x,也没法随意进行 breaking change。
image 的性能其实非常糟糕:
- 最大的问题就是为了兼容各种图片格式/用户自定义结构而搞出的 GenericImage trait,很多热点路径都在用 GenericImage 的
get_pixel和put_pixel,这俩玩意内部有 2-3 个越界检查分支,LLVM auto vectorize 看到立刻破防了。图像处理最重要的就是 batch processing,简直是为 simd 天生打造的竞技场,然而 image 除了几个依赖用了 simd feature,自己基本没有 simd 代码,之前有人想搞的 simd 计划现在也没声了。 - 项目里虽然 rayon 是 default feature,但是几乎没人用,尤其是某 Contributor 还以「用 rayon 有被 DDoS 耗尽内存的风险」为由,关闭 pic-scale-safe 的 rayon feature,我真的被无语到了。本来 image/rayon 关联 deps/rayon 就是最佳实践,处理可控输入就开内部 rayon,批量处理外部输入就在外部用 rayon 然后关内部的 rayon,一切都是如此自然,我决不能接受这种奇葩理由来故意降低性能的行为。
- 其他的还有
&dyn GenericImage动态派发无法内联、u8 -> f32 -> u8 等。
RustCrypto/AEAD 与加解密
RustCrypto 的 AEAD 实现大致也是最泛用的 pure Rust 实现,其他的要么用 aws-lc-rs 要么是 openssl。
RustCrypto 的性能仍然是最大问题,网上可以找到许多 benchmark 资料,RustCrypto 就是比 aws-lc-rs/openssl 慢几倍。issue 里也讨论过这个问题,但是显然 6 年后好像也没什么进展。
由于我主要用的是 ChaCha20Poly1305 / AES-256,这里以 ChaCha20Poly1305 为例讲讲。ChaCha20Poly1305 在 openssl 里的实现是有把 ChaCha20 与 Poly1305 融合的,而 RustCrypto 是分为两步执行。这样不仅有额外的数据拷贝,而且编译器也不好进行自动向量化。
如果你正在找 ChaCha20Poly1305 的实现,建议看看 chacha20poly1305-simd。性能表现不差,并且 pure rust 不依赖其他工具链。虽然大部分是 vibe coding,不过也经过了 fuzz 验证,可以一试。
Hashes
虽然前面批了一顿 RustCrypto,但是在 hashes 上粗看了一下,RustCrypto 的实现也没什么可挑剔的。常用的 hash 算法,比如 sha2,基本都已经顶到指令集的性能上限。而且 RustCrypto 支持的指令集还多,loongarch 都有,实在是开了眼界。
hex
hex 编解码是 simd 的竞技场。faster-hex 和 hex-simd 是其中的两个佼佼者;faster-hex 的下载量比较多,许多知名库都依赖 faster-hex,但是我深入看了下,感觉代码质量配不上它的下载量。
首先 faster-hex 完全没有 NEON decode,aarch64 上解码 hex 只能走标量实现;其次是 faster-hex 为了保证「如果 hex invalid 则不会往 target 里写东西」,使用的是两遍扫描:第一遍是 hex_check_with_case 扫一遍输入做校验,然后再 hex_decode_unchecked 扫一遍做解码。所有架构、所有输入长度都是两次遍历。这不就是狗屎实现吗,脏数据要不要写入 target 当然是调用方来决定,faster-hex 直接全部 check 一刀切然后性能减半(hex simd 已经是内存带宽瓶颈了),我觉得不是用户希望看到的实现。
再说说 hex-simd。首先 hex-simd 只是 Nugine/simd 里的一部分,这个 repo 有一个共用的底层 vsimd 实现,把 unsafe simd 抽象藏到 trait 后(可以简单理解为手搓的一套 portable-simd),代码极为优雅。
总之,faster-hex 的 crate 大小、性能、代码质量等全方位弱于 hex-simd,不推荐使用 faster-hex。
allocator
rust 的默认 allocator 并不好用(ref);只需要 3 行代码更换一个 global allocator 即可获得免费的性能提升。
之前比较广泛使用的 allocator 是 jemalloc,之前还假死了一段时间,但是现在又活了。jemalloc 是经过了大量实战测试以及生产环境的验证,可以算是知名度最高、使用最广泛的 allocator 了。jemalloc 比较适合长时间运行的 server program。jemalloc 的缺点是不支持 Windows。
mimalloc 是一个有力竞争者,可以看看它的性能测试。mimalloc 支持 Windows;但是我曾今遇到过一个严重的问题(所有使用了 mimalloc 的软件在 Windows11 24H2 上稳定崩溃),因此对其并无好感。
fuzz
fuzz 本质上是生成一堆随机输入,然后测试自己的程序在该输入下是否 panic、coredump,或者跟权威处理过程做对拍,验证输出是否相同。
最流行的 fuzz 工具就是 cargo-fuzz 了(内核为 libFuzzer),AI 写得有模有样;虽然它的 README 说不支持 Windows,但是文档里写了支持,并且我实测过了确实是支持的。
除此之外,还有个 star 数比较多的 fuzz 工具是 afl.rs,我也尝试了一下,不过体验相当差劲。
- afl 本身文档比较糊,难以理解,也没法一键看出跟 cargo-fuzz 的优劣。
- 想要运行 afl 测试需要让你的 target 链接上 afl-compiler-rt.o。NixOS 包管理的 afl 版本不匹配,用不了;而
cargo afl config --build默认会从 AFL++ C 源码里编出afl-compiler-rt.o,但 NixOS 上它根本找不到 glibc 头文件,编不出来,只能手编。 - 还有一些隐蔽的坑,要不是 AI 的话我早放弃了。
- afl 在性能方面也没有 libFuzzer 强。
音频(opus)
之前我看过一个 pure rust 实现的 opus 编解码库 opus-rs。本来音频处理就是天坑,opus 更是音频处理中的一座山巅,因此个位数时点了个 star 支持下。
但是后续的代码 review 就暴露出了一堆问题。项目 README 虽然声称 production ready 但是绝不能在 production 里用。最大问题是 SILK 编码根本不支持立体声,当前实现里立体声输入被分解为 mid=(l+r)/2,直接压成单声道。另外帧长 40ms 的立体声 SILK 编码会直接 panic,因为 FixedVec 容量按最大单帧 20ms@16k 设计,没有考虑 40/60ms 多帧包。
还有一个问题,由于太过专业了我看不懂,这里直接贴出 AI 结论(已通过另一 AI reviewer 复现验证):
SILK 立体声编码在所有采样率下都有实质缺陷:
(a) 24/48 kHz(Hybrid 路径):模式选择下 24k/48k 的 SILK-only 一律转为 Hybrid(带宽静态为 SWB/FB,`lib.rs:519-524`),Hybrid 分支把交织立体声直接喂给单声道重采样器:48k 走 `silk_resampler_down_1_3`(`lib.rs:696-704`),24k 走 `down2_3`(`lib.rs:705-714`),重采样器状态是单声道状态(`resampler.rs:11-14, 522, 568`),且 48k 路径实际只消费交织流的前半段(L 半帧‖R 半帧拼接,L/R 边界处滤波器振铃)。编码出的 "mid" 是被切碎的 L‖R 信号。C 的实现是每声道独立重采样状态,且在重采样后做真正的 L/R→M/S 转换(`stereo_LR_to_MS`)。
(b) ≤16 kHz:mid/side 拆分后 side 被完全丢弃(`lib.rs:679-695` 写入 `silk_enc.stereo.side` 后再无使用),`silk_encode_stereo(rc, 0, 0, 1)` 硬编码 pred 索引 (0,0) 与 only_middle=1(`enc_api.rs:585-587`、`encode_indices.rs:165-180`)。关键在于 mid-only 并不等于 L=R:解码端(与 C 一致)会用 pred 从 mid 预测合成 side(`stereo_ms_to_lr.rs:55-95`),而索引 0 解码出的 pred 是非零固定值——于是从正确的 mid 合成出错误的"幻影 side",L/R 都被污染。
实测数据(SNR,对齐后;C 列为本机 libopus 基线):
| 配置 | C→C | Rust→Rust | Rust→C | C→Rust |
|---|---|---|---|---|
| Hybrid stereo 48k Voip 32kbps(M3) | 2.0 / 2.2 dB,LRdiff 0.32 | −0.7 / −5.3 dB | −0.7 / −5.3 dB | 2.0 / 2.2 dB |
| Hybrid stereo 24k Voip 32kbps(M7) | 2.9 / 3.0 dB,LRdiff 0.33 | −1.2 / −7.7 dB,LRdiff 0.77 | 同左 | 3.0 / 3.1 dB |
| SILK stereo 16k Voip 24kbps(M2) | 3.0 / 2.9 dB,LRdiff 0.33 | 0.1 / −2.1 dB,LRdiff 0.52 | 同左 | 3.0 / 3.0 dB |
两个决定性对照:C→Rust 与 C→C 完全一致(Rust 解码器对真立体声流工作正常,缺陷孤立在编码端);Rust→C 与 Rust→Rust 一致(C libopus 解 Rust 码流同样差——缺陷在码流本身,不是 Rust 解码器兼容性问题)。另注:Rust→C 的码流是合法可解的,不会破坏 C 解码器,纯粹是音质/声道像损坏。感觉 opus 最复杂的一些问题都被绕过了。还有 #5: Panic on valid SILK 40/60 ms frames: SILK workspace hardcoded for 20 ms 作者说修了,但实际上只(暴力扩容)修了解码端,编码端完全相同的问题根本没修,感觉作者对自己的代码库都不是很了解。最后看了下提交历史,vibe coding 味还是相当重的。看着这位国人开发者有一堆音频领域的成果,希望不会都是 vibe 的吧……。
我对上述内容提了一个 issue,然后看作者修复它的提交记录,一眼 AI 直接实锤了,根本不需要想。
opus-rs 永远地失去了我的一颗星星。
cargo-binstall
虽然它不是一个 lib crate,不过我仍然想放在这里说(反正也都是 crate)。
202606 之后部分出国线路的网络质量大幅劣化,然后我用 cargo-binstall 安装新软件的耗时都会是 35s 左右。我真的很诧异,为啥装个 binary 耗时能这么久,然后开 DEBUG 看了下这玩意会经过串行的 6 个阶段,每个阶段里可能有一个或多个请求。首先拉 crates.io 的请求就有 3 个,然后在 Github 上花掉 3 个;而每个阶段内(特别是 Github 阶段)又会有多个并行请求,相当于让该阶段的时间变长(最慢的请求决定阶段时长)。
另外 cargo-binstall 的安全性也是狗屎,linux 和 windows 上各一个目录解压逃逸漏洞。虽然作者观点是不构成危险,因为从 Github 安装 binary 本来就很危险(),但是我仍然认为出现这种低级错误是非常不应该的(引入了 rc-zip 的项目真是有福报啦!)。
tokio-tungstenite
这玩意真的有人在维护吗?然后你可以见到:panic 是特性而不是 bug,接管原始流丢字节没人理。
opendal
因为 Xuanwo 一直在 Rust CN 群里引流,我大概在 2024 引流早期就出于兴趣看了下 opendal,当时的感受就是:文档呢?看完了 README 和文档,我仍然不知道这玩意是干啥的。因为没有使用场景,也就没管。
两年后,2026 年,由于我的 GalgameManager 有多网盘后端上传的需求,我又尝试把 opendal 集成到我的项目里。然后看了 opendal 文档,我无语道:opendal 两年前浅看过一次,感觉两年来文档和 example 没有任何长进。[3]
2026 早期,agent 还没那么流行,虽然 deepwiki 等已经可以快速读源码问问题了,但我古法手工编程还是被 opendal 坑了很多次。
比如我不想加载文件全部内容到内存,在此前提下上传文件到 webdav。opendal 的 FuturesAsyncWriter 默认会在内部用 256KB buffer 进行 chunked 上传,然后 webdav 是 oneshot writer,所以就会爆炸 OneShotWriter doesn't support multiple write。根本原因就是 opendal 没有实现 webdav on chunked,write 的时候必须一把梭算出 content-length。那我为了让 webdav 非流式上传、s3 等流式上传,就又要写一堆丑陋的胶水代码,简直违背了 opendal 的初衷。
还有踩过的一个坑是 fs 上 content-length 不可用,于是我又要为 fs backend 写一堆胶水代码……
另外还有一个安全相关的 PR,这人修了 .. 的 path 逃逸问题,提了一嘴 / 但是没有后文了,我也不知道这是怎么跟安全讨论的…… RFC 7799 也没有任何下文,没有 tracking issue。
反正 opendal 带给我的感觉就是,能用,但用着很难受。
为避免傻逼 openssl 造成的影响,一般建议起手reqwest 从 0.13 起已经将 default ssl 后端切到了 rustls,不需要再手动搞了,好事 ↩︎reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "http2", "charset", "system-proxy"] }。kanal 不支持 poll 形式,也不 cancel safe。 —— Sherlock Holo ↩︎
https://t.me/withabsolutex/2598 ↩︎
