Rust 1.99.0が10月1日に公開された。このバージョンで、extern "C"とextern "C-unwind" ABIのC可変長引数(variadic)関数をstable Rustで定義できるようになった。
libc::printfのような外部の可変長引数関数をRustから呼ぶことは以前からできた。今回安定化されたのは、そうした関数をRustで直接書く側だ。
リリース告知の例は次のとおりだ。コメントにあるように、i32の引数を少なくとも2つ渡す保証は呼び出し側の責任だ。
/// 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で、VaListはCのva_listとABI互換だ。
VaListから読み出せる型はVaArgSafeトレイトで制限される。"C"以外のABIのnaked可変長引数関数の定義も併せて安定化されたが、これはインラインアセンブリで書く必要がある。
注意点:next_argが安全になる条件
Referenceは、VaList::next_argでT型の引数を読むのが安全なのは、読むべき引数が残っており、実際の引数の型がTと互換である場合だけだと定めている。どちらも整数型なら、渡された値が両方の型で表現できなければならない。
これらの条件はコンパイラーが検査しない。VaArgSafeを満たす型でも、呼び出し側が違う型や少ない引数を渡せば未定義動作になる。
定義の対応は、x86・x86-64、AArch64、RISC-VなどReferenceが挙げるアーキテクチャーでのみ安定化され、BPFのような非対応ターゲットではコンパイルエラーになる。この記事では例をコンパイルしておらず、FFI境界も試していない。
可変長引数の境界を狭く保つ構成
引数を取り出すコードは小さな境界関数1つにまとめ、期待する引数の順序と型をその関数の安全性コメントに書いておける。
その外側は安全なラッパーが受け持ち、unsafeな読み出しに入る前に確かめられる入力を先に確かめる構造になる。
Rust 1.99のC可変長引数の安定化はnightlyへの依存をなくした変化であり、引数の型と個数を合わせるunsafeな約束は引き続き書き手にある。