Rust 1.99 Deep Dive: Native Support for Writing C Variadic Functions in Rust

Rust · Release 1.99

Rust 1.99 Deep Dive: Native Support for Writing C Variadic Functions in Rust

rustRustreleasevariadicsraw-pointersmemory-leak

Sources:GitHub Releases + 官方博客 + HN

Rust 1.99.0 has officially been released. The headline feature of this release is the stabilization of extern "C" variadics (support for C variadic parameter lists). In addition, this release refines querying layout information directly from raw pointers, provides an important warning regarding Box::leak memory deallocation practices, and stabilizes several low-level conversion APIs to further elevate the systems programming experience. Let’s take a deep dive into these exciting new capabilities.

Support for extern “C” Variadics

The Problem It Solves: In previous Rust releases, developers could easily call external variadic functions written in C or other languages (such as the printf family in libc). However, there was no direct, first-class language support for defining a variadic function in Rust that could be exposed to and called by C. As a result, developers replacing low-level C libraries or writing OS drivers that implement specific C interfaces often had to rely on complex macros or inline assembly.

Starting with 1.99, Rust officially allows directly defining variadic functions with the "C" or "C-unwind" ABI. The parameter list explicitly uses ..., which maps internally in Rust to the new VaList type. This type maintains full cross-target ABI compatibility with C’s va_list structure at the low level. To guarantee memory safety, types readable from VaList are strictly guarded by the VaArgSafe trait. Furthermore, this update stabilizes support for writing naked variadic functions for non-"C" ABIs using inline assembly.

Official Example Code:

/// 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
}

fn foo() -> i32 {
    unsafe { sum(0i32, 2i32) }
}

Inspecting Memory Layout via Raw Pointers

The Problem It Solves: When pursuing maximum performance optimization or constructing custom data structures, raw pointer manipulation is inevitable. A fundamental requirement when working with raw pointers is safely obtaining the size and alignment of the data they point to. For types implementing Sized, this is straightforward; but for dynamically sized types (DSTs such as [T] or dyn Trait), fat pointer representations make obtaining layout information cumbersome and prone to undefined behavior (UB).

This release thoroughly clarifies safety requirements in this domain and stabilizes a suite of layout-querying functions specifically tailored for raw pointers:

  • Layout::for_value_raw
  • mem::size_of_val_raw
  • mem::align_of_val_raw

The stabilization of these functions means developers can pass raw pointers directly to them without first converting them into references (which can easily trigger UB when dealing with partially initialized memory or strict borrow rules). This makes low-level memory allocation, deallocation, and custom allocator implementations safer and more ergonomic than ever.

Breaking Notice: Round-Trip Unleaking of Box::leak Is Discouraged

The Problem It Solves: This is an essential behavioral specification update. In Rust’s memory management model, Box::leak is frequently used to leak dynamically allocated memory in order to obtain a mutable reference with a 'static lifetime. Historically, some developers relied on a trick: leaking memory for temporary usage and then, at the end of some lifecycle, unsafely reconstructing a Box to drop it—a practice known as “round-trip unleaking”.

However, the official documentation received a major overhaul in 1.99.0, explicitly warning developers against this anti-pattern. The compiler optimization model (particularly future LLVM optimization passes) may make aggressive assumptions known as “unreachable deallocations” upon observing Box::leak. Deallocating memory after leaking it not only breaks compiler aliasing analysis, but can also trigger unpredictable crashes once custom allocators are stabilized.

As a safer alternative, the Rust team recommends using Box::into_non_null or Box::into_raw. These two APIs perform straightforward pointer conversions without communicating “never deallocate” semantics to the compiler. This guideline applies equally to other standard library leak functions, such as String::leak.

Stabilization of Core APIs

The Problem It Solves: Beyond the major highlights above, Rust 1.99 stabilizes a wealth of highly practical standard library APIs, providing developers with more ergonomic and standardized building blocks for complex data handling.

  • Vec and Zero-Copy Decomposition: The newly stabilized Vec::into_parts and Vec::from_parts resolve longstanding safety pain points when handling C-FFI, where developers had to manually decompose a Vec into a raw pointer, length, and capacity, and subsequently reassemble it. Previously, manual unpacking and passing were vulnerable to field order mismatches or borrow check errors that caused memory leaks or dangling pointers. The new interfaces provide a well-typed, structured vehicle that strengthens code safety at FFI boundaries.
  • Lossy String Conversions: The new String::from_utf8_lossy_owned and string::FromUtf8Error::into_utf8_lossy methods offer a way to convert an already-allocated byte buffer directly into a String. If invalid UTF-8 sequences are encountered, lossy replacement occurs in place within the existing allocation, eliminating unnecessary intermediate string allocations—critical for high-performance network protocol parsers.
  • File Metadata Operations and Collection Updates: std::fs::set_times and std::fs::set_times_nofollow allow direct, granular modification of filesystem timestamps (access and modification times), filling a longstanding gap in cross-platform timestamp manipulation. In addition, the stabilization of Box::into_non_null and Box::from_non_null, alongside an IntoIterator implementation for references to Box<[T; N]>, enhances collection flexibility. VecDeque::retain_back has also officially landed in the standard library, offering an efficient reverse-filtering mechanism for double-ended queues.

Upgrade Recommendations

Here is a practical upgrade guide for different developer profiles:

Target AudienceWhen to UpgradeKey Considerations
Developers interfacing heavily with C variadic functionsUpgrade immediatelyThe new feature significantly streamlines FFI binding layers. Note that operating on ... (i.e., VaList) remains unsafe, requiring strict adherence to calling conventions and safety boundaries.
Library authors working on low-level memory management and custom allocatorsUpgrade with a smooth transitionThoroughly audit codebases for any round-trip unleaking patterns with Box::leak, and replace them with Box::into_raw or Box::into_non_null to prevent triggering undefined compiler behavior.
General application, web service, and backend developersUpgrade at regular cadenceRun $ rustup update stable for a smooth, drop-in upgrade without breaking existing code. You will immediately benefit from compiler backend improvements, faster compilation times, and standard library additions.

References