Rust 1.99.0, released on October 1, 2026, stabilizes the ability to define C-ABI variadic functions. Until now Rust could call into C variadics — printf and friends worked fine — but it could not produce one. That asymmetry is a reasonable safety position, and the interesting part of this release is the specific shape of the escape hatch it uses to close the gap.

The mechanism is core::ffi::VaList, the type behind the ... in a variadic signature, plus a compile-time whitelist trait called VaArgSafe that restricts which types may be read out of it. Reading an argument is args.next_arg::<T>(), wrapped in unsafe, and that unsafe is the honest part: the trait constrains types, not arity, so calling it for an argument that was never passed is undefined behaviour rather than a panic. The release also carries a quieter change with a longer tail — the documentation for Box::leak now advises against round-tripping a leaked box back into a deallocation, ahead of custom allocators stabilising.

Key takeaways:

  • Variadics are now definable with "C" and "C-unwind" ABIs. The ... is typed as core::ffi::VaList.
  • VaArgSafe is a compile-time whitelist of permitted types. It does not check how many arguments were actually passed.
  • next_arg::<T>() is unsafe because getting the arity wrong is undefined behaviour, not a recoverable error.
  • Naked variadics with non-"C" ABIs are also stable, but must be written in inline assembly. There is no safe API for those.
  • Box::leak docs now warn against leaking then deallocating, citing interactions with compiler optimisations and upcoming custom allocators. Advisory, not deprecated.

What the release actually enables

unsafe extern "C" fn sum(mut args: ...) -> i32 {
    let a = unsafe { args.next_arg::<i32>() };
    let b = unsafe { args.next_arg::<i32>() };
    a + b
}

Two things are worth noticing in that snippet. The function is unsafe, because it is now the author’s job to agree with the caller about arity and types — previously that agreement was checked by the absence of a way to write the function at all. And each read is individually unsafe, because next_arg cannot know whether the next argument exists.

That is the design in one line: Rust moved the unsafety from the language boundary to the individual call, where a caller and callee can actually reason about it.

VaArgSafe is a type whitelist, not an arity check

The permitted types for next_arg::<T>() are constrained by the VaArgSafe trait, defined at core::ffi::VaArgSafe. That is what stops you reading a CString out of a position the caller filled with an integer, or any other type where the layout assumption would be nonsense.

What it does not do is count. C variadics carry no length information; the callee is expected to know its own signature, which for a truly variadic function it often does not. So if the caller passes one argument and the callee reads two, the second read walks off the end of the argument area. That is undefined behaviour, and it will not produce a panic you can catch.

Practically, this means the unsafe block around next_arg is a real obligation. If you are writing a variadic function whose arity is genuinely dynamic, you need a sentinel convention — a length argument before the variadic portion, or a terminating sentinel type — and the unsafe is where you assert that convention holds. Reading until it fails is not available.

Why this took so long

C variadics are a genuinely awkward fit for a language that usually knows the shape of everything. A fixed-arity call has a signature the compiler can check against the callee’s declaration. A variadic call does not. Beyond the fixed prefix, there is no type information at all, and the promotion rules involved (float to double, the small-integer promotion) are part of the C ABI’s definition rather than something Rust’s type system models.

Calling was always possible because the call happens at a boundary where the C declaration is already a promise someone else made. Defining is different: you are the one making the promise, and a signature that can be satisfied by anything is not much of a promise. VaArgSafe narrows that just enough to be useful without pretending the arity is known.

This is the recurring Rust pattern for admitting an unsafe-by-construction feature: keep the surface tiny, constrain what types can flow through it, and leave the genuinely unverifiable part behind an explicit unsafe block.

The naked variadic case

The release also stabilises defining naked variadic functions with ABIs other than "C". These must be written in inline assembly. There is no safe or ergonomic API for them, which is worth stating plainly so nobody goes looking for one.

The rest of the stabilised surface

Beyond the headline, 1.99.0 stabilises a set of APIs that are individually unremarkable and collectively useful:

  • IntoIterator for Box<[T; N]>, &Box<[T; N]> and &mut Box<[T; N]>, so fixed-size boxed slices iterate by value instead of by reference
  • VecDeque::retain_back, the counterpart to the existing retain_front
  • Box::into_non_null and Box::from_non_null
  • Vec::into_parts and Vec::from_parts, for reusing a vector’s allocation with a different type
  • String::from_utf8_lossy_owned and FromUtf8Error::into_utf8_lossy, so the lossy conversion can consume the buffer instead of copying
  • FusedIterator for StepBy<I>
  • std::fs::set_times and std::fs::set_times_nofollow, for setting file timestamps from Rust
  • Three layout-from-raw-pointer functions: Layout::for_value_raw, mem::size_of_val_raw and mem::align_of_val_raw

Two of these deserve a flag for anyone writing FFI or allocator-adjacent code. Vec::from_parts plus Layout::for_value_raw together cover a real gap: reconstructing a collection over memory whose layout you computed yourself, which is what custom allocators and some no_std designs need. And size_of_val_raw and align_of_val_raw require a valid pointer to an initialised value. The safety contract is real even though the operation looks harmless.

The Box::leak warning worth acting on now

Buried in the documentation changes is one with a longer tail than the headline feature. The docs for Box::leak now recommend against round-tripping — leaking a box and then deallocating it later. The stated reason is problematic interactions with current and future compiler optimisations, “especially problematic with the upcoming stabilization of custom allocators.”

It is a documentation change, not a deprecation. Nothing warns, nothing fails to compile. But the reasoning generalises beyond Box::leak to any other standard-library leak function: if you leak an allocation and hand the pointer back to a deallocating API later, you are depending on optimisers continuing to see through the intermediate representation. That is exactly the assumption custom allocators will stress, because the allocator is no longer the compiler’s to reason about.

If you have leak-then-deallocate code, this is a good moment to replace it with Box::into_non_null or Box::into_raw, which hand ownership over without the round trip.

Two things the release notes do not tell you

First, the release post does not state a contributor count. The post links a generated contributor list rather than quoting a figure, so if you see a number attributed to 1.99.0, it came from somewhere other than the announcement.

Second, this is a stabilisation release. It contains no removals and no breaking changes, which makes the upgrade uneventful. The interesting risk in this release is entirely in what you write with the new features, not in adopting them.

References

Related Tools

Try these companion tools on DevStackIO Tools:

  • unit converter — convert between measurement units for systems programming.