编程 Rust 1.96 深度实战:Range 终于可 Copy、Cargo 双源依赖、Wasm 严格链接——从设计哲学到生产级迁移的完全指南(2026)

2026-05-31 04:13:47 +0800 CST views 6

Rust 1.96 深度实战:Range 终于可 Copy、Cargo 双源依赖、Wasm 严格链接——从设计哲学到生产级迁移的完全指南(2026)

2026 年 5 月 28 日,Rust 1.96.0 正式发布。这个版本没有惊天动地的新语法,却在几个基础组件深处修复了积年已久的 API 设计瑕疵,同时给出了清晰、渐进的迁移路径。作为 Rust 开发者,理解这些变化的"为什么"和"怎么用",远比单纯浏览更新列表更有价值。本文将从设计哲学出发,深入每一个特性的底层原理,配合大量实战代码,帮你真正掌握这次更新。

一、背景:为什么 Rust 需要一次"长尾改进"

Rust 的版本发布节奏是每 6 周一个稳定版。从 1.0 到 1.96,已经走过了近 12 年。在这个过程中,标准库中的某些类型设计逐渐暴露出了"历史债务"——当时的设计决策在后来的实践中被证明不够理想,但因为向后兼容性承诺,无法直接修改。

Rust 团队选择的方式是:引入新类型,提供迁移路径,在未来 Edition 中完成切换。1.96 就是这种"渐进式修复"哲学的集中体现。

这次更新的三大核心特性:

  1. core::range 新类型:让范围类型终于支持 Copy
  2. Cargo 双源依赖:一个依赖,两种来源,按场景自动切换
  3. Wasm 链接规则严格化:从"宽容默认"到"严格默认"

再加上 assert_matches! 宏的稳定、多个实用 API 的稳定化,以及两个 Cargo 安全漏洞修复。看起来不炸裂,但每一个都直击日常开发痛点。


二、core::range:让范围类型真正"值"起来

2.1 旧世界的尴尬:Range 的"双重身份"困境

这是 Rust 标准库中最让人困惑的设计之一。我们每天都在写 0..10,但你知道它生成的 std::ops::Range 有多别扭吗?

let r = 1..10; // Range<usize>
let first = r.next(); // 作为 Iterator 使用
let r2 = r; // ❌ 编译错误:r 已被移动

为什么?因为 std::ops::Range 直接实现了 Iterator。在 Rust 的设计规范中,迭代器是独占的、一次性消费的资源——调用 next() 会消耗自身。因此,实现了 Iterator 的类型不能实现 Copy

这导致了两个极其常见的痛点:

痛点一:无法放进 Copy 容器

当你需要一个可拷贝的切片索引器时,不得不把 start 和 end 拆开存储:

// 旧版本:手动拆字段,丢失 Range 语义
#[derive(Clone, Copy)]
struct Span {
    start: usize,
    end: usize,
}

impl Span {
    fn of(self, s: &str) -> &str {
        &s[self.start..self.end] // 又拼回了 Range...
    }
}

这种手工作法丢失了 Range 自带的方法和语义,本质上是"拆了再拼"。

痛点二:RangeInclusive 的字段私有化

为了保证"已迭代完成"这一状态的正确性,旧版 RangeInclusive 的字段是私有的:

let range = 1..=10; // RangeInclusive<usize>
// range.start  // ❌ 字段是私有的!
// range.end    // ❌ 同样无法访问

你无法直接构造或解构它,只能通过 ..= 语法。这意味着你无法在运行时动态构建一个包含边界的范围——除非用 new() 方法,但那个方法的存在本身就说明了设计的别扭。

痛点三:泛型约束的尴尬

如果你想写一个接受任意范围的函数,你不得不使用 RangeBounds trait:

use std::ops::RangeBounds;

fn process<R: RangeBounds<usize>>(range: R) {
    // ...
}

process(1..10);   // OK
process(1..=10);  // OK
process(..10);    // OK

这本身没问题,但如果你想存储这个范围呢?RangeBounds 是一个 trait,不是具体类型。你无法定义 struct Config { range: impl RangeBounds<usize> }——impl trait 不能用在 struct 字段上。

2.2 新设计的核心思想:分离迭代能力

RFC 3550 引入了一套全新的范围类型,位于 core::range 模块:

新类型对应旧类型关键变化
core::range::Rangestd::ops::Range不再实现 Iterator,实现 IntoIterator
core::range::RangeFromstd::ops::RangeFrom同上
core::range::RangeInclusivestd::ops::RangeInclusive字段公开,不再实现 Iterator
(未来加入)std::ops::RangeFull-
(未来加入)std::ops::RangeTo-

关键改变就一句话:这些类型不再实现 Iterator,而是实现 IntoIterator

这意味着类型本身可以作为纯数据自由拷贝,只有当你显式调用 .into_iter() 时,才会转移所有权并开始迭代。

用代码对比再清楚不过:

use core::range::Range;

// ✅ 新 Range 实现了 Copy
let range: Range<usize> = Range { start: 1, end: 10 };
let copy = range; // 普通拷贝,不消耗
assert_eq!(copy.start, 1); // 可以继续访问字段

// 要迭代,必须显式转换
let iter = range.into_iter(); // 显式消费
for i in iter {
    println!("{}", i);
}

// 对比旧 Range(来自 std::ops)
let old_range = 1..10; // std::ops::Range<usize>
let _old_iter = old_range.into_iter(); // 旧 Range 本身就是 Iterator
// 此时 old_range 已被移动,不能再使用

2.3 深入理解:为什么 IntoIterator 而不是 Iterator?

这个设计选择的背后是 Rust 核心团队对"所有权语义"的深层思考。

Iterator trait 的 next() 方法签名是 fn next(&mut self) -> Option<Self::Item>——它通过 &mut self 消费迭代器。这意味着每次调用 next() 都在改变迭代器的内部状态。

IntoIteratorinto_iter(self) 方法签名是 fn into_iter(self) -> Self::IntoIter——它通过值消费自身,返回一个新的迭代器类型。关键区别在于:类型本身只是一个"区间描述",迭代器才是"消费状态"

这就实现了关注点分离:

  • core::range::Range = 纯数据,描述一个区间 [start, end)
  • core::range::Iter(由 into_iter() 返回)= 迭代状态,负责逐个产出值

2.4 实战一:让 Span 既 Copy 又体面

有了 core::range::Range,开头那个 Span 的例子终于可以优雅起来了:

use core::range::Range;

#[derive(Clone, Copy, Debug)]
pub struct Span(Range<usize>);

impl Span {
    pub fn new(start: usize, end: usize) -> Self {
        Span(Range { start, end })
    }

    pub fn of(self, s: &str) -> &str {
        &s[self.0]  // 直接使用新 Range 作为切片索引
    }

    pub fn len(self) -> usize {
        self.0.end.saturating_sub(self.0.start)
    }

    pub fn is_empty(self) -> bool {
        self.0.start >= self.0.end
    }
}

fn main() {
    let span = Span::new(0, 5);
    let text = "Hello, Rust 1.96!";
    let slice = span.of(text);
    assert_eq!(slice, "Hello");

    // Span 是 Copy 的,可以多次使用
    let span2 = span;
    assert_eq!(span2.of(text), "Hello");
    assert_eq!(span.len(), 5);
}

2.5 实战二:编译器诊断中的 Range 表示

在编译器、代码分析工具中,我们经常需要存储源码位置。旧的 Range 不能 Copy,导致大量冗余代码:

// 旧方案:手动拆字段
#[derive(Clone, Copy)]
struct SourceSpan {
    file_id: usize,
    start: usize,
    end: usize,
}

// 新方案:直接用 Range
use core::range::Range;

#[derive(Clone, Copy)]
struct SourceSpan {
    file_id: usize,
    range: Range<usize>,
}

impl SourceSpan {
    fn contains(self, offset: usize) -> bool {
        self.range.start <= offset && offset < self.range.end
    }

    fn merge(self, other: Self) -> Self {
        Self {
            file_id: self.file_id,
            range: Range {
                start: self.range.start.min(other.range.start),
                end: self.range.end.max(other.range.end),
            },
        }
    }
}

注意 merge 方法——如果用旧方案,你需要写 self.start.min(other.start)self.end.max(other.end),Range 的语义被拆散了。新方案保持了 Range 的整体性,代码意图更加清晰。

2.6 实战三:RangeInclusive 字段公开的威力

旧版 RangeInclusive 的字段是私有的,这导致了一个匪夷所思的问题:你无法在 const 上下文中构造一个包含边界的范围。1.96 修复了这个问题:

use core::range::RangeInclusive;

// ✅ 字段公开,可以自由构造
const FULL_BYTE_RANGE: RangeInclusive<u8> = RangeInclusive { start: 0, end: 255 };

// ✅ 也可以自由解构
let RangeInclusive { start, end } = FULL_BYTE_RANGE;
assert_eq!(start, 0);
assert_eq!(end, 255);

// 实际应用:IP 地址范围校验
fn is_private_ip(octets: [u8; 4]) -> bool {
    // 10.0.0.0 - 10.255.255.255
    if octets[0] == 10 {
        return true;
    }
    // 172.16.0.0 - 172.31.255.255
    if octets[0] == 172 {
        let second_range = RangeInclusive { start: 16u8, end: 31 };
        return second_range.contains(&octets[1]);
    }
    // 192.168.0.0 - 192.168.255.255
    if octets[0] == 192 && octets[1] == 168 {
        return true;
    }
    false
}

2.7 迁移策略:库作者现在该怎么做?

新旧范围类型将在未来一个 Edition 中完成切换(.. 语法届时会生成 core::range 类型)。在此之前,你的公开 API 应该遵循兼容之道:

// ✅ 推荐:使用 trait bound 接受所有范围类型
pub fn process_range(range: impl std::ops::RangeBounds<usize>) {
    // ...
}

// ✅ 如果需要存储范围,可以开始使用新类型,同时提供旧类型的转换
pub fn normalize_range(range: impl std::ops::RangeBounds<usize>) -> core::range::Range<usize> {
    use std::ops::Bound;
    let start = match range.start_bound() {
        Bound::Included(&s) => s,
        Bound::Excluded(&s) => s + 1,
        Bound::Unbounded => 0,
    };
    let end = match range.end_bound() {
        Bound::Included(&e) => e + 1,
        Bound::Excluded(&e) => e,
        Bound::Unbounded => usize::MAX,
    };
    core::range::Range { start, end }
}

// ✅ 暴露两种构造方式
pub struct Query {
    range: core::range::Range<usize>,
}

impl Query {
    // 接受新类型
    pub fn from_range(range: core::range::Range<usize>) -> Self {
        Self { range }
    }

    // 兼容旧 Range 语法
    pub fn from_std_range(range: std::ops::Range<usize>) -> Self {
        Self {
            range: core::range::Range {
                start: range.start,
                end: range.end,
            },
        }
    }
}

2.8 性能分析:Copy vs Clone 的零成本抽象

有人可能会问:新 Range 的 Copy 语义是否有运行时开销?

答案是:没有Copy trait 的语义是按位复制,对于 Range<usize>(本质就是两个 usize),编译器生成的代码和手动拷贝两个字段完全一致。

// 旧方案
let span = Span { start: 1, end: 10 };
let span2 = span; // Clone,需要显式调用

// 新方案
let span = Span(core::range::Range { start: 1, end: 10 });
let span2 = span; // Copy,自动按位复制

在优化后的汇编层面,两者生成的指令完全相同——都是两次 movCopy vs Clone 的区别只在类型系统层面,不影响运行时性能。


三、assert_matches!:断言失败时,让错误开口说话

3.1 assert!(matches!(...)) 的致命缺陷

测试中我们经常用 matches! 宏检查模式:

fn get_status() -> Result<u32, String> {
    Ok(42)
}

#[test]
fn test_status() {
    let result = get_status();
    // 旧写法:matches! 只返回 bool,assert! 不知道实际值是什么
    assert!(matches!(result, Ok(_)), "Expected Ok, got {:?}", result);
}

问题在于:

  1. 你必须手动写格式化字符串 "Expected Ok, got {:?}",否则断言失败时只会显示 assertion failed
  2. matches! 不捕获绑定变量,你无法在格式化字符串中引用匹配到的值
  3. 每次都要写 assert!(matches!(...)) 这个冗长的组合

3.2 assert_matches! 的智能之处

1.96.0 新增的 assert_matches! 宏解决了这个痛点:失败时自动以 Debug 格式打印被检查的值

use core::assert_matches;

fn get_number() -> u32 { 42 }

fn main() {
    assert_matches!(get_number(), 1..=6);
}

输出会变成:

thread 'main' panicked at 'assertion failed: `(left matches right)`
  left: `42`,
 right: `1..=6`', src/main.rs:5:5

left 直接给出了实际值 42,right 显示了期望的模式。这种"所见即所得"的诊断,在测试失败时能帮你节省大量时间。

3.3 深入用法:枚举模式匹配

这是 assert_matches! 最实用的场景——检查枚举变体:

use core::assert_matches;

#[derive(Debug)]
enum ApiResponse {
    Success { data: Vec<String> },
    Error { code: u16, message: String },
    Timeout,
}

fn parse_response(raw: &str) -> ApiResponse {
    if raw.is_empty() {
        ApiResponse::Timeout
    } else if raw.starts_with("ERR") {
        ApiResponse::Error {
            code: 500,
            message: raw.to_string(),
        }
    } else {
        ApiResponse::Success {
            data: vec![raw.to_string()],
        }
    }
}

#[test]
fn test_success_response() {
    let resp = parse_response("hello");
    assert_matches!(resp, ApiResponse::Success { .. });
}

#[test]
fn test_error_code() {
    let resp = parse_response("ERR: timeout");
    // 使用守卫条件进一步检查
    assert_matches!(
        resp,
        ApiResponse::Error { code, .. } if code >= 400 && code < 500
    );
}

#[test]
fn test_not_timeout() {
    let resp = parse_response("hello");
    // 否定检查:确保不是 Timeout
    assert_matches!(resp, ApiResponse::Success { .. } | ApiResponse::Error { .. });
}

3.4 实战:构建类型状态机的断言套件

在类型状态机(Typestate Pattern)中,assert_matches! 可以用来验证状态转换的正确性:

use core::assert_matches;

#[derive(Debug, Clone)]
enum ConnectionState {
    Disconnected,
    Connecting { endpoint: String },
    Connected { latency_ms: u32 },
    Error { retries: u32, last_error: String },
}

struct Connection {
    state: ConnectionState,
}

impl Connection {
    fn new() -> Self {
        Self { state: ConnectionState::Disconnected }
    }

    fn connect(&mut self, endpoint: &str) {
        self.state = ConnectionState::Connecting {
            endpoint: endpoint.to_string(),
        };
    }

    fn on_connected(&mut self, latency_ms: u32) {
        self.state = ConnectionState::Connected { latency_ms };
    }

    fn on_error(&mut self, error: &str) {
        let retries = match &self.state {
            ConnectionState::Error { retries, .. } => retries + 1,
            _ => 1,
        };
        self.state = ConnectionState::Error {
            retries,
            last_error: error.to_string(),
        };
    }
}

#[test]
fn test_connection_lifecycle() {
    let mut conn = Connection::new();
    assert_matches!(conn.state, ConnectionState::Disconnected);

    conn.connect("api.example.com:443");
    assert_matches!(conn.state, ConnectionState::Connecting { .. });

    conn.on_connected(23);
    assert_matches!(conn.state, ConnectionState::Connected { latency_ms: 23 });

    conn.on_error("ECONNRESET");
    assert_matches!(conn.state, ConnectionState::Error { retries: 1, .. });
}

3.5 debug_assert_matches!:Release 构建零开销

debug_assert! 系列一致,debug_assert_matches! 在 release 构建中会被完全移除:

use core::debug_assert_matches;

fn process(data: &[u8]) {
    // 只在 debug 构建中检查,release 中零开销
    debug_assert_matches!(data.len(), 0..=1024);

    // 实际处理逻辑...
}

这在性能敏感的路径上特别有用——你既想确保开发阶段的正确性,又不想在发布版本中留下任何断言开销。

3.6 与第三方 crate 的兼容性注意

assert_matches! 不在 prelude 中(避免与第三方 crate 的同名宏冲突),使用时需要显式引入:

// 标准库路径
use std::assert_matches;  // 或 use core::assert_matches;

// 如果你之前用了第三方 crate(如 static_assertions)
// 不会产生冲突,因为路径不同
use static_assertions::assert_eq_size;  // 不冲突
use core::assert_matches;              // 标准库

四、Cargo 双源依赖:终结开发与发布的依赖割裂

4.1 痛点场景

这是本次更新中最实用的特性之一。先看一个真实场景:

你的团队维护一个内部公共工具库 my-utils。日常开发时,你需要依赖 Git 仓库的最新代码(修复 bug、新增特性),所以 Cargo.toml 这样写:

[dependencies]
my-utils = { git = "https://github.com/company/my-utils.git", branch = "main" }

但项目正式打包、发布、CI 构建时,必须使用私有 registry 或 crates.io 上的稳定版本:

[dependencies]
my-utils = { version = "1.2.0", registry = "my-private-registry" }

问题来了:一个依赖只能指定一种来源。

过去的常见做法:

  1. 手动切换:本地开发时改 Git 源,发布前改回 registry 版本。完全依赖人工操作,容易出错。
  2. Git patch:用 [patch] 段临时覆盖。但 [patch] 只能覆盖版本号依赖,不能覆盖 Git 依赖,且需要维护额外的配置。
  3. Cargo workspace hack:在 workspace 根目录维护多个 Cargo.toml 变体。复杂且脆弱。
  4. CI 脚本:在 CI 中用 sed 动态替换。不可靠,且难以本地调试。

4.2 双源依赖的规则

Rust 1.96 终于支持在同一个依赖声明中同时指定两种来源:

[dependencies]
my-utils = { git = "https://github.com/company/my-utils.git", registry = "my-private-registry" }

规则清晰:

  • 本地开发、日常构建:优先使用 Git 源(始终获取最新代码)
  • 项目发布、cargo publish 构建、正式打包:自动降级使用 registry 注册表稳定版本

4.3 深入理解:双源的工作机制

双源依赖的核心逻辑在 Cargo 的"发布感知"构建中:

cargo build          → 使用 Git 源(本地开发)
cargo build --release → 使用 Git 源(仍是本地构建)
cargo publish        → 使用 registry 版本(发布构建)

关键区别在于 cargo publish 命令。当执行 cargo publish 时,Cargo 会检查所有依赖的来源:

  1. 如果依赖只声明了 git,Cargo 会报错——不能发布依赖 Git 源的 crate
  2. 如果依赖同时声明了 gitregistry/version,Cargo 在发布时会自动忽略 git,只使用 registry/version
  3. 如果依赖只声明了 version/registry,Cargo 正常使用

4.4 实战配置:多种场景

场景一:Git + 私有 Registry

[dependencies]
# 日常开发用 Git 最新代码,发布时用私有 registry 稳定版本
auth-lib = { git = "https://git.company.com/auth-lib.git", registry = "company-registry" }

场景二:Git + crates.io

[dependencies]
# 日常开发用 Git main 分支,发布时用 crates.io 版本
serde_json = { git = "https://github.com/serde-rs/json.git", version = "1.0" }

场景三:Git + version 双指定(最常见)

[dependencies]
# Git 源码的 version 字段仅用于发布时确定 registry 依赖
my-core = { git = "https://github.com/company/core.git", branch = "develop", version = "2.1" }

场景四:完整 Cargo.toml 示例

[package]
name = "my-service"
version = "0.5.0"
edition = "2021"
publish = ["company-registry"]

[dependencies]
# 公共依赖:只用 crates.io
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }

# 内部依赖:双源
my-proto = { git = "https://git.company.com/proto.git", version = "0.3" }
my-auth = { git = "https://git.company.com/auth.git", registry = "company-registry" }
my-logger = { git = "https://git.company.com/logger.git", branch = "feat/structured-log", version = "1.0" }

[registries.company-registry]
index = "https://cargo.company.com/index"

4.5 团队工作流最佳实践

双源依赖解决了技术问题,但团队协作还需要一些流程配合:

Git 依赖的分支策略

# ❌ 不推荐:依赖 main 分支,代码可能随时变动
my-lib = { git = "https://github.com/company/my-lib.git", version = "1.0" }

# ✅ 推荐:依赖特定分支或 tag
my-lib = { git = "https://github.com/company/my-lib.git", branch = "release/1.0", version = "1.0" }

# ✅ 更好:依赖 Git tag
my-lib = { git = "https://github.com/company/my-lib.git", tag = "v1.0.3", version = "1.0" }

版本号对齐检查

Git 源码的 Cargo.toml 中的版本号应该与双源声明中的 version 字段保持兼容。建议在 CI 中加入检查:

#!/bin/bash
# check_dual_source_versions.sh
grep -E '^\w.*=.*\{.*git.*version' Cargo.toml | while read line; do
    dep_name=$(echo "$line" | sed 's/^\([^ =]*\).*/\1/')
    declared_version=$(echo "$line" | grep -oP 'version\s*=\s*"([^"]*)"' | head -1 | grep -oP '"[^"]*"')
    echo "Dependency: $dep_name, Declared version: $declared_version"
done

4.6 常见陷阱与排障

陷阱一:双源的 version 不是 Git 源的版本

# ⚠️ version = "1.0" 指的是 registry 上的版本,不是 Git 仓库中的版本
my-lib = { git = "https://github.com/company/my-lib.git", version = "1.0" }

version 字段仅在发布时生效,用于从 registry 拉取对应版本。本地开发时,Cargo 完全忽略 version,只使用 git

陷阱二:Git 依赖的 lock 文件行为

双源依赖中,Cargo.lock 记录的是 Git 源的具体 commit hash。当 Git 仓库有新提交时,需要手动 cargo update 来更新:

# 更新特定依赖到 Git 仓库最新提交
cargo update my-lib

# 或更新所有依赖
cargo update

陷阱三:私有 registry 认证配置

如果双源声明中使用了私有 registry,确保 .cargo/config.toml 中正确配置了认证:

# .cargo/config.toml
[registry]
global-credential = "cargo.company.com"

[net]
git-fetch-with-cli = true  # 使用系统 Git,支持 SSH 认证

五、WebAssembly 链接规则严格化:从"宽容"到"严格"

5.1 变更内容

升级到 1.96 后,为 Wasm 目标编译时,链接器不再默认传递 --allow-undefined。这意味着任何未定义的链接符号将直接导致链接错误,而不再是默默地变成从 env 模块导入的 stub。

5.2 旧行为为什么危险

旧链接器会"好心"地将未定义符号变成来自 env 模块的导入。典型场景:

#[link(wasm_import_module = "my_host")]
extern "C" {
    fn host_func();
}

fn main() {
    unsafe { host_func(); }
}

如果你写错了函数名(比如 host_func 实际是 host_function),旧链接器不会报错,而是:

  1. 忽略 #[link(wasm_import_module = "my_host")]
  2. host_func 变成一个来自 env 模块的未定义导入
  3. 你的 Wasm 模块在运行时可能静默失败或表现出怪异行为

这种"宽容"掩盖了配置错误,让你以为链接成功,实际上运行时会出各种诡异问题。更糟糕的是,由于 Wasm 的运行时错误通常不如原生平台那么直观,排查这种问题可能耗费数小时。

5.3 新行为:严格链接

1.96 的行为变更非常直接:未定义符号 → 链接报错,不再默默创建 stub。

error: ld.lld: error: undefined symbol: host_func
  >>> referenced by main
  >>>       /path/to/main.o:(host_func)

这是一个典型的"提前失败"(fail fast)设计——在构建阶段暴露问题,而不是让问题溜到运行时。

5.4 如何恢复旧行为

如果你的项目确实需要允许未定义符号(比如动态加载场景),有两种方法恢复旧行为:

方法一:环境变量(全局)

RUSTFLAGS="-Clink-arg=--allow-undefined" cargo build --target wasm32-unknown-unknown

方法二:源码级显式注解(推荐)

在声明外部块的 extern 上添加 link(wasm_import_module = "env"),明确表达你的意图:

#[link(wasm_import_module = "env")] // 显式指出导入自 env 模块
extern "C" {
    fn some_dynamic_import();
}

方法二更好,因为它是源码级的,不会影响项目中其他 Wasm 外部函数的严格检查。

5.5 实战:Wasm 项目迁移检查清单

// ❌ 升级后会链接错误的代码
#[link(wasm_import_module = "my_host")]
extern "C" {
    fn host_compute(input: u32) -> u32;  // 如果宿主环境没有这个函数名
}

// ✅ 确保函数名与宿主环境完全一致
#[link(wasm_import_module = "my_host")]
extern "C" {
    fn host_compute(input: u32) -> u32;  // 宿主环境必须导出 host_compute
}

// ✅ 如果确实需要动态导入
#[link(wasm_import_module = "env")]
extern "C" {
    fn dynamic_callback() -> u32;
}

迁移步骤:

  1. 升级到 Rust 1.96
  2. 运行 cargo build --target wasm32-unknown-unknown
  3. 如果出现链接错误,检查每个 extern "C" 块的函数名拼写和 #[link(...)] 属性
  4. 如果使用了 wasm-bindgen,通常不需要修改(绑定生成器会自动处理符号)
  5. 如果确实需要允许未定义符号,使用上述方法一或方法二

六、其他值得关注的稳定化 API

6.1 pointer::is_aligned

检查指针是否满足给定对齐,无需 unsafe 手动计算:

let ptr: *const u32 = &42u32;
assert!(ptr.is_aligned());

// 对齐检查在自定义分配器中特别有用
struct AlignedBuffer<const ALIGN: usize> {
    ptr: *mut u8,
    len: usize,
}

impl<const ALIGN: usize> AlignedBuffer<ALIGN> {
    fn new(len: usize) -> Self {
        let layout = std::alloc::Layout::from_size_align(len, ALIGN).unwrap();
        let ptr = unsafe { std::alloc::alloc(layout) };
        debug_assert!(ptr.is_aligned());
        Self { ptr, len }
    }
}

6.2 NonNull::is_aligned

同上,适用于非空指针:

use std::ptr::NonNull;

fn check_alignment(ptr: NonNull<u64>) -> bool {
    ptr.is_aligned()  // 检查是否满足 8 字节对齐
}

6.3 {slice, array}::as_flattened_mut

&mut [[T; N]] 重新解释为 &mut [T],便于对二维数组进行线性操作:

let mut matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];

// 旧方式:需要 unsafe 或逐个元素处理
// 新方式:一行搞定
let flat: &mut [i32] = matrix.as_flattened_mut();
flat[4] = 0;  // 修改 matrix[1][1]
assert_eq!(matrix[1][1], 0);

这在图像处理、矩阵运算等场景中特别有用——你可以安全地在行优先和列优先之间切换,而无需手动计算偏移量。

6.4 Option::take_if

条件性地取出值,失败时返回 None,类似于 filter 但获取所有权:

let mut x = Some(42);
let taken = x.take_if(|v| *v > 10);
// x 变为 None,taken 为 Some(42)
assert_eq!(x, None);
assert_eq!(taken, Some(42));

let mut y = Some(5);
let not_taken = y.take_if(|v| *v > 10);
// y 保持 Some(5),not_taken 为 None
assert_eq!(y, Some(5));
assert_eq!(not_taken, None);

实际应用——从任务队列中取出特定类型的任务:

#[derive(Debug)]
enum Task {
    Compute(u32),
    Download(String),
    Shutdown,
}

fn process_shutdown(queue: &mut Vec<Option<Task>>) -> bool {
    for slot in queue.iter_mut() {
        if let Some(Task::Shutdown) = slot.take_if(|t| matches!(t, Task::Shutdown)) {
            return true;  // 找到并取出 Shutdown 任务
        }
    }
    false
}

七、Cargo 安全漏洞修复

7.1 CVE-2026-5223(中危):软链接篡改

影响范围:仅第三方私有 registry 用户,crates.io 用户不受影响。

漏洞描述:第三方注册表的 crate 压缩包中如果包含符号链接(symlink),解压时可能通过软链接篡改本地文件。攻击者可以在 crate 包中放置指向敏感路径的软链接,当 Cargo 解压时会跟随链接写入攻击者控制的内容。

1.96 的修复方式:直接拒绝解析包内软链接。如果 crate 包中包含软链接,Cargo 会报错并拒绝安装。

7.2 CVE-2026-5222(低危):URL 标准化认证漏洞

影响范围:仅使用 Git 协议注册表的用户。

漏洞描述:Git 协议注册表的 URL 标准化过程中存在认证缺陷,可能导致认证信息被错误地应用到不同的源地址。

1.96 优化了源地址校验逻辑,确保 URL 标准化过程中认证信息不会被误用。

7.3 你需要做什么?

  • 如果你只使用 crates.io:什么都不用做,这两个漏洞不影响你
  • 如果你使用第三方私有 registry:立即升级到 1.96,修复已在工具链中
  • 如果你维护私有 registry:建议在服务端也加入软链接检测,作为纵深防御

八、升级指南与最佳实践

8.1 升级命令

# 使用 rustup 升级
rustup update stable

# 验证版本
rustc --version
# 应输出:rustc 1.96.0 (...)

# 如果需要指定版本
rustup install 1.96.0
rustup default 1.96.0

8.2 升级后的立即收益

  1. Wasm 项目:运行 cargo build --target wasm32-unknown-unknown,如果链接通过,说明你的符号声明正确。如果出现链接错误,恭喜你——1.96 帮你提前发现了一个潜在的运行时 bug。

  2. 测试套件:将 assert!(matches!(...)) 替换为 assert_matches!(...),失败信息会更有用。可以批量替换:

# 查找所有可替换的断言
rg 'assert!\(matches!' --type rust

# 替换模板
# assert!(matches!(expr, pattern)) → assert_matches!(expr, pattern)
# assert!(matches!(expr, pattern), msg) → assert_matches!(expr, pattern)  // 错误信息现在是自动的
  1. 库作者:开始在公开 API 中使用 impl RangeBounds 而不是具体的 std::ops::Range,为未来的 Edition 切换做好准备。

8.3 长期迁移路线图

时间线建议动作
现在(1.96)升级工具链,使用 assert_matches!,评估 core::range 新类型
下一个 Edition.. 语法将生成 core::range 类型,旧代码可能需要适配
长期std::ops::Range 可能被弃用,全面迁移到 core::range

九、总结与展望

Rust 1.96.0 是一次典型的"长尾改进"式发布。它没有引入惊天动地的新语法,却在几个基础组件的深处修复了积年已久的 API 设计瑕疵。

三大核心变更回顾:

  1. core::range:将"区间描述"和"迭代状态"分离,让 Range 类型终于支持 Copy。这是一个教科书级的 API 重设计——不破坏旧代码,引入新类型提供渐进迁移路径,在未来 Edition 中完成切换。

  2. Cargo 双源依赖:一个声明搞定开发与发布两种场景,终结了长期以来的手动切换之痛。这个特性对团队协作的影响尤其深远——再也不用担心"本地能跑,CI 挂了"的尴尬。

  3. Wasm 严格链接:从"宽容默认"到"严格默认",这是一个安全哲学的转变——让错误在编译时暴露,而不是在运行时爆炸。

Rust 的成熟体现在哪里?

不在于每隔几个版本就搞出个惊天动地的新特性,而在于这种持续打磨、渐进修复的耐心。每一个版本都在让标准库更一致、工具链更安全、开发体验更顺畅。1.96 正是这样一枚"精益求精"的补丁。

作为开发者,我的建议是:立即升级。这些改进看似细微,但日积月累,它们会让你的 Rust 代码更简洁、更安全、更易维护。而 Rust 正是通过这种持续迭代,一步步兑现"零成本抽象"和"安全默认"的承诺。


参考链接:

复制全文 生成海报 Rust Cargo WebAssembly Range 依赖管理

推荐文章

介绍Vue3的静态提升是什么?
2024-11-18 10:25:10 +0800 CST
如何在Vue中处理动态路由?
2024-11-19 06:09:50 +0800 CST
20个超实用的CSS动画库
2024-11-18 07:23:12 +0800 CST
Vue3中的Slots有哪些变化?
2024-11-18 16:34:49 +0800 CST
前端代码规范 - 图片相关
2024-11-19 08:34:48 +0800 CST
批量导入scv数据库
2024-11-17 05:07:51 +0800 CST
Vue3中如何实现插件?
2024-11-18 04:27:04 +0800 CST
Java环境中使用Elasticsearch
2024-11-18 22:46:32 +0800 CST
程序员茄子在线接单