如果你写的代码连你自己都看不懂,那编译器看懂又有什么用?
Rust 1.97.0 版本正式发布了。这个版本干了几件大事:默认启用了全新的符号命名规则(symbol mangling v0),让 Cargo 自己管警告是报错还是忽略,还把链接器(linker)以前藏起来的小话痨全放出来了。对于整天跟编译器斗智斗勇的开发者来说,这几个变化就像把你家那位不爱说话的室友突然换成了一个有啥说啥的直球选手。
符号命名规则终于换成了 Rust 自己的版本
编译器把 Rust 代码变成机器能跑的目标文件和二进制文件时,每个函数、每个静态变量都得有个全球唯一的身份证号,这个号就叫“符号”。为了防止你把两个不同 Rust 程序硬凑在一起链接时打起来,编译器会把原来的函数名搅成一团乱码,里面塞进模块路径、来自哪个包、泛型参数这些额外信息。以前这套搅和手法是照着 C++ 用的 Itanium ABI 标准抄的。
这套老方法有个特别坑的地方。泛型参数实例化之后,具体值到底是多少,它根本记不住,只在背后搞了个哈希值来区分。你写了一个 foo:: 和 foo::,按理说参数类型不一样,但在老方案里它们长得跟双胞胎似的,全靠那个哈希值来猜谁是谁。更离谱的是,Rust 当年并没有百分百照着 Itanium 的标准来,有些部分是自己瞎搞的,这就导致你想把符号还原成能看懂的 Rust 代码时,还得专门写一套 Rust 自己的解码器。
从 Rust 1.59 开始,编译器就给了你一个选择权,可以通过 -Csymbol-mangling-version=v0 这个编译参数来试用 Rust 自创的命名方案。到了 2025 年 11 月,这个方案在 nightly 版本里就默认开启了。现在 1.97.0 稳定版直接把它扶正了。如果你还念着旧方案的好,不好意思,只能在 nightly 版本里手动开启,而且官方已经放话出来,过段时间就把老代码彻底删干净。
光看技术细节你可能觉得这没啥,但你想过没有,一个连编译器自己内部标识符都搞不定的语言,怎么让开发者放心写大型项目?以前调试的时候,报错信息里蹦出来的符号名跟你代码里写的函数名差着十万八千里,你得对着哈希值和乱码猜半天才知道是哪个泛型实例出了事。现在这个 v0 方案直接把泛型参数的值明明白白保留在符号里,不用再绕弯子猜谜了。这就好比你家快递以前只写了个收件人手机号后四位,你得挨个试才知道是谁的包裹,现在直接写全名和门牌号。
Cargo 自己管警告了不再依赖环境变量
在持续集成(CI)流水线里把警告当错误处理,是大家早就习惯的操作。以前这事儿得靠 RUSTFLAGS=-Dwarnings 这个环境变量来硬干。你在命令行敲 cargo build 的时候,这个变量会被读进去,然后编译过程会把所有警告都升级成编译错误,只要有一个警告你的构建就失败。
但这事儿有个尴尬的地方。你改一下 RUSTFLAGS,哪怕只是从 -Dwarnings 改成不带参数,整个构建缓存就无效了,所有东西都得重新编译一遍。你改个编译器参数就得等好几分钟全量重编,这谁顶得住。而且这个环境变量是个全局开关,一开全开,一关全关,你想针对某个包或者某个场景单独设置警告策略,没门。
Rust 1.97.0 把这事儿收归 Cargo 自己管了。现在 Cargo 配置文件里直接控制警告怎么影响构建结果。你可以设置三个级别:allow 是闭嘴,啥警告也不说;warn 是默认状态,警告照常输出但不影响编译成功与否;deny 就是把警告当错误,编译直接失败。
最有意思的是,因为这是 Cargo 自己配置决定的,你改这个设置不会让构建缓存失效。想临时把警告关掉,静一静专心改代码,直接跑一句 CARGO_BUILD_WARNINGS=allow cargo check,警告就全没了,而且之前编译过的中间文件还能接着用,不用重新编译。等 CI 跑的时候,你把环境变量设成 deny,该报错就报错。更狠的是,你还能跟 --keep-going 参数配合使用,这样遇到第一个报错的包不会停下来,而是把所有包的错误和警告全收集齐了一起给你看。
这就好比你家以前只有一个总电闸,一拉全屋停电。现在每个房间都有独立的开关,你书房灯太刺眼了可以只关书房,厨房的灯还亮着继续做饭。而且你切换开关的时候,冰箱里冻的肉不会化掉重冻。
链接器的小话痨终于被放出来了
rustc 在背后替你调用链接器的时候,以前有个臭毛病。只要链接最终成功了,链接器在屏幕上叨叨的所有话都会被 rustc 默默吞掉。你只能看到编译通过,然后一脸懵。但链接器有时候会悄悄输出一些有用的信息,比如过时的优化选项、不推荐的用法提示,甚至是某些潜在问题的预警。这些信息虽然不至于让链接失败,但放着不管早晚要出事。
Rust 1.97.0 把这个遮羞布给扯了。现在链接器的输出会默认显示出来,而且是以警告(warning)级别的 lint 信息呈现的。你在终端会看到类似这样的东西:
``#[warn(linker_messages)]
warning: linker stderr: ignoring deprecated linker optimization setting '1'
|
= note: on by default`
当然,rustc 也不是傻到啥都往外倒。那些已经被证实是误报或者链接器本身刻意为之的常见消息,编译器会帮你过滤掉。你看到的至少是有点价值的信息。官方还说,自从 nightly 版本不再隐藏这些输出之后,已经靠这个修掉了好几个编译器本身的缺陷。你看,把问题露出来才能解决问题,藏着掖着只会让病根烂在肚子里。
需要注意一个坑:linker_messages 这个 lint 比较特殊,它不受 warnings 这个大组的控制。你如果像以前一样用 -Dwarnings 想顺便把链接器的警告也变成错误,那是没用的。因为编译器觉得链接器的输出它不能完全控制,而且不同平台上的表现差异太大,不能一刀切。所以这套机制是独立出来的。你如果觉得链接器输出的某个警告纯属吃饱了撑的想关掉,可以在 Cargo.toml 文件里加一个 [lints.rust] 段,里面写上 linker_messages = "allow",世界就清净了。
以前链接器憋着一肚子话不敢说,现在终于有个传声筒了。你写代码的时候,链接器在旁边小声逼逼你这个优化选项已经过时了,你赶紧记下来回头改掉。这比啥都不说让你以为一切都岁月静好要强得多。就像一个总爱把家里破事藏起来的家长突然开始唠叨了,虽然烦,但至少你知道哪儿要修了。
稳定版 API 里藏了几个能让你笑出来的小工具
这次稳定了一大串 API,大多都是给整数类型用的。而且这个命名风格有点意思。比如 isolate_highest_one 和 isolate_lowest_one,听名字就大概能猜到是干嘛的。给你一个二进制数 0b10101000,isolate_highest_one 会给你把最高位的那个 1 单独拎出来,变成 0b10000000。isolate_lowest_one 则会把最低位的 1 拎出来,变成 0b00001000。
highest_one 和 lowest_one 更直接,不拎出来了,直接告诉你最高位的 1 在第几位,或者最低位的 1 在第几位。bit_width 就是告诉你这个数用二进制表示占多少位。这些玩意儿放在一起,基本上就是在为位运算爱好者开 party。你以前可能要写好几行移位和与操作才能搞定的活儿,现在一行方法调用就完事了。
NonZero 版本也同步有了这些方法。非零整数本来就是 Rust 拿来优化内存用的,比如 Option 和 u32 占用的空间一样大,但能表达“没有值”的状态。现在这个优化过的类型也能享受位操作便利了。
还有 Default for RepeatN 这个实现,RepeatN 是个迭代器,就是重复生成同一个元素 N 次。现在它有了默认值,你创建一个空的默认迭代器啥也不干,随时准备着被人塞进元素和次数。ffi::FromBytesUntilNulError 也实现了 Copy,这玩意儿是处理 C 风格字符串时用的错误类型,以前不能随便复制,现在可以了。
最冷门但最提气的是 Send for std::fs::File on UEFI。UEFI 是现在电脑主板启动时跑的那个固件环境,以前在那个环境下的文件句柄不能在线程间安全传递,现在可以了。虽然 99.9% 的开发者这辈子都不会在 UEFI 环境里写多线程文件操作,但那个 0.1% 的人看到这个更新会直接原地起飞。这就是 Rust 的做事风格,角落里的小众需求也给你照顾到。
常量环境里也能判断字符是不是控制字符了
char::is_control 这个方法现在可以在 const 上下文里用了。啥意思呢?就是你可以在编译期判断一个字符是不是控制字符,然后基于这个结果去初始化静态变量或者做编译期计算。以前你只能在程序运行起来之后才能调用这个判断,现在编译阶段就能搞定。
这种变化对于大多数写业务代码的人来说没啥感觉,但对于搞嵌入式、做无分配器环境(no_std)或者搞编译期元编程的人来说,这就是福音。你在编译期就能把一堆字符里哪些是控制字符给筛出来,生成一个只包含可见字符的查找表,运行的时候直接查表,速度快得飞起。而且因为是在编译期算好的,运行时不占 CPU 时间。
编译器的进化有时候就是这样,你感觉不到它变了,但底层那些抠门到家的系统程序员已经感动哭了。就像你每天坐的公交车,你可能没注意它换了个更省油的发动机,但司机师傅每天少加一次油,笑得合不拢嘴。
其他零碎变化和感谢名单
除了上面这些大块头,Rust、Cargo 和 Clippy 各自还有一堆小修小补。官方的发布页面和各个子项目的更新日志里列了满满当当的条目。那些东西太碎,这里就不一一念了,反正你平时该遇到的问题,大概率已经在这些条目里有人帮你趟过一遍了。
每次发布版本都离不开一堆贡献者的努力。1.97.0 版本的功劳簿上又记了一长串名字。Rust 项目能一直保持每六周稳定发一个版本的节奏,全靠这些人在背后修 bug、写文档、审代码。而且他们的贡献名单是公开的,你随时可以去 thanks.rust-lang.org 上看。这种透明度在开源社区里算是标杆了。
一个工具链的迭代速度如果太快,你可能觉得不稳定;如果太慢,你又觉得它死了。Rust 这种六周一次的固定节奏,就像每周四晚上准时来你家收垃圾的环卫车,不管刮风下雨他都来,来了就把你家代码里的破烂带走,再给你留点新垃圾桶。你不需要每次都欢呼雀跃,但你知道这辆车从来没迟到过。
总结下来,Rust 1.97.0 在符号命名、警告控制和链接器输出这三个方向上,把以前膈应人的地方全给你抹平了。API 稳定和常量支持则是给特定领域的人塞了点糖。
所以别纠结了,赶紧 rustup update stable` 吧,反正你上次写的那段代码,编译器早就想跟你好好聊聊了。
另外:Rust 1.97.0 可能存在编译错误:https://github.com/rust-lang/rust/issues/159035