Rust 1.99:终于可以用 Rust 定义 C 可变参数函数

2026-10-01 18 预计阅读时间: 1 分钟
来源: blog.rust-lang.org AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

Rust 1.99.0 把一批面向底层开发和 FFI 的能力带入稳定通道。其中最醒目的变化,是 Rust 不仅能调用 C ABI 的可变参数函数,现在也能直接定义这类函数。与此同时,标准库新增了原始指针布局查询、Box 裸指针所有权转换、文件时间设置等 API。

如果你维护 C/Rust 混合项目、系统库或自定义内存管理代码,这个版本值得重点检查。

升级与验证环境

通过 rustup 安装 Rust 的开发者可以直接升级稳定工具链:

rustup update stable
rustc --version
cargo --version

准备测试未来版本时,建议只在独立项目或 CI 任务中切换通道,避免影响日常开发环境:

# 为当前目录固定 beta,而不是修改全局默认工具链
rustup override set beta

# 测试完成后恢复
rustup override unset

也可以将 beta 换成 nightly。发现回归时,应尽量附上最小复现代码、目标平台、rustc -Vv 输出和完整报错信息。

Rust 现在可以定义 C ABI 可变参数函数

过去,Rust 已经可以调用 printf 这类由外部库定义的 C 可变参数函数。Rust 1.99.0 进一步稳定了使用 extern "C" 和 extern "C-unwind" 定义可变参数函数的能力。

下面是一个可直接创建并运行的最小项目。运行前请确认当前工具链为 Rust 1.99.0 或更高版本:

mkdir rust-variadic-demo
cd rust-variadic-demo
cargo init --bin

cat > src/main.rs <<'RS'
/// # Safety
/// 调用者必须传入至少两个类型为 i32 的参数。
unsafe extern "C" fn sum(mut args: ...) -> i32 {
    // SAFETY: 函数契约要求调用者提供两个 i32 参数。
    let a = unsafe { args.next_arg::<i32>() };
    let b = unsafe { args.next_arg::<i32>() };
    a + b
}

fn main() {
    // SAFETY: 这里确实传入了两个 i32 参数。
    let result = unsafe { sum(20_i32, 22_i32) };
    println!("result = {result}");
}
RS

cargo run

预期输出为:

result = 42

函数签名中的 ... 对应 VaList,它会在不同目标平台上与 C 的 va_list 保持 ABI 兼容。能够通过 next_arg 读取的类型则受到 VaArgSafe 约束。

这里的关键并不是语法,而是调用双方必须严格共享同一份隐含契约。可变参数列表不会携带足够的信息,让被调用函数自动判断参数数量和真实类型。读取数量错误、类型错误或忽略 C 的默认参数提升规则,都可能导致未定义行为。

因此,实际导出这类接口时,最好显式增加数量或格式参数。例如可以设计成:

// 假设性接口设计:count 明确说明后面有多少个参数。
unsafe extern "C" fn sum_n(count: usize, mut args: ...) -> i64 {
    let mut total = 0_i64;

    for _ in 0..count {
        // 接口契约必须规定每个参数均以 i64 传入。
        total += unsafe { args.next_arg::<i64>() };
    }

    total
}

这仍然是 unsafe 接口,但比完全依赖口头约定更容易审计。跨语言发布时,还应提供对应的 C 头文件,并在文档中明确参数提升、整数宽度、调用约定和异常跨边界策略。

Rust 1.99 也稳定了使用非 "C" ABI 定义 naked 可变参数函数的支持,不过这类函数必须通过内联汇编编写。它更适合启动代码、上下文切换或极底层运行时,不应作为普通业务 FFI 的首选方案。

原始指针也能查询动态布局

本次发布稳定了三个与原始指针布局相关的 API:

  • core::mem::size_of_val_raw
  • core::mem::align_of_val_raw
  • core::alloc::Layout::for_value_raw

它们覆盖了 Sized 和非 Sized 类型。后者尤其重要,因为切片和 trait object 的大小依赖指针携带的元数据,不能只通过静态类型得出。

这类 API 适合分配器、序列化框架、FFI 桥接层和容器实现,但“只查询大小”不代表可以忽略安全条件。传入的原始指针及其元数据仍须满足相应要求。在封装这些函数时,可以这样实践:

  1. 将原始指针的来源限制在少数构造函数中。
  2. 在 unsafe 块旁写明指针元数据为何有效。
  3. 不要把布局有效误解为对象已经初始化或可以解引用。
  4. 对切片长度溢出、伪造 trait object 元数据等情况做单独测试。

不要再用 Box::leak 绕一圈回收内存

Rust 1.99 没有改变 Box::leak 的语言语义,但更新了文档建议:不推荐先泄漏 Box、之后再设法重新获得所有权并释放内存。这种模式可能与当前及未来的编译器优化产生问题,在自定义分配器即将稳定的背景下尤其值得警惕。

如果目标是暂时把 Box 转换成稳定的非空指针,应该优先使用新稳定的 Box::into_non_null,并用 Box::from_non_null 恢复所有权:

use std::ptr::NonNull;

fn main() {
    let boxed = Box::new(String::from("owned by Box"));
    let ptr: NonNull<String> = Box::into_non_null(boxed);

    // 在裸指针阶段,代码必须自行维护别名、生命周期和线程安全约束。
    unsafe {
        println!("{}", ptr.as_ref());
    }

    // SAFETY:
    // 1. ptr 来自 Box::into_non_null;
    // 2. 尚未通过其他方式释放;
    // 3. 只恢复一次 Box 所有权。
    let boxed = unsafe { Box::from_non_null(ptr) };
    drop(boxed);
}

这对 API 把所有权转移表达得更直接,但不会自动消除风险。必须保证 from_non_null 只调用一次,指针来自兼容的 Box 分配,且恢复所有权前没有被释放。否则可能发生重复释放、释放器不匹配或悬垂指针访问。

相同的审慎原则也适用于标准库中的其他 leak 类函数。

其他值得关注的稳定 API

Rust 1.99.0 还稳定了多项标准库能力:

  • 为 Box<[T; N]>、&Box<[T; N]> 和 &mut Box<[T; N]> 实现 IntoIterator。
  • VecDeque::retain_back,便于从队尾方向执行保留操作。
  • Vec::into_parts 与 Vec::from_parts,为底层容器所有权拆分和重建提供更明确的接口。
  • String::from_utf8_lossy_owned 与 FromUtf8Error::into_utf8_lossy,方便处理已经拥有所有权的无效 UTF-8 数据。
  • 为 StepBy<I> 实现 FusedIterator。
  • std::fs::set_times 与 std::fs::set_times_nofollow,用于设置文件时间;后者不会跟随符号链接。

其中涉及裸指针、所有权拆分和文件系统符号链接的 API,都需要额外关注平台差异与安全边界。升级时不要只确认“能够编译”,还应在实际支持的操作系统和目标架构上运行测试。

升级时建议检查什么

普通应用可以先升级本地工具链,再让 CI 覆盖所有目标平台。维护底层库时,建议增加以下检查:

  • 搜索是否存在 Box::leak 后重新构造并释放内存的代码。
  • 审计 C 可变参数接口的参数数量、类型和默认参数提升规则。
  • 检查原始指针布局查询是否记录了完整的安全前提。
  • 在 Miri、地址消毒器或现有 FFI 集成测试中覆盖新增路径。
  • 查看 Cargo 和 Clippy 的完整变更,确认构建脚本与 lint 基线没有回归。
  • 若要采用新的稳定 API,明确项目的最低支持 Rust 版本是否可以提升到 1.99。

Rust 1.99 的重点不是更华丽的语言语法,而是让过去需要夜间工具链或临时指针技巧的底层工作拥有更正式的稳定接口。新能力减少了绕路,但 FFI、可变参数和裸指针仍然依赖精确的安全契约;升级之后,最有价值的工作依旧是把这些契约写进类型、注释和测试里。


相关推荐