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
}레퍼런스에 따르면 가변 인자를 받는 함수 정의는 unsafe여야 하고, 가변 인자는 마지막 매개변수여야 한다. 본문에서 그 매개변수의 타입은 VaList이며, VaList는 C의 va_list와 ABI 호환이다.
VaList에서 읽을 수 있는 타입은 VaArgSafe 트레이트로 제한된다. 비-"C" ABI의 naked 가변 인자 함수 정의도 함께 안정화됐는데, 이는 인라인 어셈블리로 작성해야 한다.
주의할 점: next_arg가 안전한 조건
레퍼런스는 VaList::next_arg로 T 타입 인자를 읽는 것이 안전하려면 읽을 인자가 남아 있고, 실제 인자 타입이 T와 호환돼야 한다고 규정한다. 두 타입이 모두 정수라면 넘긴 값이 양쪽 타입에서 표현 가능해야 한다.
이 조건은 컴파일러가 검사하지 않는다. VaArgSafe를 만족하는 타입이라도 호출자가 다른 타입이나 더 적은 인자를 넘기면 정의되지 않은 동작이 된다.
정의 지원은 x86·x86-64, AArch64, RISC-V 등 레퍼런스가 나열한 아키텍처에서만 안정화됐고, BPF처럼 지원하지 않는 대상에서는 컴파일 오류가 난다. 이 기사에서 예제를 컴파일하거나 FFI 경계를 시험하지는 않았다.
가변 인자 경계를 좁게 두는 구성
인자를 꺼내는 코드는 작은 경계 함수 하나에 모으고, 기대하는 인자 순서와 타입을 그 함수의 안전 주석에 적어 둘 수 있다.
그 바깥은 안전한 래퍼가 맡아, unsafe한 읽기에 들어가기 전에 확인할 수 있는 입력을 먼저 확인하는 구조가 된다.
Rust 1.99의 C 가변 인자 안정화는 nightly 의존을 없앤 변화이며, 인자 타입과 개수를 맞추는 unsafe 계약은 여전히 작성자에게 있다.