Rust 1.99.0 is out, and it lets programmers write C-style variadic functions in Rust itself, for the first time on the stable compiler. The Rust release team shipped it on 1 October, along with three new raw-pointer layout functions and a batch of newly stable standard library APIs.

If you've already got Rust through rustup, the release announcement says one command does it: rustup update stable. Nothing about how the language behaves has changed in this release, so it should be a quiet upgrade for most code.

Variadics, written in Rust

A variadic function is one that takes a variable number of arguments. C's printf is the famous one. You can hand it one value or ten, and it works out what it got from the format string.

Tap to enlarge

Rust has long been able to call variadic functions written somewhere else, like libc::printf. What it couldn't do on stable was define one. Say a C library wanted you to give it a callback with a ... in its signature. Or say you were rewriting a C library in Rust and had to keep its interface. Either way, you had to leave that piece in C or find a workaround.

Rust 1.99 stabilises defining variadic functions with the "C" and "C-unwind" ABIs. The release post shows a short example, an unsafe extern "C" fn sum(mut args: ...) that pulls two i32 values off the argument list and adds them up.

The type behind the ... is VaList, which the Rust team says is ABI-compatible with C's va_list across targets. What you're allowed to read out of a VaList is policed by a trait called VaArgSafe. That's Rust's way of stopping you pulling out types the C calling convention can't pass safely.

The functions are still unsafe, and there's a good reason. The caller has to pass the right number and types of arguments, and the compiler can't check that for you. The example in the release notes says so in its safety comment. The function "must be called with (at least) 2 i32 arguments". Rust gives you a typed, documented way to do the C thing, but it doesn't pretend the C thing is safe.

The release also stabilises naked variadic functions with non-"C" ABIs. Those have to be written in inline assembly, so they're for people writing very low-level code, like runtime and operating system developers.

Why that matters

Rust's big promise is memory safety without a garbage collector, and a lot of its real-world growth comes from replacing C bit by bit. Every gap in how well Rust can stand in for C makes that harder. Variadic functions were one of those gaps. You don't see them much in new code, but they're all over older C interfaces, and when the C side sets the signature, you don't get to choose a nicer one.

Being able to write them in Rust means fewer little bits of C glue left over in mixed codebases. A logging function, an error formatter, or a plugin entry point that a C program expects to call with a variable list of arguments can now live in the same language as the rest of the project.

Memory-safety bugs are still a real problem in systems code. Last month's four Linux kernel root flaws included out-of-bounds access and memory corruption in the kernel's C code. Linus Torvalds merged initial Rust support into the kernel for Linux 6.1 in 2022, so Rust has sat alongside C there ever since, and this isn't an abstract debate. Rust won't fix existing C on its own. But each release that makes it easier to swap C for Rust makes rewriting the risky pieces a bit more practical.

Asking a pointer how big it is

The second big change is about raw pointers. Rust 1.99 settles the safety rules for getting the size and alignment of whatever a raw pointer points at. That includes pointers to types whose size isn't known when the code's compiled, like slices and trait objects.

Three functions become stable to do it: Layout::for_value_raw, mem::size_of_val_raw and mem::align_of_val_raw. For ordinary Sized types, the release post notes, this was already trivially safe and possible on stable. The new part is the unsized case, which matters to people writing allocators, smart pointers and other code that handles memory directly.

There's a documentation change worth knowing about too. The Rust team's updated Box::leak to recommend against "round-trip unleaking". That means leaking a box and later turning it back into one to free the memory. The team says that pattern turned out to clash with current and possible future compiler optimisations, and that it's especially problematic with custom allocators about to be stabilised. It recommends Box::into_non_null or Box::into_raw instead. The same advice goes for the other leak functions in the standard library.

The rest of the list

The newly stable APIs are a practical lot. Vec::into_parts and Vec::from_parts let you take a vector apart into its pointer, length and capacity, and put it back together again. Box::into_non_null and Box::from_non_null do a similar job for boxes, using the non-null pointer type.

String::from_utf8_lossy_owned and FromUtf8Error::into_utf8_lossy turn bytes that might not be valid UTF-8 into a string, without an extra copy when you already own the buffer. Linuxiac's write-up of the release picks that out as one of the more noteworthy additions.

For files, std::fs::set_times and std::fs::set_times_nofollow let you change file timestamps from the standard library, the second one without following symbolic links. VecDeque::retain_back filters a double-ended queue working from the back. Boxed arrays get IntoIterator for owned, borrowed and mutably borrowed forms, and StepBy now implements FusedIterator.

The full release notes also list changes to the compiler, Cargo and Clippy.

Rust puts out a new stable version every six weeks, and the team asks anyone who wants to help to test the beta and nightly channels and report bugs. That steady schedule is why a release like this doesn't need a big launch. It's another set of rough edges filed down, and for anyone gluing Rust to C, one of the bigger ones is gone.