防止 Rust 标准库意外破坏

Lobsters Hottest 工具

摘要

Rust 标准库现在使用 `cargo-semver-checks` 来防止意外破坏,通过过往事件和集成该工具的协作努力详细说明。

<p><a href="https://lobste.rs/s/hnx6id/protecting_rust_standard_library_from">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/16 15:56

# 保护 Rust 标准库免受意外破坏 来源:https://predr.ag/blog/protecting-the-rust-stdlib-from-breakage/ 意外破坏可能发生在任何代码库中 (https://predr.ag/blog/semver-violations-are-common-better-tooling-is-the-answer/)。Rust 标准库也不能神奇地豁免于此——因此,它现在也使用 `cargo-semver-checks` (https://github.com/rust-lang/rust/pull/159671) 来防止意外破坏 (https://github.com/rust-lang/rust/pull/160253)。以下解释了为什么这需要多位 Rust 开发者耗时数月的工作,涉及数十个拉取请求,以及横跨 Rust 仓库、`cargo-semver-checks` 及其组件库的 15,000 多行代码。 2020 年 9 月,一个不稳定所需方法被添加到一个稳定的 `std` trait (https://github.com/rust-lang/rust/pull/76110)。这个看似无害的更改在 nightly 构建上破坏了 `async-std` (https://github.com/async-rs/async-std/issues/883) (https://github.com/rust-lang/rust/issues/77089),并被迅速回滚 (https://github.com/rust-lang/rust/pull/77090)。 2021 年 6 月,一个泛型方法被添加到 `core` 的 `BuildHasher` trait (https://github.com/rust-lang/rust/pull/86151)。该方法意外地没有 `where Self: Sized` 守护,导致 `BuildHasher` 不再是 `dyn` 安全的 (https://github.com/rust-lang/rust/issues/87991)。该问题在 Rust 1.55-beta 的 crater 运行期间被发现,并需要修复 (https://github.com/rust-lang/rust/pull/88031) 以避免破坏稳定版 Rust。 2022 年 7 月,针对 `ChunksMut` 等迭代器的健全性修复被合并到 `core` 中 (https://github.com/rust-lang/rust/pull/94247)。新的实现意外地不再实现 `Send` 和 `Sync` 自动 trait (https://github.com/rust-lang/rust/issues/100014),需要补丁 (https://github.com/rust-lang/rust/pull/100023) 以避免破坏稳定版 Rust。 2026 年 3 月,`tokio` 维护者发现他们的测试套件在 Windows 上的 Rust 1.94 中无法编译 (https://github.com/rust-lang/rust/issues/153486)。另一个 `std` trait 也增加了不稳定方法 (https://github.com/rust-lang/rust/pull/149718),这次破坏相当严重,以至于修复被包含在 Rust 1.94.1 点发布中 (https://blog.rust-lang.org/2026/03/26/1.94.1-release/)。 我可以继续列举下去。旁注:自 2020 年以来,我又发现了两个实例 (https://github.com/rust-lang/rust/issues/146087) (https://github.com/rust-lang/rust/issues/103306)。我的搜索并不详尽。很可能还有更多。 人类根本无法可靠地捕捉到意外破坏。我回顾了上述每一个引发破坏的 PR,我不相信我单靠自己能发现这个问题。最初参与这些 PR 的、经验远比我丰富的作者和评审们也没有发现!我们最努力的尝试仍不够,因此我们转向工具化。 **`cargo-semver-checks` 今天就能捕捉所有这些问题。我们选择不再让它们再次发生!** - 稳定性、破坏与稳定性破坏 (https://predr.ag/blog/protecting-the-rust-stdlib-from-breakage/#stability-breakage-and-stability-breakage) - 简单情况:项的稳定性 (https://predr.ag/blog/protecting-the-rust-stdlib-from-breakage/#the-straightforward-case-item-stability) - 部分稳定性使一切变得更难 (https://predr.ag/blog/protecting-the-rust-stdlib-from-breakage/#partial-stability-makes-everything-harder) - 将稳定性信息注入 `cargo-semver-checks` (https://predr.ag/blog/protecting-the-rust-stdlib-from-breakage/#plugging-stability-info-into-cargo-semver-checks) - 我们如何走到这里及前方道路 (https://predr.ag/blog/protecting-the-rust-stdlib-from-breakage/#how-we-got-here-and-what-lies-ahead) *感谢 Jakub Beránek (kobzol) (https://github.com/kobzol)、rustdoc 团队 (https://rust-lang.org/governance/teams/#team-rustdoc)、library (https://rust-lang.org/governance/teams/#team-libs) 和 library contributors (https://rust-lang.org/governance/teams/#team-libs-contributors) 团队、RustWeek 2026 和 Rust All Hands 组织者 (https://rustnl.org/about/),以及许多其他为实现这一目标贡献时间、精力和善意的 Rustaceans 🦀 `cargo-semver-checks` 是站在巨人的肩膀上。* ## 稳定性、破坏与稳定性破坏 Rust 标准库使用*稳定性*作为机制,以区分可在常规 Rust 发布版中使用的 API 和实验性、仅在 nightly Rust 上可选启用的 API。 顾名思义,不稳定 API 不提供稳定性或语义版本保证,可能随时更改。而稳定 API 的行为则完全类似于任何其他 Rust 库的公共 API。 首先,将 `cargo-semver-checks` 应用于标准库需要理解这两者的区别,以免通过让 CI 对显式不稳定 API 的破坏发出抱怨而挫伤维护者的积极性。旁注:当然,*有意的*不稳定 API 破坏与*无意的*此类 API 破坏是有区别的。我们*尚未*构建对此的支持,因此这里还有更深度集成的空间!但通常,不稳定 API 的破坏应报告为“这是变更内容,请确保这是您预期的”,而不应阻止 CI。 还有另一类破坏:*去稳定化*(de-stabilizing)一个先前稳定的 API。遗憾的是,这也不仅仅是一个假设的情况:这种破坏类型已有先例 (https://github.com/rust-lang/rust/issues/103306)。我们也想捕捉这一点——而且我们做到了。 最后,项的*名称和存在*可能是稳定的,但它们的某些方面,如 `const` 或默认值,可能不稳定。`cargo-semver-checks` 也必须对此进行建模。 我们需要解决两组挑战:在 rustdoc JSON 中暴露稳定性信息以便 `cargo-semver-checks` 可以读取它,以及使稳定性适应 `cargo-semver-checks` 的 lint 数据模型,而无需重写成百上千的 lint 规则。 让我们先讨论稳定性和 rustdoc JSON。 ### 简单情况:项的稳定性 看看这个示例: ```rust #[stable(feature = "example", since = "1.0.0")] pub struct Example { #[stable(feature = "example", since = "1.0.0")] pub stable_field: u32, #[unstable(feature = "example_unstable_field", issue = "none")] pub unstable_field: u32, } ``` 如你所见,`stable_field` 可在稳定版 Rust 上使用,而 `unstable_field` 需要 nightly Rust 并通过 `#![feature(example_unstable_field)]` 显式启用。旁注:因此,稳定代码不能仅通过字段表达式创建新的 `Example`,必须在模式中使用 `..`,即使该结构体没有正式标记为 `#[non_exhaustive]`。使用现有 `Example` 的函数式更新语法,如 `Example { stable_field, ..existing }`,仍然有效。 移除 `stable_field` 或将其标记为 `#[unstable]` 都会破坏稳定版 Rust API,我们需要捕捉这些情况。仅影响 `unstable_field` 的更改是允许的,前提是它们不改变包含类型的稳定属性——例如,通过移除一个稳定的自动 trait 实现。 为了在 rustdoc JSON 中导出此数据,此 PR 添加了一个 `Item::stability` 字段 (https://github.com/rust-lang/rust/pull/158230/changes#diff-ede26372490522288745c5b3df2b6b2a1cc913dcd09b29af3a49935afe00c7e6R301-R320),可用于填充标准库项的 `#[stable]` 或 `#[unstable]` 属性数据。 ### 部分稳定性使一切变得更难 项的稳定性并非全部。一个项的名称和存在可能是稳定的,但仅其*部分*能力是稳定的。 以 `const` 函数为例: ```rust #[stable(feature = "example", since = "1.0.0")] #[rustc_const_unstable(feature = "example_const", issue = "none")] pub const fn answer() -> u32 { 42 } ``` 在 `const` 上下文之外,`answer()` 可在稳定版 Rust 上正常调用。但在 `const` 中调用它是不稳定的,需要 nightly Rust 并显式启用 `#![feature(example_const)]`。 因此,移除此函数的 `const` 从稳定版 Rust 的角度看并不是破坏性变更。要破坏稳定版 Rust,它原本必须是 `#[rustc_const_stable]`。旁注:我们还必须考虑 `const trait` 声明、`const` trait 实现以及它们关联方法的 const 行为——截至 Rust 1.97.1,这些目前都是不稳定的。发现并妥善处理此类案例所需的深入研究,是成功完成此任务的挑战之一。 类似于项的稳定性,我们在 rustdoc JSON 中添加了一个 `Item::const_stability` 字段 (https://github.com/rust-lang/rust/pull/158343/changes#diff-ede26372490522288745c5b3df2b6b2a1cc913dcd09b29af3a49935afe00c7e6R324-R332)。 具有提供默认实现的 trait 项 [旁注:这包括函数、关联常量和关联类型。再次说明,关联类型默认值本身是 Rust 的不稳定特性,使得发现这个边界情况本身也是此处的挑战之一。] 有一个类似的概念——*默认稳定性*: ```rust #[stable(feature = "example", since = "1.0.0")] pub trait Example { #[stable(feature = "example", since = "1.0.0")] #[rustc_default_body_unstable( feature = "example_default", issue = "none" )] fn answer_in_trait(&self) -> u32 { 42 } } ``` `answer_in_trait()` 方法是稳定的,但其默认实现不是。稳定版 Rust 中的 `Example` 实现必须提供它们自己的 `answer_in_trait()` 方法,而启用了 `#![feature(example_default)]` 的 nightly Rust 用户可以依赖该默认实现。 由于稳定的下游 trait 实现不能依赖不稳定的默认实现,因此移除它不会破坏稳定版 Rust 的源代码兼容性。 为了向 rustdoc JSON 暴露默认稳定性,我们的 PR 在 `Function`、`ItemEnum::AssocConst` 和 `ItemEnum::AssocType` 中添加了 (https://github.com/rust-lang/rust/pull/158468/changes#diff-ede26372490522288745c5b3df2b6b2a1cc913dcd09b29af3a49935afe00c7e6) `default_unstable` 字段。 ## 将稳定性信息注入 `cargo-semver-checks` 在 rustdoc JSON 中提供稳定性信息只是故事的一半。我们在检测破坏时如何利用它? 完全重写(更糟的是,复制)每个 lint 规则是不可行的。几年来的指数级增长已经产生了数百条 lint,而且我们仍在添加更多! 要找到答案,请比较以下两种情况: ```rust // 在 crates.io 上的一个普通 crate 中: pub struct UserExample { pub visible: u32, #[doc(hidden)] pub unstable: u32, } // 在 Rust 标准库中: #[stable(feature = "example", since = "1.0.0")] pub struct StdlibExample { #[stable(feature = "example", since = "1.0.0")] pub visible: u32, #[unstable(feature = "example_unstable_field", issue = "none")] pub unstable: u32, } ``` `UserExample::unstable` 与 `StdlibExample::unstable` 有何不同?`UserExample` 与 `StdlibExample` 有何不同? 两个 `unstable` 字段都选择不成为稳定的公共 API。 两个结构体*技术上*都有全公共字段。但这两个结构体的*公共 API* 都不支持用 `Example { visible, unstable }` 结构体字面量语法进行初始化,因为这需要命名 `unstable` 字段,而该字段不在语义版本保证的公共 API 之内。 在非主要版本中破坏不稳定 API 是允许的。在非主要版本中破坏 `#[doc(hidden)]` API 也是允许的。 **稳定性属性是另一种公共 API 标记!**我们已经投入了大量精力 (https://predr.ag/blog/checking-semver-for-doc-hidden-items/) 来处理 `#[doc(hidden)]`。我们几乎可以重用那里的所有基础设施来完成此任务 🎉 - 标记为 `#[unstable]` 的项被视为非公共 API,就像它们是 `#[doc(hidden)]` 一样。 - 如果一个项是 const-不稳定的,`cargo-semver-checks` 将其视为非 const。 - 如果一个提供的默认实现是不稳定的,`cargo-semver-checks` 会假定该默认实现未提供。 在结构上,这实现了我们想要的一切:稳定项的破坏被正确报告,去稳定化被视为从公共 API 中移除,而不稳定项自身的破坏永远不会被报告。旁注:一点小的 UX 优化即将到来:[去稳定化将被具体报告为添加了 `#[doc(hidden)]`](https://github.com/obi1kenobi/cargo-semver-checks/issues/1672),即使现在有几种其他属性可能导致这种情况。我们也会修复这个问题! 但这是我最喜欢的部分:这些 lint 规则*完全对此一无所知*。 `cargo-semver-checks` 之所以出现如此寒武纪大爆发般的 lint 规则 (https://predr.ag/blog/cargo-semver-checks-2025-year-in-review/),是因为编写 lint 仍然*相对容易*——通常如此,尤其是与静态分析工具中现有的先例相比。编写新的 lint 规则甚至是推荐给新贡献者的入门任务 (https://github.com/obi1kenobi/cargo-semver-checks/blob/c47d6c3e5fee2e63fff191f741352e2570f2be0e/CONTRIBUTING.md#L9)! 采用这种处理稳定性信息的方法,这种情况得以延续:新编写的 lint 规则将*直接生效*。在正确处理 `#[doc(hidden)]` 的过程中,它们的稳定性处理也会通过构造保持正确。它们落入了成功的陷阱 (https://blog.codinghorror.com/falling-into-the-pit-of-success/)。 ## 我们如何走到这里及前方道路 今年年初,我写道我选择拒绝“lint 规则数量”作为基准 (https://predr.ag/blog/cargo-semver-checks-2025-year-in-review/#the-path-forward-for-2026-and-beyond),转而寻找最大化我们对 Rust 生态系统积极影响的方法。 这是此类工作的一个绝佳范例。 机会出现在 RustWeek 2026 和 All Hands 会议 (https://2026.rustweek.org/) 期间与从事 Rust 标准库工作的人们的一系列偶然对话中。从一次走廊闲聊中的随口评论开始,在连续几天充满 Rust 主题的演讲、会议、晚餐和活动巴士行程中,迅速发展成一个想法的草图,然后变成一个具体的提议。 顺便说一句,这就是在 RustWeek 这样的大型会议之后立即举办 All Hands 是个绝妙主意的原因。它最大化了*此类幸运巧合*发生的概率——并获得足够的动力来克服一个大胆新想法诞生时常出现的所有“它不会成功”的理由。许多其他想法也从中受益 (https://blog.rust-lang.org/inside-rust/2026/07/31/all-hands-2026-retrospective/)!我对 RustWeek 和 All Hands 组织者出色的工作表示敬意! 接下来就是实现那些在那些面对面交谈中(大致)达成共识的内容。这花了一些时间,而且工作尚未完全完成——我们只是达到了在 Rust CI 中采用 `cargo-semver-checks` 明显优于之前状态的程度。仍有一些问题需要解决,我们将继续努力。旁注:例如:关于稳定性破坏的更好的用户体验 (https://github.com/obi1kenobi/cargo-semver-checks/issues/1672)、在更多平台(而不仅仅是 x86 Linux)上捕捉破坏、以及 glob 导入与稳定性和 `#[doc(hidden)]` 交互方式的边界情况 (https://rust-lang.zulipchat.com/#narrow/channel/266220-t-rustdoc/topic/Can.20non-public.20API.20glob.20re-export.20produce.20public.20API.20items.3F/with/616735517) 等。 尽管还有更多工作要做,我们仍有许多值得庆祝的地方! 我们减少了 Rust 开发者可能必须承受、报告、分类和修复的意外破坏量。 从现在起,`cargo-semver-checks` 的每一项改进都将不仅直接惠及 crates.io 库生态系统,也将惠及 Rust 本身。 RustWeek 和 All Hands 的积极影响仍在持续……

相似文章

使用 Cackle 提高 Rust 供应链攻击难度(2023)

Lobsters Hottest

David Lattimore 介绍了 Cackle,这是一个通过使用访问控制列表(ACL)限制依赖项行为来帮助防止 Rust 供应链攻击的工具,从而降低通过第三方 crate 引入恶意代码的风险。

Rust 1.97.0 发布公告

Lobsters Hottest

Rust 1.97.0 已发布,默认启用符号修饰 v0,Cargo 支持禁止警告,并且链接器输出不再隐藏。

Rust 中的进行中工作

Lobsters Hottest

本文介绍了一种在 Rust 开发过程中延迟错误处理的技术和库,允许开发者临时将错误降级为警告,从而在保持正确性的同时维持生产力。