October 1, 2026·6 min read·engineeringrustffimemoryabi

Rust 1.99 can implement C variadic functions

Rust can now define C variadic entry points through VaList. The release also specifies raw-pointer layout queries and clarifies why temporary ownership transfer should use raw Box APIs instead of leak.

Rust could call a C variadic function before it could define one on stable. That asymmetry matters when replacing a C implementation while preserving an existing entry point. The Rust 1.99 announcement, published on October 1, stabilizes definitions using the C and C-unwind ABIs. The function's body can now be Rust while its variable argument list follows the platform's C convention.

The release also adds raw-pointer layout queries and revises guidance on reclaiming leaked boxes. These changes concern related boundaries between typed ownership and lower-level representations. They provide more operations in stable Rust while keeping the requirements for using those operations explicit.

My interest in Rust comes partly from its usefulness where C would otherwise be the default. Interoperation is an important part of that choice. A replacement implementation often has to preserve the caller's contract before its internal design can change. C variadic definitions remove one reason to leave an entry-point wrapper in another language.

VaList follows the platform's C argument representation

The Rust 1.99 VaList documentation describes the value created by a named ... parameter. It is ABI-compatible with the platform's C va_list, including layout. Rust initializes it automatically, corresponding to C's va_start. Reading advances its cursor through next_arg, corresponding to va_arg.

Cloning the value creates an independent cursor through the equivalent of va_copy, and dropping it performs va_end. Those operations matter when forwarding arguments to a helper or examining the list more than once. The platform representation remains the ABI's responsibility, while Rust expresses cursor management through normal value operations.

The type does not turn the variable arguments into a self-describing collection. next_arg remains unsafe because the caller must actually have supplied another argument of a compatible type. A type parameter tells the operation what to read. It does not ask the argument list to discover the next value's dynamic type.

That distinction is central to preserving a C interface. The surrounding protocol must say how many arguments exist and which types they have. A count or a format description can supply that information, but the list itself cannot prove that the caller honored it. Replacing the implementation language leaves this caller obligation in place.

The VaArgSafe documentation restricts types that can be read from the list. It guarantees support for the relevant C integer types, c_double, and raw pointers. It warns against assuming that every Rust integer spelling has the same availability on every platform.

C's default argument promotions are the reason a small source-level value cannot always be read using its original type. The documentation specifies promotion for integer types smaller than the corresponding C integer type, and promotion of c_float to c_double. Implementing VaArgSafe for a type subject to those promotions is invalid.

A floating-point reader therefore needs to account for the value passed through the variadic ABI, not merely the type the C programmer wrote before promotion. That is a concrete portability requirement. A test that happens to pass for one register layout cannot replace the documented rule about the type present in the argument list.

The trait also does not prove that a particular argument has the requested type. It establishes that the type has an appropriate variadic ABI on the target. The next_arg contract separately requires compatibility with the actual supplied argument and, for integers, representability in the relevant types. These are distinct checks at different levels of the interface.

The definition retains an unsafe boundary

The versioned function reference requires the variadic parameter to appear last and to have a binding in a function definition. A minimal reader can express its contract directly. This example requires at least one argument of type c_int.

use std::ffi::c_int;

/// # Safety
/// The caller must supply at least one c_int argument.
unsafe extern "C" fn first(mut args: ...) -> c_int {
    // SAFETY: The caller guarantees an argument of this type.
    unsafe { args.next_arg::<c_int>() }
}

The safety comment connects the unsafe operation to the interface requirement. Adding a typed wrapper for Rust callers can enforce a narrower interface before reaching this entry point. A foreign caller still has to follow the documented ABI contract. The new capability makes the implementation possible without making an untyped foreign call intrinsically safe.

The C-unwind option also belongs to the boundary design. The reference distinguishes ABIs that permit unwinding from those that do not. Selecting an ABI must agree with how the surrounding call chain handles unwinding. A function's use of variable arguments does not by itself settle that separate question.

An existing foreign interface also constrains error reporting. If its contract provides no way to report a malformed argument sequence, changing the implementation to Rust does not create that channel. The implementation must preserve the caller requirements or introduce a separately versioned interface with a different contract. This is why the practical unit of migration is the whole entry-point contract, including its failure behavior. A typed internal helper can improve the implementation without claiming that the external argument list has gained runtime type information. That distinction leaves room for an eventual safer interface while allowing the existing one to keep working for callers that satisfy its requirements. The language feature removes an implementation restriction, not the need to define the protocol.

Raw layout queries avoid requiring a reference

Rust 1.99 stabilizes Layout::for_value_raw, size_of_val_raw, and align_of_val_raw. The release announcement says the change settles their safety requirements for sized and dynamically sized types. These operations obtain layout information through raw pointers, which matters when forming a reference would impose requirements the caller does not intend to claim.

The versioned Layout contract states that a sized type is always safe for this query. For supported unsized tails such as slices, strings, and trait objects, the total size must fit in isize. It also specifies a special case for a zero-length dynamic tail and defines the tail by descending through the final field of nested structures.

These are rules for computing layout. They do not establish that a pointer is valid for every subsequent access to a value of that layout. Knowing the amount of backing storage a type requires is different from proving that live initialized storage exists at the supplied address. Keeping those claims separate allows low-level code to ask the narrower question.

This precision is useful in allocation code because metadata may describe the storage needed before a fully usable value exists. A layout query should not need to manufacture a reference merely to recover that information. The raw API gives the operation its own contract, so a reviewer can assess the query independently of later initialization or dereferencing.

Box ownership transfer should use the matching API

The release revises documentation for Box::leak without changing language semantics. It recommends against leaking a box and later reconstructing it to deallocate the memory. The announcement identifies problematic interactions with compiler optimizations and points to Box::into_raw or Box::into_non_null for ownership transfer instead.

The Rust 1.99 Box documentation says leak is mainly for data intended to remain for the rest of the program's lifetime. It describes reconstruction from the returned mutable reference as a restricted and problematic area, even with the global allocator. The recommended alternatives make the intended eventual reclamation explicit.

That difference is meaningful at an FFI boundary. A pointer handed to foreign code may represent a temporary transfer of responsibility with a later release operation. Using an ownership-transfer API describes that intention more accurately than an API whose ordinary purpose is to abandon reclamation. The later release still needs to use the correct allocation and satisfy the reconstruction requirements.

I prefer this approach to treating every way of obtaining a pointer as interchangeable. The raw address is only part of the interface. Its origin and ownership history determine what the receiving code may do with it. Rust 1.99 adds more stable ways to express those operations, while the revised documentation makes the intended history of a leaked allocation less ambiguous.

For a C-facing library, the resulting design can keep the foreign contract in a narrow entry point. Variadic reads belong there, with their count and promoted-type obligations stated next to the operation. The rest of the implementation can receive typed values and ordinary ownership. That separation preserves the existing ABI while limiting how far its untyped assumptions spread through the Rust code.