Rust 1.99.0 于 10 月 1 日发布。这个版本允许在 stable Rust 中定义 extern "C" 和 extern "C-unwind" ABI 的 C 可变参数(variadic)函数。
在 Rust 中调用 libc::printf 等外部可变参数函数此前就已可行。这次稳定的是用 Rust 直接编写这类函数。
发布公告给出的示例如下。正如注释所写,保证至少传入 2 个 i32 参数是调用方的责任。
/// 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
}根据 Reference,接受可变参数列表的函数定义必须是 unsafe 的,可变参数必须是最后一个参数。在函数体内,该参数的类型是 VaList,它与 C 的 va_list ABI 兼容。
可以从 VaList 读取的类型受 VaArgSafe trait 限制。非 "C" ABI 的 naked 可变参数函数定义也一并稳定,这类函数必须用内联汇编编写。
注意事项:next_arg 安全的条件
Reference 规定,用 VaList::next_arg 读取 T 类型参数要安全,必须还有可读的参数,且实际参数类型与 T 兼容。如果两者都是整数类型,传入的值必须能在两种类型中表示。
编译器不会检查这些条件。即使类型满足 VaArgSafe,只要调用方传入了不同类型或更少的参数,就是未定义行为。
定义支持只在 Reference 列出的架构上稳定,例如 x86 和 x86-64、AArch64、RISC-V;在 BPF 等不支持的目标上会产生编译错误。本文没有编译示例,也没有测试 FFI 边界。
让可变参数边界保持狭窄的结构
可以把取参数的代码集中到一个小的边界函数中,并在其安全注释里写明预期的参数顺序和类型。
其外部由安全的包装函数负责,在进入 unsafe 读取之前先检查能够检查的输入。
Rust 1.99 的 C 可变参数稳定化消除了对 nightly 的依赖,而让参数类型和数量相符的 unsafe 约定仍由作者承担。