Rust 1.99 新特性详解:正式支持使用 Rust 编写 C 语言可变参数函数

Rust · 版本发布 1.99

Rust 1.99 新特性详解:正式支持使用 Rust 编写 C 语言可变参数函数

rustRust版本发布可变参数裸指针内存泄漏

数据源:GitHub Releases + 官方博客 + HN

本次 Rust 1.99.0 的发布,核心亮点在于稳定了 extern "C" variadics(C 语言可变参数列表支持)。此外,新版本还完善了裸指针的布局信息获取,并对 Box::leak 的内存释放规范给出了重要警告,同时稳定了多项底层转换 API,进一步提升了语言的底层开发体验。笔者将带领大家深入探讨这些激动人心的新特性。

extern “C” variadics 支持

解决什么问题: 在以往的 Rust 版本中,开发者可以轻松调用由 C 语言或其他语言编写的外部可变参数函数(例如 libc 中的 printf 家族)。但是,如果想要在 Rust 内部定义一个能够接收可变参数并暴露给 C 语言调用的函数,一直缺乏直接的语言级支持。这使得 Rust 在替换 C 语言底层库,或者编写需要实现特定 C 接口的操作系统驱动程序时,常常需要依赖复杂的宏或内联汇编。

从 1.99 开始,Rust 终于正式允许直接定义具有 "C" 或 "C-unwind" ABI 的可变参数函数。其参数类型明确使用 ... 表示,在 Rust 内部对应的新类型是 VaList。这个类型能够在底层与 C 语言的 va_list 结构实现跨目标平台的 ABI 完全兼容。为了确保安全性,可以从 VaList 中读取的数据类型受到了 VaArgSafe trait 的严格保护。不仅如此,本次更新还稳定了通过内联汇编为非 "C" ABI 编写 naked variadic 函数的支持。

官方示例代码:

/// SAFETY: must be called with (at least) 2 i32 arguments.
unsafe extern "C" fn sum(mut args: ...) -> i32 {
    // SAFETY: guaranteed by the caller.
    let a = unsafe { args.next_arg::<i32>() };
    let b = unsafe { args.next_arg::<i32>() };
    a + b
}

fn foo() -> i32 {
    unsafe { sum(0i32, 2i32) }
}

通过裸指针获取内存布局信息

解决什么问题: 在进行极致性能优化或构建自定义数据结构时,裸指针操作是不可避免的。而在操作裸指针时,安全地获取其背后数据的内存大小(Size)和对齐信息(Alignment)是一项核心基础需求。对于实现了 Sized 的类型,这相对简单;但对于非 Sized 类型(如 [T] 或 dyn Trait),由于胖指针的设计,直接获取布局信息不仅麻烦,还往往伴随着未定义行为(UB)的风险。

本次发布彻底厘清了这部分的安全性要求,并稳定了一系列专用于裸指针的布局获取函数:

  • Layout::for_value_raw
  • mem::size_of_val_raw
  • mem::align_of_val_raw

这些函数的稳定意味着开发者可以直接向它们传递裸指针,而无需事先将其转换为引用(这在未完全初始化的内存或借用规则严格限制的场景下极易触发 UB)。这让底层内存分配、回收操作和自定义分配器(Custom Allocators)的实现变得前所未有的安全与便捷。

破坏性注意点:不再建议撤销 Box::leak 的泄露

解决什么问题: 这是一个极其重要的行为规范更新。在 Rust 的内存管理模式中,Box::leak 常常被用来将动态分配的内存泄露,从而获得一个 'static 生命周期的可变引用。过去,一些开发者会采用一种 trick:先将内存 leak 出来使用,在程序某个生命周期末尾,再用 unsafe 方式重新将其构建回 Box 并将其丢弃(Drop),从而达到所谓“撤销泄露”(round-trip unleaking)的目的。

然而,官方文档在 1.99.0 中大幅更新,明确警告开发者不要再采用这种反模式。这是因为编译器优化模型(尤其是未来的 LLVM 优化升级)在看到 Box::leak 时,可能会进行一种被称为“不可达释放”的激进优化假设。如果在 leak 之后再次释放该内存,不仅会破坏编译器的别名分析(aliasing analysis),更可能在自定义分配器稳定后引发不可预测的崩溃问题。

作为更安全的替代方案,官方建议开发者使用 Box::into_non_null 或 Box::into_raw。这两个 API 纯粹只是进行了指针类型的转换,并没有向编译器传达“永不释放”的语义。此指导原则同样适用于标准库中的其他 leak 函数,如 String::leak 等。

核心 API 稳定化

解决什么问题: 除了上述重大特性外,Rust 1.99 还稳定了大量极具实用价值的基础库 API,为开发者在进行复杂数据处理时提供更符合人体工程学的标准方案。

  • Vec 与零拷贝拆解: 新增稳定的 Vec::into_parts 和 Vec::from_parts 彻底解决了开发者在处理 C-FFI 时,将 Vec 拆解为裸指针、长度和容量再重新组合的安全痛点。过去许多开发者在手动拆解并传递结构时,容易因为字段顺序或是借用检查错误导致内存泄漏或悬垂指针。新接口提供了更清晰的结构化数据载体,提升了 FFI 边界的代码安全性。
  • 字符串有损转换: 新增的 String::from_utf8_lossy_owned 以及 string::FromUtf8Error::into_utf8_lossy 提供了一种直接将已分配内存字节数组转换为 String 的方法。如果是无效的 UTF-8 序列,它会在原分配空间上进行有损替换,避免了大量不必要的中间字符串分配,对于编写高性能网络协议解析器极为重要。
  • 文件元数据操作与集合更新: std::fs::set_times 和 std::fs::set_times_nofollow 允许直接对文件系统的时间戳(访问时间与修改时间)进行精细化设置,弥补了长期以来跨平台时间修改的不足。此外 Box::into_non_null 和 Box::from_non_null 接口的稳定,以及为 Box<[T; N]> 引用实现了 IntoIterator trait,进一步丰富了集合操作的灵活性。VecDeque::retain_back 也被正式加入标准库,为双端队列提供了更高效的反向过滤手段。

升级建议

笔者整理了针对不同类型开发者的升级建议表,大家可以根据自己的项目需求进行对照:

目标用户升级时机注意事项
需要与 C 语言可变参数深度交互的开发者建议立即升级新特性能够显著精简 FFI 绑定层的代码。但需要注意 ... 类型(即 VaList)的操作依然属于 unsafe 范畴,必须严格遵循调用约定与安全边界。
编写底层内存管理和自定义分配器的库作者建议平稳过渡升级务必全面排查代码库中是否存在撤销 Box::leak 的操作模式,请统一替换为更安全的 Box::into_raw 或 Box::into_non_null 接口,以防止触发编译器未定义行为。
一般应用、Web 服务与后端开发者按常规节奏升级即可执行 $ rustup update stable 即可平滑无缝升级,无需修改现有代码。您可以直接享受新版本带来的编译器底层优化、更快的编译速度和基础库功能扩展。

参考来源