Support defining C-compatible variadic functions in Rust - #2137
Conversation
|
A bit of background: this was inspired by a co-worker who wanted to define a debugging library to intercept certain C calls, for use with |
Having the compiler enforce special lifetime rules for one function sounds really dubious. Either don't enforce lifetimes or make it work with the normal rules - either with a API that takes |
|
@comex I didn't intend it to be a "special lifetime rule", so much as the compiler simply defining and passing the appropriate lifetime. I don't really think an API based around calling a closure would work very smoothly, but I'd be open to switching to the |
|
I'd also appreciate feedback from appropriate compiler folks, regarding what solution might make the most sense from a compiler-internals point of view, in terms of preventing the type from outliving the variadic function associated with it. |
People have talked about introducing a special ie. impl<'a> VaList<'a> {
pub unsafe fn start() -> VaList<'fn>
} |
| impl<'a> Drop for VaList<'a>; | ||
|
|
||
| /// The type of arguments extractable from VaList | ||
| trait VaArg; |
| the raw C types corresponding to the Rust integer and float types above: | ||
|
|
||
| ```rust | ||
| impl VaArg for c_char; |
There was a problem hiding this comment.
Unless these type aliases became concrete types, I don't think the std and libc crates need to provide any additional impls.
| let args = VaList::start(); | ||
| let x: u8 = args::arg(); | ||
| let y: u16 = args::arg(); | ||
| let z: u32 = args::arg(); |
There was a problem hiding this comment.
You mean
let mut args = VaList::start();
let x: u8 = args.arg();
let y: u16 = args.arg();
let z: u32 = args.arg();?
There was a problem hiding this comment.
Good catch, thank you.
| ```rust | ||
| let closure = |arg, arg2, ...| { | ||
| // implementation | ||
| }; |
There was a problem hiding this comment.
You can't cast a closure to extern "C" fn, so I don't think supporting variadic closure is necessary in this RFC.
fn main() {
unsafe {
let _x = (|| {}) as extern fn(); //~ ERROR E0605
}
}
There was a problem hiding this comment.
Plus, I think extern "C" variadic functions should always be unsafe functions, and closures currently can't be unsafe.
There was a problem hiding this comment.
I didn't realize this; I'd assumed that RFC 1558 made this possible, but digging through it, it doesn't allow coercing to extern fn, only to fn. I filed rust-lang/rust#44291 to allow coercing to extern fn, but in the meantime, I'll drop this for now and move it to an open for the future. Would have been nice for more convenient callbacks.
|
The only safe way that comes to mind is requiring the function take a EDIT: there would need to be a way to mark the function as variadic, |
|
The RFC says that only |
Rust doesn't support passing a closure as an `extern "C" fn`, so these wouldn't serve any purpose yet.
|
@mark-i-m Fixed now, since libc's types are just type aliases. |
…times This allows the compiler to pass an appropriate lifetime without as much magic.
|
@eddyb OK, based on your comment, I've tried reworking the RFC so that it accepts a |
| use std::intrinsics::VaArg; | ||
|
|
||
| #[no_mangle] | ||
| pub unsafe extern "C" fn func(fixed: u32, args: ...) { |
There was a problem hiding this comment.
This should be mut args: ... then?
| Such a declaration looks like this: | ||
|
|
||
| ```rust | ||
| pub unsafe extern "C" fn func(arg: T, arg2: T2, args: ...) { |
There was a problem hiding this comment.
For symmetry, I think we should also allow
extern {
fn func(arg: u8, arg2: u16, args: ...);
// ^~~~~
}There was a problem hiding this comment.
Ah, for external variadic functions with the current mechanism?
I'm not sure that makes sense to add, since the argument isn't named in C. What would be the advantage of this?
There was a problem hiding this comment.
@joshtriplett Just for consistency, the args: has no meaning here 😄. This is really outside of the scope of this RFC, feel free to ignore it.
|
|
||
| Note that extracting an argument from a `VaList` follows the platform-specific | ||
| rules for argument passing and promotion. In particular, many platforms promote | ||
| any argument smaller than a C `int` to an `int`. On such platforms, extracting |
There was a problem hiding this comment.
This is true for all platforms, it's required by the C standard.
Basically, any integral type smaller than an int is converted to int (and any floating point type smaller than a double (basically float or short float, for implementations that provide the latter, is converted to double)).
Side note, this paragraph points out something important:
emphasis on class type and enumeration - this means that it should be valid for programs to impl VaArg for their #[repr(C|uN|iN)] structure and simple enumeration types.
I'd also argue that VaArg should be implemented for &T, &mut T, Option<&T>, and Option<&mut T>. This is all unsafe anyways :)
There was a problem hiding this comment.
I'll phrase the bit about promotions more carefully, and put some thought into the structure and enum cases.
I'd prefer not to implement VaArg for references; there are plenty of ways to convert a pointer to a reference in unsafe code, if you really want a reference, and those ways allow you to inject the appropriate lifetime of that reference more easily.
There was a problem hiding this comment.
I prefer not treating references specially - they are just pointers after all. And it just makes sense sometimes - if you're implementing printf:
#[no_mangle]
unsafe extern "C" printf(fmt: &CStr, args: ...) -> c_int {
for ch in fmt {
// ...
// found a %s:
{
print!("{}", args.next::<&CStr>());
}
}
}
There was a problem hiding this comment.
Except &CStr is a fat pointer.
There was a problem hiding this comment.
@ubsan If, at some point in the future, there's a reasonable possibility of extracting a reference directly from a variadic function argument and getting a reasonable result, then we can add such a mechanism at that time. Until then, though, I'd like to stick with only allowing raw pointers.
There was a problem hiding this comment.
@ubsan I clarified the RFC's details about promotion to take your explanation into account, and to avoid suggesting "platform-specific".
There was a problem hiding this comment.
@joshtriplett I disagree with the reasoning there. There is no reason not to allow references. You almost never actually want raw pointers when dealing with C; you want Option<&T> (and given that references are valid in C functions...)
|
|
||
| impl<'a> VaList<'a> { | ||
| /// Extract the next argument from the argument list. | ||
| pub unsafe fn arg<T: VaArg>(&mut self) -> T; |
There was a problem hiding this comment.
I don't like this name - I'd prefer next.
There was a problem hiding this comment.
I think arg was chosen to match the C name: va_arg... If we are going to deviate from this, we could change the interface altogether...
fn my_vararg_fn(args: VaList<c_int>) {...}There was a problem hiding this comment.
but you can pass any type through variable arguments, not just one type.
There was a problem hiding this comment.
This was chosen to match va_arg, yes. I intentionally didn't use next, because I don't want to give the impression that this works anything like an iterator, rather than a razor-sharp ball of unsafety.
| impl<'a> Drop for VaList<'a>; | ||
|
|
||
| /// The type of arguments extractable from VaList | ||
| unsafe trait VaArg; |
There was a problem hiding this comment.
what's the point of the VaArg trait anyways? No Rust types should be invalid to pass/take in variadic functions (although perhaps non-#[repr(C)] types shouldn't be valid? there's no technical reason though).
There was a problem hiding this comment.
+1 for replacing the trait with a #[repr(C)] check.
There was a problem hiding this comment.
@ubsan I don't think it makes sense to accept arbitrary Rust types, and in particular not references, or arbitrary structures. But accepting #[repr(C)] structures makes sense (that's possible in C), as does accepting C-style enums with a declared size.
|
@joshtriplett Not bad, I don't have any more suggestions at this time, as I believe that solves the problem of the short-lived data escaping the function. |
| [reference-level-explanation]: #reference-level-explanation | ||
|
|
||
| LLVM already provides a set of intrinsics, implementing `va_start`, `va_arg`, | ||
| `va_end`, and `va_copy`. The implementation of `VaList::start` will call the |
There was a problem hiding this comment.
Since you've changed the RFC to args: ..., the VaList::start function should no longer be mentioned. Just say va_start will be automatically inserted at the start of the function.
| name along with the `...`: | ||
|
|
||
| ```rust | ||
| pub unsafe extern "C" fn func(fixed: u32, ...args) { |
There was a problem hiding this comment.
VaList::start should now be an alternative itself. This is at best an "alternative syntax".
|
This is getting long, so I write it as the main comment here. You mentioned
This will conflict with unsafe extern fn forward_to_printf(fmt: *const c_char, ap: ...) -> c_int {
vprintf(fmt, ap)
}Here int forward_to_printf(const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
}That means impl VaList {
unsafe fn as_va_list(&mut self) -> va_list;
}so the caller still retains the ownership of unsafe extern fn forward_to_printf(fmt: *const c_char, ap: ...) -> c_int {
vprintf(fmt, ap.as_va_list())
}(Some non-standard stuff)Uniquely-borrowing the VaList instead of moving it will allow you to translate weird things like this into Rust: void just_print_one(va_list ap) {
printf("%d\n", va_arg(ap, int));
}
void print_nums(size_t n, ...) {
va_list ap;
va_start(ap, n);
for (size_t i = 0; i < n; ++ i) {
just_print_one(ap);
}
va_end(ap);
}
int main() {
print_nums(4, 1, 2, 3, 4);
}Note that this violates the C standard §7.16/3, so it is actually invalid. But this works on my machine™.
Actually, would it be even better if |
|
The RFC as accepted proposes putting (I'd like to avoid bikeshedding this if possible. It definitely has to go in |
|
@joshtriplett that seems reasonable, although I want to make sure - it's still on unstable, right?
|
|
@ubsan It's still going to be unstable until stabilized, yes. The issue was that it'd be non-trivial to ever stabilize bits of Also, I think |
|
@ubsan The |
|
@joshtriplett right, that makes sense. We really don't want to stabilize bits of |
|
I've started implementing variadic functions in rust, but some people have suggested changing the syntax to |
|
@dlrobertson Writing Could you explain more about the complexity that would create? |
|
@joshtriplett I have git branches with each implemented, just not generating the
I'm not sure what is meant by this? The fn test(fixed: i32, ap: ...VaList) -> VaList { ap }
// or even
fn test<'a>(fixed: i32, ap: ...VaList<'a>) -> VaList<'a> { ap }
The complexity is just a side point and mainly has to do with the internals of parsing in the compiler. Either way we've converged after typeck.
Adding a new |
I believe the issue is that you're writing a type that has a lifetime parameter, except it's not possible to write the correct lifetime as there is no name for it. By writing |
Good point. That could indeed be confusing. For now I'll continue with just |
|
On December 10, 2018 9:04:00 PM PST, Dan Robertson ***@***.***> wrote:
@joshtriplett I have git branches with each implemented, just not
generating the `va_start`/`va_end` yet. My main concern is the
diverging syntaxes for variadic generics and variadic arguments.
That's a feature. This is specific to extern C functions, and Rust type-safe variadics of any kind can and should be different.
|
|
I also see it as a good thing that C variadics get a syntax which is similar enough to variadic generics to be unsurprising, but still distinct enough that it's hard to get confused about which feature you're looking at. Because they are two very different and fundamentally incompatible features. |
This is easily solved by checking that the lifetime parameter of It needs to be an "universally quantified" lifetime anyway, just like @dlrobertson got a bit into the weeds of the implementation, but this is my perspective on why I think it's better for the user to write out With e.g. We could even rename (Also, a random note: |
|
That seems like revisiting the same argument that was made and explored during the RFC. Is there a new argument here that wasn't previously explored? |
|
Added a comment on rust-lang/rust#57760, but realized I should probably post here too. We need a library feature-gate for
|
|
@dlrobertson Sounds good to me, go for it. :) |
|
Just FYI @varkor fixed the issue with feature-gates so the |
…=tgross35,traviscross Stabilize c-variadic function definitions tracking issue: Fixes rust-lang#44930 reference PR: rust-lang/reference#2177 There are some minor things still in flight, but I think we're far enough along now. # Stabilization report ## Summary In C, functions can use a variable argument list `...` to accept an arbitrary number of untyped arguments. Rust is already able to call such functions (e.g. `libc::printf`), the `c_variadic` feature adds the ability to define them. A rust c-variadic function looks like this: ```rust /// SAFETY: must be called with (at least) 2 i32 arguments. unsafe extern "C" fn sum(mut args: ...) -> i32 { let a = args.next_arg::<i32>(); let b = args.next_arg::<i32>(); a + b } fn foo() -> i32 { unsafe { sum(0i32, 2i32) } } ``` This function accepts a variable arguments list `args: ...`, from which it is able to read arguments using the `next_arg` method. The main goal of defining c-variadic functions in rust is interaction with C code. Therefore it is a design goal that the rust types map directly to their C counterparts on all targets. Additionally, we disallow interaction between c-variadic functions and certain rust features that don't make much sense in an FFI context (e.g. `extern "Rust" fn` or `async fn`). ## How variadics work in C The authoritative source for how variadics (also known as "variable arguments") work in C is the C specification. In this document we'll use [section 7.16 of the final draft of the C23 standard](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf#page=302). > A function may be called with a variable number of arguments of varying types if its parameter type list ends with an ellipsis Earlier versions of C furthermore required that the `...` argument is not the first argument of the parameter type list (so at least one other argument was required). Starting in C23 this requirement has been lifted. ### C API surface - `va_list`: an opaque type that stores the information needed to read variadic arguments, or copy the list of variadic arguments. Typically values of this type get the name `ap`. - `va_start`: a `va_list` must be initialized with the `va_start` macro before it can be used. - `va_copy`: the `va_copy` macro copies a `va_list`. The copy starts at the position in the argument list of the original (so **not** at the first variadic argument to the function), and both can be moved forward independently. This means the same argument can be read multiple times. - `va_arg`: reads the next argument from the `va_ist`. - `va_end`: deinitializes a `va_list`. ### Important notes #### Not calling `va_end` is UB Section 7.16.1 > Each invocation of the `va_start` and `va_copy` macros shall be matched by a corresponding invocation of the `va_end` macro in the same function. Section 7.16.1.3: > The `va_end` macro facilitates a normal return from the function whose variable argument list was referred to by the expansion of the `va_start` macro, or the function containing the expansion of the `va_copy` macro, that initialized the `va_list` ap. The `va_end` macro may modify ap so that it is no longer usable (without being reinitialized by the `va_start` or `va_copy` macro). If there is no corresponding invocation of the `va_start` or `va_copy` macro, or if the `va_end` macro is not invoked before the return, the behavior is undefined. We believe that this behavior is this strict because some early C implementations chose to implement `va_list` like so [(source)](https://softwarepreservation.computerhistory.org/c_plus_plus/cfront/release_3.0.3/source/incl-master/proto-headers/stdarg.sol): ```rust #define va_start(ap, parmN) {\ va_buf _va;\ _vastart(ap = (va_list)_va, (char *)&parmN + sizeof parmN) #define va_end(ap) } #define va_arg(ap, mode) *((mode *)_vaarg(ap, sizeof (mode))) ``` To our knowledge no remotely-modern implementation actually implements `va_end` as anything but a no-op. #### A `va_list` may be moved Section 7.16 > The object `ap` may be passed as an argument to another function; if that function invokes the `va_arg` macro with parameter `ap`, the representation of `ap` in the calling function is indeterminate and shall be passed to the `va_end` macro prior to any further reference to `ap`. and > A pointer to a `va_list` can be created and passed to another function, in which case the original function can make further use of the original list after the other function returns So `va_list` can be moved into another function, but `va_end` must still run in the frame that initialized (with `va_start` or `va_copy`) the `va_list` . #### Representation of `va_list` The representation of `va_list` is platform-specific. There are three flavors that are used by current rust targets: - `va_list` is an opaque pointer - `va_list` is a struct - `va_list` is a single-element array, containing a struct The opaque pointer approach is the simplest to implement: the pointer just points to an array of arguments on the caller's stack. The struct and single-element array variants are more complex, but potentially more efficient because the additional state makes it possible to pass c-variadic arguments via registers. #### array-to-pointer decay If the `va_list` is of the single-element array flavor, it is subject to array-to-pointer decay: in C, arrays are passed not by-value, but as pointers. Hence, from an FFI perspective, these two functions are equivalent. ```c #include <stdarg.h> extern int foo(va_list va) { return va_arg(va, int); } extern int bar(va_list *va) { return va_arg(*va, int); } ``` Indeed, they generate the same assembly, see https://godbolt.org/z/n8c4aq5hM. #### other calling conventions Both `clang` and `gcc` refuse to compile a function that uses variadic arguments and a non-default calling convention. See also rust-lang#141618, in particular rust-lang#141618 (comment). The LLVM intrinsics (`va_start`, `va_arg`, etc.) always expand based on the default C ABI on the current platform, not the ABI of the function they are used in. While rust only supports defining C-variadic functions with the `extern "C"` ABI that is fine, but relaxing that restriction would require either changing LLVM to take the calling convention of the current function into account, or implementing `va_start` ourselves for all platforms. ### `va_arg` and argument promotion With some exceptions, the return type of a `va_arg` call must match the type of the supplied argument: Section 7.16.1 > If type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined These default argument promotions are specified in section 6.5.3.3: > The arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type. The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter, if present. The integer promotions are performed on each trailing argument, and trailing arguments that have type float are promoted to double. These are called the default argument promotions. No other conversions are performed implicitly There are a couple of additional conversions that are allowed, such as casting signed to/from unsigned integers. A concrete example of what is not allowed is to use `va_arg` to read (signed or unsigned) `char` or `short`, or `float` arguments. Reading such types is UB. See also rust-lang#61275 (comment). ## How c-variadics work in rust ### Rust API surface The rust API has similar capabilities to C but uses rust names and concepts. ```rust pub struct VaList<'f> { /* ... */ _marker: PhantomCovariantLifetime<'f>, } ``` The `VaList` struct has a lifetime parameter that ensures that the `VaList` cannot outlive the function that created it. Semantically `VaList` contains mutable references (to the caller's and/or callee's stack), so the lifetime is covariant. The `#[rustc_pass_indirectly_in_non_rustic_abis]` attribute is applied to the definition if `va_list` is a single-element array on the target platform. This attribute simulates the array-to-pointer decay that the C `va_list` type is subject to on that target. A goal of `c_variadic` is to be able to write portable code that uses c-variadics. When `va_list` is a pointer or struct FFI compatibility with a rust `VaList` is straightforward, the attribute to pass indirectly is needed to also make the types FFI compatible on targets where array-to-pointer decay kicks in. The `#[rustc_pass_indirectly_in_non_rustic_abis]` attribute works as desired because `VaList` is a by-move type: in rust this is enforced by the type system, in C the specification requires it. Mutations to the passed object are not observable in the caller (because the value is semantically moved and hence inaccessible). The `VaList::next_arg` method can be used to read the next argument. ```rust pub unsafe trait VaArgSafe: Copy + Sealed {} impl<'f> VaList<'f> { pub unsafe fn next_arg<T: VaArgSafe>(&mut self) -> T { /* ... */ } } ``` The implementation is equivalent to C `va_arg`, though for all targets that we propose to stabilize here the `va_arg` logic is implemented in `rustc` itself, and does not rely on LLVM (more information on that below). The return type is constrained by `VaArgSafe` so that only valid argument types can be read. In particular this mechanism prevents subtle issues around implicit numeric promotion in C. Reading an argument is unsafe because reading more arguments than were supplied is UB. Implementors of `VaArgSafe` must implement `Copy` because the `VaList` can be cloned and hence the same variable argument can be read twice, which is only safe if the type is `Copy`. The `VaArgSafe` trait is guaranteed to be implemented for: - `c_int`, `c_long` and `c_longlong` - `c_uint`, `c_ulong` and `c_ulonglong` - `c_double` - `*const T` and `*mut T` Implementations for other types are not guaranteed to be portable, so portable programs should not rely on e.g. `usize` or `f64` implementing this trait directly. C argument types are considered to have the rust type that corresponds to `core::ffi::*`, so a C `int` is mapped to `c_int` and so on. We don't consider the C `_BitInt` or `_Float32` types here, rather we (on most platforms) map `f64` to `double` etc. `_BitInt(32)` and `int` are distinct types. `_BitInt` is furthermore special because it does not participate in integer promotion. Calling `VaList::next_arg` to read an argument of type `T` is only safe if all of the following conditions are satisfied: - There is another c-variadic argument to read. - The actual type of the argument `U` is compatible with `T` (as defined below). - If `U` and `T` are both integer types, then the value passed by the caller must be representable in both types. Types `T` and `U` are compatible when: - `T` and `U` are the same type (up to free lifetimes). - `T` and `U` are integer types of the same size. - `T` and `U` are both pointers, and their target types are compatible. - `T` is a pointer to `c_void` and `U` is a pointer to `i8` or `u8`, or vice versa. ```rust // SAFETY: the caller must supply an argument that is compatible with `u64`, // optionally followed by other arguments that are ignored. unsafe extern "C" fn variadic(mut ap: ...) -> u64 { unsafe { ap.next_arg::<u64>() } } // The type is incompatible, Miri will report UB. unsafe { variadic(1u32) } // The value is not representable by u64, Miri will report UB. unsafe { variadic(-1i64) } // This is fine. unsafe { variadic(42i64) } ``` And an example of when equality up to free lifetimes is relevant. ```rust const unsafe extern "C" fn read_as<T: core::ffi::VaArgSafe>(mut ap: ...) -> T { ap.next_arg::<T>() } unsafe fn read_cast_lifetime() { // Allowed const { read_as::<*const &'static i32>(std::ptr::dangling::<&i32>()) }; // Not allowed const { read_as::<*const fn(&'static ())>(std::ptr::dangling::<for<'a> fn(&'a ())>()) }; //~^ ERROR va_arg type mismatch: requested `*const fn(&())` is incompatible with next argument of type `*const for<'a> fn(&'a ())` } ``` The `VaList` type implements `Clone` and `Drop`: ```rust impl<'f> Clone for VaList<'f> { /* ... */ } impl<'f> Drop for VaList<'f> { fn drop(&mut self) { /* no-op */ } } ``` The `Clone` implementation can be used to duplicate a `VaList`. The copy has the same position as the original, but both can be incremented independently. While all current targets could also implement `Copy` for `VaList`, a future target might not, so for now `Copy` is not implemented. LLVM now guarantee that `memcpy` may be used to duplicate a `VaList` when the operation is equivalent to `memcpy` on the platform, which is true for all current LLVM targets. The `Drop` implementation is a no-op, it does call a function named `va_end`, but that itself is a no-op that exists only for Miri to detect UB. In C,`va_end` must run in the frame where the `va_list` was initialized. Because `VaList` can be moved (like the C `va_list`), the frame in which a `VaList` is dropped may not be the frame in which it was initialized. LLVM now guarantees that `va_end` may be omitted when `va_end` is a no-op, which is the case for all current LLVM targets. The `VaList` type is available on all targets, even on targets that don't actually support c-variadic definitions. On such targets, it is impossible to get a valid `VaList` value, because attempting to define a c-variadic function (using the `...` argument) will throw an error. ### Syntax In rust, a C-variadic function looks like this: ```rust unsafe extern "C" fn foo(a: i32, b: i32, args: ...) { /* body */ } ``` The special `args: ...` argument stands in for an arbitrary number of arguments that the caller may pass. The `...` argument must be the last argument in the parameter list of a function. Like in C23 and later, `...` may be the only argument. The `...` syntax is already stable in foreign functions, `c_variadic` additionally allows it in function definitions. In function definitions, the `...` argument must have a pattern. The argument can be ignored by using `_: ...`. In foreign function declarations the pattern can be omitted. A function definition with a `...` argument must be an `unsafe` function. Passing an incorrect number of arguments, or arguments of the wrong type, is UB, and hence every call site has to satisfy the safety conditions. A special case is a function that ignores its `VaList` entirely using `_: ...`: we may decide to allow such functions to be safe. At the time of writing we see insufficient benefits relative to the additional complexity that this entails. A function with the `...` argument must be an `extern "C"` or `extern "C-unwind"` function. In the future we want to extend the set of accepted ABIs to include all ABIs for which we allow calling a c-variadic function (including e.g. `sysv64` and `win64`). The `...` argument can occur in definitions of functions, inherent methods, and trait methods. When any method on a trait uses a c-variadic argument, the trait is no longer dyn-compatible. The technical reason is that there is no sound way to generate a `ReifyShim` that passes on the c-variadic arguments. ### Desugaring In a function like this: ```rust unsafe extern "C" fn foo(args: ...) { // ... } ``` The `args: ...` is internally desugared into a call to LLVM's `va_start` that initializes `args` as a `VaList`. The `VaList` gets the lifetime of a local variable on `foo`'s stack, so that the `VaList` cannot outlive the function that created it. The desugaring will fail with an error when the current target does not support c-variadic definitions. Currently this is the case for `spriv` and `bpf`: ``` error: the `bpfel` target does not support c-variadic functions --> $DIR/not-supported.rs:23:31 | LL | unsafe extern "C" fn variadic(_: ...) {} | ^^^^^^ ``` ### A note on LLVM `va_arg` The LLVM `va_arg` intrinsic is known to silently miscompile. A [comment in the implementation](https://github.com/llvm/llvm-project/blob/72c8f98f74f2b4f9677d0d5e3dc91bc4d6cb39f4/clang/lib/CodeGen/ABIInfoImpl.cpp#L406-L411) notes: > This default implementation defers to the llvm backend's va_arg instruction. It can handle only passing arguments directly (typically only handled in the backend for primitive types), or aggregates passed indirectly by pointer (NOTE: if the "byval" flag has ABI impact in the callee, this implementation cannot work.) > > Only a few cases are covered here at the moment -- those needed by the default abi. Hence, like `clang`, `rustc` implements `va_arg` for the vast majority of targets (specifically including all tier-1 targets) in [`va_arg.rs`](https://github.com/rust-lang/rust/blob/master/compiler/rustc_codegen_llvm/src/va_arg.rs). Note that the match on the architecture is exhaustive, however the behavior on targets where we've not validated the correctness of the implementation is an unresolved question (see below). ## Future extensions ### C-variadics and `const fn` Support for c-variadic `const fn` (and by extension, support in Miri) is implemented in rust-lang#150601 and gated by `const_c_variadic`. Practical usage requires `const_destruct` too because `VaList` has a custom `Drop` implementation. ```rust #![feature(c_variadic, const_c_variadic, const_destruct)] const unsafe extern "C" fn variadic(mut ap: ...) -> i32 { ap.next_arg() } ``` ### Naked variadic functions Currently only `C` and `C-unwind` are valid ABIs for all c-variadic function definitions. With naked functions it is possible to define e.g. a `win64` c-variadic function in a program where `sysv64` is the default. This feature is tracked as [`c_variadic_naked_functions`](rust-lang#148767). ```rust #![feature(c_variadic, c_variadic_naked_functions)] #[unsafe(naked)] unsafe extern "win64" fn variadic_win64(_: u32, _: ...) -> u32 { core::arch::naked_asm!( r#" push rax mov qword ptr [rsp + 40], r9 mov qword ptr [rsp + 24], rdx mov qword ptr [rsp + 32], r8 lea rax, [rsp + 40] mov qword ptr [rsp], rax lea eax, [rdx + rcx] add eax, r8d pop rcx ret "#, ) } ``` ### C-variadics and coroutines An `async fn` or any other type of coroutine cannot be c-variadic. We see no reason to support this. ### Defining safe C-variadic functions In `extern` blocks, it is valid to mark C-variadic functions as safe, under the assumption that the function completely ignores the variable arguments list: ```rust unsafe extern "C" { safe fn foo(...); } ``` Normally, C-variadic function definitions must be unsafe, because calling the function with unexpected (in type or number) elements is UB. We could relax this constraint on C-variadic functions that ignore their C variable argument list, e.g.: ```rust // NOTE: not unsafe extern "C" fn(x: i32, _: ...) -> i32 { x } ``` At the moment we don't have a good reason to add this behavior. It is completely backwards compatible, so if a need arises in the future we can revisit this. ### Accepting more `va_arg` return types Discussed in rust-lang#44930 (comment). We only want to implement `VaArgSafe` for types that have a clear counterpart in C. That rules out types like `Option<NonNull>` or `NonZeroI32`. We might add `MaybeUninit<c_int>` and so on in the future if a use case comes up: this would map to a C `union`. The only planned extension right now is support for `i128` and `u128` where they can be mapped to (unsigned) `__int128`. However `rustc` currently does not know on what targets this type is available, and hence we leave it out of the stabilization for now. Adding 128-bit support is in progress in rust-lang#155429. ### C-variadic support on untested targets Because c-variadic is a platform-specific feature, and extremely unsafe, we're apprehensive about stabilizing it for targets where that implementation has not actually been validated. The [`#[c_variadic_experimental_arch]`](rust-lang#155973) feature gates such targets. The current list of targets which are kept unstable by this PR is: - `riscv32e-unknown-none-elf` because its ABI may change in the future - `sparc` because compilers for it are no longer distributed - `avr`, `m68k` and `msp430` because they are hard to validate - targets categorized as `Other`, e.g. through a custom `target.json` ### Multiple C-variadic ABIs in the same program rust-lang#141618 Both `clang` and `gcc` reject using `...` in functions with a non-default ABI for the target. That makes the layout of `VaList` and expansion of `va_start`, `va_arg` etc. unambiguous. For now we impose a similar restriction for the rust implementation. This restriction could be lifted in the future, but this would requite that `VaList` somehow "stores" its ABI. One approach is to add a type parameter to `VaList` that default's to the platform's default ABI. Each c-variadic argument would then desugar to use the ABI of the c-variadic function that creates it. ## History [RFC 2137](rust-lang/rfcs#2137) proposes to "support defining C-compatible variadic functions in rust" in 2017, and it is still the core of the implementation today. The text lays out a basic rust API and highlights potential issues (e.g. some solution is needed to match C's array-to-pointer decay), but does not always provide concrete solutions. In 2019 rust-lang#59625 introduces a wrapper type to simulate array-to-pointer decay. With this API the C semantics can be matched, but doing so correctly takes a great deal of care. The `VaList` type also has two lifetime arguments in this version, which is inelegant. Then, little seems to have happened for 6 years, until the recent burst of activity that resulted in the current proposal. - [#t-compiler > c_variadic API and ABI](https://rust-lang.zulipchat.com/#narrow/channel/131828-t-compiler/topic/c_variadic.20API.20and.20ABI/with/527115587) - rust-lang#141524 **implementation history** The list of PRs is long, but they have all been labled with [`F-c_variadic`](https://github.com/rust-lang/rust/pulls?q=is%3Apr+label%3AF-c_variadic+). ## Unresolved Questions ### `VaArgSafe` and function pointers rust-lang#153646 Currently `VaArgSafe` is not implemented for function pointers, and doing so would be tricky. There is the practical issue of not being able to be generic over the number of arguments, but there are also some complex constraints on the signature, see https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Compatible-Types.html. ### Thanks Many people have worked on this feature over the years, and many more have provided input. I'd like to credit here the people that have been especially involved in this push for stabilization: @workingjubilee, @RalfJung, @beetrees, @joshtriplett and @tgross35. r? @tgross35
…=tgross35,traviscross Stabilize c-variadic function definitions tracking issue: Fixes rust-lang#44930 reference PR: rust-lang/reference#2177 There are some minor things still in flight, but I think we're far enough along now. # Stabilization report ## Summary In C, functions can use a variable argument list `...` to accept an arbitrary number of untyped arguments. Rust is already able to call such functions (e.g. `libc::printf`), the `c_variadic` feature adds the ability to define them. A rust c-variadic function looks like this: ```rust /// SAFETY: must be called with (at least) 2 i32 arguments. unsafe extern "C" fn sum(mut args: ...) -> i32 { let a = args.next_arg::<i32>(); let b = args.next_arg::<i32>(); a + b } fn foo() -> i32 { unsafe { sum(0i32, 2i32) } } ``` This function accepts a variable arguments list `args: ...`, from which it is able to read arguments using the `next_arg` method. The main goal of defining c-variadic functions in rust is interaction with C code. Therefore it is a design goal that the rust types map directly to their C counterparts on all targets. Additionally, we disallow interaction between c-variadic functions and certain rust features that don't make much sense in an FFI context (e.g. `extern "Rust" fn` or `async fn`). ## How variadics work in C The authoritative source for how variadics (also known as "variable arguments") work in C is the C specification. In this document we'll use [section 7.16 of the final draft of the C23 standard](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf#page=302). > A function may be called with a variable number of arguments of varying types if its parameter type list ends with an ellipsis Earlier versions of C furthermore required that the `...` argument is not the first argument of the parameter type list (so at least one other argument was required). Starting in C23 this requirement has been lifted. ### C API surface - `va_list`: an opaque type that stores the information needed to read variadic arguments, or copy the list of variadic arguments. Typically values of this type get the name `ap`. - `va_start`: a `va_list` must be initialized with the `va_start` macro before it can be used. - `va_copy`: the `va_copy` macro copies a `va_list`. The copy starts at the position in the argument list of the original (so **not** at the first variadic argument to the function), and both can be moved forward independently. This means the same argument can be read multiple times. - `va_arg`: reads the next argument from the `va_ist`. - `va_end`: deinitializes a `va_list`. ### Important notes #### Not calling `va_end` is UB Section 7.16.1 > Each invocation of the `va_start` and `va_copy` macros shall be matched by a corresponding invocation of the `va_end` macro in the same function. Section 7.16.1.3: > The `va_end` macro facilitates a normal return from the function whose variable argument list was referred to by the expansion of the `va_start` macro, or the function containing the expansion of the `va_copy` macro, that initialized the `va_list` ap. The `va_end` macro may modify ap so that it is no longer usable (without being reinitialized by the `va_start` or `va_copy` macro). If there is no corresponding invocation of the `va_start` or `va_copy` macro, or if the `va_end` macro is not invoked before the return, the behavior is undefined. We believe that this behavior is this strict because some early C implementations chose to implement `va_list` like so [(source)](https://softwarepreservation.computerhistory.org/c_plus_plus/cfront/release_3.0.3/source/incl-master/proto-headers/stdarg.sol): ```rust #define va_start(ap, parmN) {\ va_buf _va;\ _vastart(ap = (va_list)_va, (char *)&parmN + sizeof parmN) #define va_end(ap) } #define va_arg(ap, mode) *((mode *)_vaarg(ap, sizeof (mode))) ``` To our knowledge no remotely-modern implementation actually implements `va_end` as anything but a no-op. #### A `va_list` may be moved Section 7.16 > The object `ap` may be passed as an argument to another function; if that function invokes the `va_arg` macro with parameter `ap`, the representation of `ap` in the calling function is indeterminate and shall be passed to the `va_end` macro prior to any further reference to `ap`. and > A pointer to a `va_list` can be created and passed to another function, in which case the original function can make further use of the original list after the other function returns So `va_list` can be moved into another function, but `va_end` must still run in the frame that initialized (with `va_start` or `va_copy`) the `va_list` . #### Representation of `va_list` The representation of `va_list` is platform-specific. There are three flavors that are used by current rust targets: - `va_list` is an opaque pointer - `va_list` is a struct - `va_list` is a single-element array, containing a struct The opaque pointer approach is the simplest to implement: the pointer just points to an array of arguments on the caller's stack. The struct and single-element array variants are more complex, but potentially more efficient because the additional state makes it possible to pass c-variadic arguments via registers. #### array-to-pointer decay If the `va_list` is of the single-element array flavor, it is subject to array-to-pointer decay: in C, arrays are passed not by-value, but as pointers. Hence, from an FFI perspective, these two functions are equivalent. ```c #include <stdarg.h> extern int foo(va_list va) { return va_arg(va, int); } extern int bar(va_list *va) { return va_arg(*va, int); } ``` Indeed, they generate the same assembly, see https://godbolt.org/z/n8c4aq5hM. #### other calling conventions Both `clang` and `gcc` refuse to compile a function that uses variadic arguments and a non-default calling convention. See also rust-lang#141618, in particular rust-lang#141618 (comment). The LLVM intrinsics (`va_start`, `va_arg`, etc.) always expand based on the default C ABI on the current platform, not the ABI of the function they are used in. While rust only supports defining C-variadic functions with the `extern "C"` ABI that is fine, but relaxing that restriction would require either changing LLVM to take the calling convention of the current function into account, or implementing `va_start` ourselves for all platforms. ### `va_arg` and argument promotion With some exceptions, the return type of a `va_arg` call must match the type of the supplied argument: Section 7.16.1 > If type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined These default argument promotions are specified in section 6.5.3.3: > The arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type. The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter, if present. The integer promotions are performed on each trailing argument, and trailing arguments that have type float are promoted to double. These are called the default argument promotions. No other conversions are performed implicitly There are a couple of additional conversions that are allowed, such as casting signed to/from unsigned integers. A concrete example of what is not allowed is to use `va_arg` to read (signed or unsigned) `char` or `short`, or `float` arguments. Reading such types is UB. See also rust-lang#61275 (comment). ## How c-variadics work in rust ### Rust API surface The rust API has similar capabilities to C but uses rust names and concepts. ```rust pub struct VaList<'f> { /* ... */ _marker: PhantomCovariantLifetime<'f>, } ``` The `VaList` struct has a lifetime parameter that ensures that the `VaList` cannot outlive the function that created it. Semantically `VaList` contains mutable references (to the caller's and/or callee's stack), so the lifetime is covariant. The `#[rustc_pass_indirectly_in_non_rustic_abis]` attribute is applied to the definition if `va_list` is a single-element array on the target platform. This attribute simulates the array-to-pointer decay that the C `va_list` type is subject to on that target. A goal of `c_variadic` is to be able to write portable code that uses c-variadics. When `va_list` is a pointer or struct FFI compatibility with a rust `VaList` is straightforward, the attribute to pass indirectly is needed to also make the types FFI compatible on targets where array-to-pointer decay kicks in. The `#[rustc_pass_indirectly_in_non_rustic_abis]` attribute works as desired because `VaList` is a by-move type: in rust this is enforced by the type system, in C the specification requires it. Mutations to the passed object are not observable in the caller (because the value is semantically moved and hence inaccessible). The `VaList::next_arg` method can be used to read the next argument. ```rust pub unsafe trait VaArgSafe: Copy + Sealed {} impl<'f> VaList<'f> { pub unsafe fn next_arg<T: VaArgSafe>(&mut self) -> T { /* ... */ } } ``` The implementation is equivalent to C `va_arg`, though for all targets that we propose to stabilize here the `va_arg` logic is implemented in `rustc` itself, and does not rely on LLVM (more information on that below). The return type is constrained by `VaArgSafe` so that only valid argument types can be read. In particular this mechanism prevents subtle issues around implicit numeric promotion in C. Reading an argument is unsafe because reading more arguments than were supplied is UB. Implementors of `VaArgSafe` must implement `Copy` because the `VaList` can be cloned and hence the same variable argument can be read twice, which is only safe if the type is `Copy`. The `VaArgSafe` trait is guaranteed to be implemented for: - `c_int`, `c_long` and `c_longlong` - `c_uint`, `c_ulong` and `c_ulonglong` - `c_double` - `*const T` and `*mut T` Implementations for other types are not guaranteed to be portable, so portable programs should not rely on e.g. `usize` or `f64` implementing this trait directly. C argument types are considered to have the rust type that corresponds to `core::ffi::*`, so a C `int` is mapped to `c_int` and so on. We don't consider the C `_BitInt` or `_Float32` types here, rather we (on most platforms) map `f64` to `double` etc. `_BitInt(32)` and `int` are distinct types. `_BitInt` is furthermore special because it does not participate in integer promotion. Calling `VaList::next_arg` to read an argument of type `T` is only safe if all of the following conditions are satisfied: - There is another c-variadic argument to read. - The actual type of the argument `U` is compatible with `T` (as defined below). - If `U` and `T` are both integer types, then the value passed by the caller must be representable in both types. Types `T` and `U` are compatible when: - `T` and `U` are the same type (up to free lifetimes). - `T` and `U` are integer types of the same size. - `T` and `U` are both pointers, and their target types are compatible. - `T` is a pointer to `c_void` and `U` is a pointer to `i8` or `u8`, or vice versa. ```rust // SAFETY: the caller must supply an argument that is compatible with `u64`, // optionally followed by other arguments that are ignored. unsafe extern "C" fn variadic(mut ap: ...) -> u64 { unsafe { ap.next_arg::<u64>() } } // The type is incompatible, Miri will report UB. unsafe { variadic(1u32) } // The value is not representable by u64, Miri will report UB. unsafe { variadic(-1i64) } // This is fine. unsafe { variadic(42i64) } ``` And an example of when equality up to free lifetimes is relevant. ```rust const unsafe extern "C" fn read_as<T: core::ffi::VaArgSafe>(mut ap: ...) -> T { ap.next_arg::<T>() } unsafe fn read_cast_lifetime() { // Allowed const { read_as::<*const &'static i32>(std::ptr::dangling::<&i32>()) }; // Not allowed const { read_as::<*const fn(&'static ())>(std::ptr::dangling::<for<'a> fn(&'a ())>()) }; //~^ ERROR va_arg type mismatch: requested `*const fn(&())` is incompatible with next argument of type `*const for<'a> fn(&'a ())` } ``` The `VaList` type implements `Clone` and `Drop`: ```rust impl<'f> Clone for VaList<'f> { /* ... */ } impl<'f> Drop for VaList<'f> { fn drop(&mut self) { /* no-op */ } } ``` The `Clone` implementation can be used to duplicate a `VaList`. The copy has the same position as the original, but both can be incremented independently. While all current targets could also implement `Copy` for `VaList`, a future target might not, so for now `Copy` is not implemented. LLVM now guarantee that `memcpy` may be used to duplicate a `VaList` when the operation is equivalent to `memcpy` on the platform, which is true for all current LLVM targets. The `Drop` implementation is a no-op, it does call a function named `va_end`, but that itself is a no-op that exists only for Miri to detect UB. In C,`va_end` must run in the frame where the `va_list` was initialized. Because `VaList` can be moved (like the C `va_list`), the frame in which a `VaList` is dropped may not be the frame in which it was initialized. LLVM now guarantees that `va_end` may be omitted when `va_end` is a no-op, which is the case for all current LLVM targets. The `VaList` type is available on all targets, even on targets that don't actually support c-variadic definitions. On such targets, it is impossible to get a valid `VaList` value, because attempting to define a c-variadic function (using the `...` argument) will throw an error. ### Syntax In rust, a C-variadic function looks like this: ```rust unsafe extern "C" fn foo(a: i32, b: i32, args: ...) { /* body */ } ``` The special `args: ...` argument stands in for an arbitrary number of arguments that the caller may pass. The `...` argument must be the last argument in the parameter list of a function. Like in C23 and later, `...` may be the only argument. The `...` syntax is already stable in foreign functions, `c_variadic` additionally allows it in function definitions. In function definitions, the `...` argument must have a pattern. The argument can be ignored by using `_: ...`. In foreign function declarations the pattern can be omitted. A function definition with a `...` argument must be an `unsafe` function. Passing an incorrect number of arguments, or arguments of the wrong type, is UB, and hence every call site has to satisfy the safety conditions. A special case is a function that ignores its `VaList` entirely using `_: ...`: we may decide to allow such functions to be safe. At the time of writing we see insufficient benefits relative to the additional complexity that this entails. A function with the `...` argument must be an `extern "C"` or `extern "C-unwind"` function. In the future we want to extend the set of accepted ABIs to include all ABIs for which we allow calling a c-variadic function (including e.g. `sysv64` and `win64`). The `...` argument can occur in definitions of functions, inherent methods, and trait methods. When any method on a trait uses a c-variadic argument, the trait is no longer dyn-compatible. The technical reason is that there is no sound way to generate a `ReifyShim` that passes on the c-variadic arguments. ### Desugaring In a function like this: ```rust unsafe extern "C" fn foo(args: ...) { // ... } ``` The `args: ...` is internally desugared into a call to LLVM's `va_start` that initializes `args` as a `VaList`. The `VaList` gets the lifetime of a local variable on `foo`'s stack, so that the `VaList` cannot outlive the function that created it. The desugaring will fail with an error when the current target does not support c-variadic definitions. Currently this is the case for `spriv` and `bpf`: ``` error: the `bpfel` target does not support c-variadic functions --> $DIR/not-supported.rs:23:31 | LL | unsafe extern "C" fn variadic(_: ...) {} | ^^^^^^ ``` ### A note on LLVM `va_arg` The LLVM `va_arg` intrinsic is known to silently miscompile. A [comment in the implementation](https://github.com/llvm/llvm-project/blob/72c8f98f74f2b4f9677d0d5e3dc91bc4d6cb39f4/clang/lib/CodeGen/ABIInfoImpl.cpp#L406-L411) notes: > This default implementation defers to the llvm backend's va_arg instruction. It can handle only passing arguments directly (typically only handled in the backend for primitive types), or aggregates passed indirectly by pointer (NOTE: if the "byval" flag has ABI impact in the callee, this implementation cannot work.) > > Only a few cases are covered here at the moment -- those needed by the default abi. Hence, like `clang`, `rustc` implements `va_arg` for the vast majority of targets (specifically including all tier-1 targets) in [`va_arg.rs`](https://github.com/rust-lang/rust/blob/master/compiler/rustc_codegen_llvm/src/va_arg.rs). Note that the match on the architecture is exhaustive, however the behavior on targets where we've not validated the correctness of the implementation is an unresolved question (see below). ## Future extensions ### C-variadics and `const fn` Support for c-variadic `const fn` (and by extension, support in Miri) is implemented in rust-lang#150601 and gated by `const_c_variadic`. Practical usage requires `const_destruct` too because `VaList` has a custom `Drop` implementation. ```rust #![feature(c_variadic, const_c_variadic, const_destruct)] const unsafe extern "C" fn variadic(mut ap: ...) -> i32 { ap.next_arg() } ``` ### Naked variadic functions Currently only `C` and `C-unwind` are valid ABIs for all c-variadic function definitions. With naked functions it is possible to define e.g. a `win64` c-variadic function in a program where `sysv64` is the default. This feature is tracked as [`c_variadic_naked_functions`](rust-lang#148767). ```rust #![feature(c_variadic, c_variadic_naked_functions)] #[unsafe(naked)] unsafe extern "win64" fn variadic_win64(_: u32, _: ...) -> u32 { core::arch::naked_asm!( r#" push rax mov qword ptr [rsp + 40], r9 mov qword ptr [rsp + 24], rdx mov qword ptr [rsp + 32], r8 lea rax, [rsp + 40] mov qword ptr [rsp], rax lea eax, [rdx + rcx] add eax, r8d pop rcx ret "#, ) } ``` ### C-variadics and coroutines An `async fn` or any other type of coroutine cannot be c-variadic. We see no reason to support this. ### Defining safe C-variadic functions In `extern` blocks, it is valid to mark C-variadic functions as safe, under the assumption that the function completely ignores the variable arguments list: ```rust unsafe extern "C" { safe fn foo(...); } ``` Normally, C-variadic function definitions must be unsafe, because calling the function with unexpected (in type or number) elements is UB. We could relax this constraint on C-variadic functions that ignore their C variable argument list, e.g.: ```rust // NOTE: not unsafe extern "C" fn(x: i32, _: ...) -> i32 { x } ``` At the moment we don't have a good reason to add this behavior. It is completely backwards compatible, so if a need arises in the future we can revisit this. ### Accepting more `va_arg` return types Discussed in rust-lang#44930 (comment). We only want to implement `VaArgSafe` for types that have a clear counterpart in C. That rules out types like `Option<NonNull>` or `NonZeroI32`. We might add `MaybeUninit<c_int>` and so on in the future if a use case comes up: this would map to a C `union`. The only planned extension right now is support for `i128` and `u128` where they can be mapped to (unsigned) `__int128`. However `rustc` currently does not know on what targets this type is available, and hence we leave it out of the stabilization for now. Adding 128-bit support is in progress in rust-lang#155429. ### C-variadic support on untested targets Because c-variadic is a platform-specific feature, and extremely unsafe, we're apprehensive about stabilizing it for targets where that implementation has not actually been validated. The [`#[c_variadic_experimental_arch]`](rust-lang#155973) feature gates such targets. The current list of targets which are kept unstable by this PR is: - `riscv32e-unknown-none-elf` because its ABI may change in the future - `sparc` because compilers for it are no longer distributed - `avr`, `m68k` and `msp430` because they are hard to validate - targets categorized as `Other`, e.g. through a custom `target.json` ### Multiple C-variadic ABIs in the same program rust-lang#141618 Both `clang` and `gcc` reject using `...` in functions with a non-default ABI for the target. That makes the layout of `VaList` and expansion of `va_start`, `va_arg` etc. unambiguous. For now we impose a similar restriction for the rust implementation. This restriction could be lifted in the future, but this would requite that `VaList` somehow "stores" its ABI. One approach is to add a type parameter to `VaList` that default's to the platform's default ABI. Each c-variadic argument would then desugar to use the ABI of the c-variadic function that creates it. ## History [RFC 2137](rust-lang/rfcs#2137) proposes to "support defining C-compatible variadic functions in rust" in 2017, and it is still the core of the implementation today. The text lays out a basic rust API and highlights potential issues (e.g. some solution is needed to match C's array-to-pointer decay), but does not always provide concrete solutions. In 2019 rust-lang#59625 introduces a wrapper type to simulate array-to-pointer decay. With this API the C semantics can be matched, but doing so correctly takes a great deal of care. The `VaList` type also has two lifetime arguments in this version, which is inelegant. Then, little seems to have happened for 6 years, until the recent burst of activity that resulted in the current proposal. - [#t-compiler > c_variadic API and ABI](https://rust-lang.zulipchat.com/#narrow/channel/131828-t-compiler/topic/c_variadic.20API.20and.20ABI/with/527115587) - rust-lang#141524 **implementation history** The list of PRs is long, but they have all been labled with [`F-c_variadic`](https://github.com/rust-lang/rust/pulls?q=is%3Apr+label%3AF-c_variadic+). ## Unresolved Questions ### `VaArgSafe` and function pointers rust-lang#153646 Currently `VaArgSafe` is not implemented for function pointers, and doing so would be tricky. There is the practical issue of not being able to be generic over the number of arguments, but there are also some complex constraints on the signature, see https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Compatible-Types.html. ### Thanks Many people have worked on this feature over the years, and many more have provided input. I'd like to credit here the people that have been especially involved in this push for stabilization: @workingjubilee, @RalfJung, @beetrees, @joshtriplett and @tgross35. r? @tgross35
…=tgross35,traviscross Stabilize c-variadic function definitions tracking issue: Fixes rust-lang#44930 reference PR: rust-lang/reference#2177 There are some minor things still in flight, but I think we're far enough along now. # Stabilization report ## Summary In C, functions can use a variable argument list `...` to accept an arbitrary number of untyped arguments. Rust is already able to call such functions (e.g. `libc::printf`), the `c_variadic` feature adds the ability to define them. A rust c-variadic function looks like this: ```rust /// SAFETY: must be called with (at least) 2 i32 arguments. unsafe extern "C" fn sum(mut args: ...) -> i32 { let a = args.next_arg::<i32>(); let b = args.next_arg::<i32>(); a + b } fn foo() -> i32 { unsafe { sum(0i32, 2i32) } } ``` This function accepts a variable arguments list `args: ...`, from which it is able to read arguments using the `next_arg` method. The main goal of defining c-variadic functions in rust is interaction with C code. Therefore it is a design goal that the rust types map directly to their C counterparts on all targets. Additionally, we disallow interaction between c-variadic functions and certain rust features that don't make much sense in an FFI context (e.g. `extern "Rust" fn` or `async fn`). ## How variadics work in C The authoritative source for how variadics (also known as "variable arguments") work in C is the C specification. In this document we'll use [section 7.16 of the final draft of the C23 standard](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf#page=302). > A function may be called with a variable number of arguments of varying types if its parameter type list ends with an ellipsis Earlier versions of C furthermore required that the `...` argument is not the first argument of the parameter type list (so at least one other argument was required). Starting in C23 this requirement has been lifted. ### C API surface - `va_list`: an opaque type that stores the information needed to read variadic arguments, or copy the list of variadic arguments. Typically values of this type get the name `ap`. - `va_start`: a `va_list` must be initialized with the `va_start` macro before it can be used. - `va_copy`: the `va_copy` macro copies a `va_list`. The copy starts at the position in the argument list of the original (so **not** at the first variadic argument to the function), and both can be moved forward independently. This means the same argument can be read multiple times. - `va_arg`: reads the next argument from the `va_ist`. - `va_end`: deinitializes a `va_list`. ### Important notes #### Not calling `va_end` is UB Section 7.16.1 > Each invocation of the `va_start` and `va_copy` macros shall be matched by a corresponding invocation of the `va_end` macro in the same function. Section 7.16.1.3: > The `va_end` macro facilitates a normal return from the function whose variable argument list was referred to by the expansion of the `va_start` macro, or the function containing the expansion of the `va_copy` macro, that initialized the `va_list` ap. The `va_end` macro may modify ap so that it is no longer usable (without being reinitialized by the `va_start` or `va_copy` macro). If there is no corresponding invocation of the `va_start` or `va_copy` macro, or if the `va_end` macro is not invoked before the return, the behavior is undefined. We believe that this behavior is this strict because some early C implementations chose to implement `va_list` like so [(source)](https://softwarepreservation.computerhistory.org/c_plus_plus/cfront/release_3.0.3/source/incl-master/proto-headers/stdarg.sol): ```rust #define va_start(ap, parmN) {\ va_buf _va;\ _vastart(ap = (va_list)_va, (char *)&parmN + sizeof parmN) #define va_end(ap) } #define va_arg(ap, mode) *((mode *)_vaarg(ap, sizeof (mode))) ``` To our knowledge no remotely-modern implementation actually implements `va_end` as anything but a no-op. #### A `va_list` may be moved Section 7.16 > The object `ap` may be passed as an argument to another function; if that function invokes the `va_arg` macro with parameter `ap`, the representation of `ap` in the calling function is indeterminate and shall be passed to the `va_end` macro prior to any further reference to `ap`. and > A pointer to a `va_list` can be created and passed to another function, in which case the original function can make further use of the original list after the other function returns So `va_list` can be moved into another function, but `va_end` must still run in the frame that initialized (with `va_start` or `va_copy`) the `va_list` . #### Representation of `va_list` The representation of `va_list` is platform-specific. There are three flavors that are used by current rust targets: - `va_list` is an opaque pointer - `va_list` is a struct - `va_list` is a single-element array, containing a struct The opaque pointer approach is the simplest to implement: the pointer just points to an array of arguments on the caller's stack. The struct and single-element array variants are more complex, but potentially more efficient because the additional state makes it possible to pass c-variadic arguments via registers. #### array-to-pointer decay If the `va_list` is of the single-element array flavor, it is subject to array-to-pointer decay: in C, arrays are passed not by-value, but as pointers. Hence, from an FFI perspective, these two functions are equivalent. ```c #include <stdarg.h> extern int foo(va_list va) { return va_arg(va, int); } extern int bar(va_list *va) { return va_arg(*va, int); } ``` Indeed, they generate the same assembly, see https://godbolt.org/z/n8c4aq5hM. #### other calling conventions Both `clang` and `gcc` refuse to compile a function that uses variadic arguments and a non-default calling convention. See also rust-lang#141618, in particular rust-lang#141618 (comment). The LLVM intrinsics (`va_start`, `va_arg`, etc.) always expand based on the default C ABI on the current platform, not the ABI of the function they are used in. While rust only supports defining C-variadic functions with the `extern "C"` ABI that is fine, but relaxing that restriction would require either changing LLVM to take the calling convention of the current function into account, or implementing `va_start` ourselves for all platforms. ### `va_arg` and argument promotion With some exceptions, the return type of a `va_arg` call must match the type of the supplied argument: Section 7.16.1 > If type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined These default argument promotions are specified in section 6.5.3.3: > The arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type. The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter, if present. The integer promotions are performed on each trailing argument, and trailing arguments that have type float are promoted to double. These are called the default argument promotions. No other conversions are performed implicitly There are a couple of additional conversions that are allowed, such as casting signed to/from unsigned integers. A concrete example of what is not allowed is to use `va_arg` to read (signed or unsigned) `char` or `short`, or `float` arguments. Reading such types is UB. See also rust-lang#61275 (comment). ## How c-variadics work in rust ### Rust API surface The rust API has similar capabilities to C but uses rust names and concepts. ```rust pub struct VaList<'f> { /* ... */ _marker: PhantomCovariantLifetime<'f>, } ``` The `VaList` struct has a lifetime parameter that ensures that the `VaList` cannot outlive the function that created it. Semantically `VaList` contains mutable references (to the caller's and/or callee's stack), so the lifetime is covariant. The `#[rustc_pass_indirectly_in_non_rustic_abis]` attribute is applied to the definition if `va_list` is a single-element array on the target platform. This attribute simulates the array-to-pointer decay that the C `va_list` type is subject to on that target. A goal of `c_variadic` is to be able to write portable code that uses c-variadics. When `va_list` is a pointer or struct FFI compatibility with a rust `VaList` is straightforward, the attribute to pass indirectly is needed to also make the types FFI compatible on targets where array-to-pointer decay kicks in. The `#[rustc_pass_indirectly_in_non_rustic_abis]` attribute works as desired because `VaList` is a by-move type: in rust this is enforced by the type system, in C the specification requires it. Mutations to the passed object are not observable in the caller (because the value is semantically moved and hence inaccessible). The `VaList::next_arg` method can be used to read the next argument. ```rust pub unsafe trait VaArgSafe: Copy + Sealed {} impl<'f> VaList<'f> { pub unsafe fn next_arg<T: VaArgSafe>(&mut self) -> T { /* ... */ } } ``` The implementation is equivalent to C `va_arg`, though for all targets that we propose to stabilize here the `va_arg` logic is implemented in `rustc` itself, and does not rely on LLVM (more information on that below). The return type is constrained by `VaArgSafe` so that only valid argument types can be read. In particular this mechanism prevents subtle issues around implicit numeric promotion in C. Reading an argument is unsafe because reading more arguments than were supplied is UB. Implementors of `VaArgSafe` must implement `Copy` because the `VaList` can be cloned and hence the same variable argument can be read twice, which is only safe if the type is `Copy`. The `VaArgSafe` trait is guaranteed to be implemented for: - `c_int`, `c_long` and `c_longlong` - `c_uint`, `c_ulong` and `c_ulonglong` - `c_double` - `*const T` and `*mut T` Implementations for other types are not guaranteed to be portable, so portable programs should not rely on e.g. `usize` or `f64` implementing this trait directly. C argument types are considered to have the rust type that corresponds to `core::ffi::*`, so a C `int` is mapped to `c_int` and so on. We don't consider the C `_BitInt` or `_Float32` types here, rather we (on most platforms) map `f64` to `double` etc. `_BitInt(32)` and `int` are distinct types. `_BitInt` is furthermore special because it does not participate in integer promotion. Calling `VaList::next_arg` to read an argument of type `T` is only safe if all of the following conditions are satisfied: - There is another c-variadic argument to read. - The actual type of the argument `U` is compatible with `T` (as defined below). - If `U` and `T` are both integer types, then the value passed by the caller must be representable in both types. Types `T` and `U` are compatible when: - `T` and `U` are the same type (up to free lifetimes). - `T` and `U` are integer types of the same size. - `T` and `U` are both pointers, and their target types are compatible. - `T` is a pointer to `c_void` and `U` is a pointer to `i8` or `u8`, or vice versa. ```rust // SAFETY: the caller must supply an argument that is compatible with `u64`, // optionally followed by other arguments that are ignored. unsafe extern "C" fn variadic(mut ap: ...) -> u64 { unsafe { ap.next_arg::<u64>() } } // The type is incompatible, Miri will report UB. unsafe { variadic(1u32) } // The value is not representable by u64, Miri will report UB. unsafe { variadic(-1i64) } // This is fine. unsafe { variadic(42i64) } ``` And an example of when equality up to free lifetimes is relevant. ```rust const unsafe extern "C" fn read_as<T: core::ffi::VaArgSafe>(mut ap: ...) -> T { ap.next_arg::<T>() } unsafe fn read_cast_lifetime() { // Allowed const { read_as::<*const &'static i32>(std::ptr::dangling::<&i32>()) }; // Not allowed const { read_as::<*const fn(&'static ())>(std::ptr::dangling::<for<'a> fn(&'a ())>()) }; //~^ ERROR va_arg type mismatch: requested `*const fn(&())` is incompatible with next argument of type `*const for<'a> fn(&'a ())` } ``` The `VaList` type implements `Clone` and `Drop`: ```rust impl<'f> Clone for VaList<'f> { /* ... */ } impl<'f> Drop for VaList<'f> { fn drop(&mut self) { /* no-op */ } } ``` The `Clone` implementation can be used to duplicate a `VaList`. The copy has the same position as the original, but both can be incremented independently. While all current targets could also implement `Copy` for `VaList`, a future target might not, so for now `Copy` is not implemented. LLVM now guarantee that `memcpy` may be used to duplicate a `VaList` when the operation is equivalent to `memcpy` on the platform, which is true for all current LLVM targets. The `Drop` implementation is a no-op, it does call a function named `va_end`, but that itself is a no-op that exists only for Miri to detect UB. In C,`va_end` must run in the frame where the `va_list` was initialized. Because `VaList` can be moved (like the C `va_list`), the frame in which a `VaList` is dropped may not be the frame in which it was initialized. LLVM now guarantees that `va_end` may be omitted when `va_end` is a no-op, which is the case for all current LLVM targets. The `VaList` type is available on all targets, even on targets that don't actually support c-variadic definitions. On such targets, it is impossible to get a valid `VaList` value, because attempting to define a c-variadic function (using the `...` argument) will throw an error. ### Syntax In rust, a C-variadic function looks like this: ```rust unsafe extern "C" fn foo(a: i32, b: i32, args: ...) { /* body */ } ``` The special `args: ...` argument stands in for an arbitrary number of arguments that the caller may pass. The `...` argument must be the last argument in the parameter list of a function. Like in C23 and later, `...` may be the only argument. The `...` syntax is already stable in foreign functions, `c_variadic` additionally allows it in function definitions. In function definitions, the `...` argument must have a pattern. The argument can be ignored by using `_: ...`. In foreign function declarations the pattern can be omitted. A function definition with a `...` argument must be an `unsafe` function. Passing an incorrect number of arguments, or arguments of the wrong type, is UB, and hence every call site has to satisfy the safety conditions. A special case is a function that ignores its `VaList` entirely using `_: ...`: we may decide to allow such functions to be safe. At the time of writing we see insufficient benefits relative to the additional complexity that this entails. A function with the `...` argument must be an `extern "C"` or `extern "C-unwind"` function. In the future we want to extend the set of accepted ABIs to include all ABIs for which we allow calling a c-variadic function (including e.g. `sysv64` and `win64`). The `...` argument can occur in definitions of functions, inherent methods, and trait methods. When any method on a trait uses a c-variadic argument, the trait is no longer dyn-compatible. The technical reason is that there is no sound way to generate a `ReifyShim` that passes on the c-variadic arguments. ### Desugaring In a function like this: ```rust unsafe extern "C" fn foo(args: ...) { // ... } ``` The `args: ...` is internally desugared into a call to LLVM's `va_start` that initializes `args` as a `VaList`. The `VaList` gets the lifetime of a local variable on `foo`'s stack, so that the `VaList` cannot outlive the function that created it. The desugaring will fail with an error when the current target does not support c-variadic definitions. Currently this is the case for `spriv` and `bpf`: ``` error: the `bpfel` target does not support c-variadic functions --> $DIR/not-supported.rs:23:31 | LL | unsafe extern "C" fn variadic(_: ...) {} | ^^^^^^ ``` ### A note on LLVM `va_arg` The LLVM `va_arg` intrinsic is known to silently miscompile. A [comment in the implementation](https://github.com/llvm/llvm-project/blob/72c8f98f74f2b4f9677d0d5e3dc91bc4d6cb39f4/clang/lib/CodeGen/ABIInfoImpl.cpp#L406-L411) notes: > This default implementation defers to the llvm backend's va_arg instruction. It can handle only passing arguments directly (typically only handled in the backend for primitive types), or aggregates passed indirectly by pointer (NOTE: if the "byval" flag has ABI impact in the callee, this implementation cannot work.) > > Only a few cases are covered here at the moment -- those needed by the default abi. Hence, like `clang`, `rustc` implements `va_arg` for the vast majority of targets (specifically including all tier-1 targets) in [`va_arg.rs`](https://github.com/rust-lang/rust/blob/master/compiler/rustc_codegen_llvm/src/va_arg.rs). Note that the match on the architecture is exhaustive, however the behavior on targets where we've not validated the correctness of the implementation is an unresolved question (see below). ## Future extensions ### C-variadics and `const fn` Support for c-variadic `const fn` (and by extension, support in Miri) is implemented in rust-lang#150601 and gated by `const_c_variadic`. Practical usage requires `const_destruct` too because `VaList` has a custom `Drop` implementation. ```rust #![feature(c_variadic, const_c_variadic, const_destruct)] const unsafe extern "C" fn variadic(mut ap: ...) -> i32 { ap.next_arg() } ``` ### Naked variadic functions Currently only `C` and `C-unwind` are valid ABIs for all c-variadic function definitions. With naked functions it is possible to define e.g. a `win64` c-variadic function in a program where `sysv64` is the default. This feature is tracked as [`c_variadic_naked_functions`](rust-lang#148767). ```rust #![feature(c_variadic, c_variadic_naked_functions)] #[unsafe(naked)] unsafe extern "win64" fn variadic_win64(_: u32, _: ...) -> u32 { core::arch::naked_asm!( r#" push rax mov qword ptr [rsp + 40], r9 mov qword ptr [rsp + 24], rdx mov qword ptr [rsp + 32], r8 lea rax, [rsp + 40] mov qword ptr [rsp], rax lea eax, [rdx + rcx] add eax, r8d pop rcx ret "#, ) } ``` ### C-variadics and coroutines An `async fn` or any other type of coroutine cannot be c-variadic. We see no reason to support this. ### Defining safe C-variadic functions In `extern` blocks, it is valid to mark C-variadic functions as safe, under the assumption that the function completely ignores the variable arguments list: ```rust unsafe extern "C" { safe fn foo(...); } ``` Normally, C-variadic function definitions must be unsafe, because calling the function with unexpected (in type or number) elements is UB. We could relax this constraint on C-variadic functions that ignore their C variable argument list, e.g.: ```rust // NOTE: not unsafe extern "C" fn(x: i32, _: ...) -> i32 { x } ``` At the moment we don't have a good reason to add this behavior. It is completely backwards compatible, so if a need arises in the future we can revisit this. ### Accepting more `va_arg` return types Discussed in rust-lang#44930 (comment). We only want to implement `VaArgSafe` for types that have a clear counterpart in C. That rules out types like `Option<NonNull>` or `NonZeroI32`. We might add `MaybeUninit<c_int>` and so on in the future if a use case comes up: this would map to a C `union`. The only planned extension right now is support for `i128` and `u128` where they can be mapped to (unsigned) `__int128`. However `rustc` currently does not know on what targets this type is available, and hence we leave it out of the stabilization for now. Adding 128-bit support is in progress in rust-lang#155429. ### C-variadic support on untested targets Because c-variadic is a platform-specific feature, and extremely unsafe, we're apprehensive about stabilizing it for targets where that implementation has not actually been validated. The [`#[c_variadic_experimental_arch]`](rust-lang#155973) feature gates such targets. The current list of targets which are kept unstable by this PR is: - `riscv32e-unknown-none-elf` because its ABI may change in the future - `sparc` because compilers for it are no longer distributed - `avr`, `m68k` and `msp430` because they are hard to validate - targets categorized as `Other`, e.g. through a custom `target.json` ### Multiple C-variadic ABIs in the same program rust-lang#141618 Both `clang` and `gcc` reject using `...` in functions with a non-default ABI for the target. That makes the layout of `VaList` and expansion of `va_start`, `va_arg` etc. unambiguous. For now we impose a similar restriction for the rust implementation. This restriction could be lifted in the future, but this would requite that `VaList` somehow "stores" its ABI. One approach is to add a type parameter to `VaList` that default's to the platform's default ABI. Each c-variadic argument would then desugar to use the ABI of the c-variadic function that creates it. ## History [RFC 2137](rust-lang/rfcs#2137) proposes to "support defining C-compatible variadic functions in rust" in 2017, and it is still the core of the implementation today. The text lays out a basic rust API and highlights potential issues (e.g. some solution is needed to match C's array-to-pointer decay), but does not always provide concrete solutions. In 2019 rust-lang#59625 introduces a wrapper type to simulate array-to-pointer decay. With this API the C semantics can be matched, but doing so correctly takes a great deal of care. The `VaList` type also has two lifetime arguments in this version, which is inelegant. Then, little seems to have happened for 6 years, until the recent burst of activity that resulted in the current proposal. - [#t-compiler > c_variadic API and ABI](https://rust-lang.zulipchat.com/#narrow/channel/131828-t-compiler/topic/c_variadic.20API.20and.20ABI/with/527115587) - rust-lang#141524 **implementation history** The list of PRs is long, but they have all been labled with [`F-c_variadic`](https://github.com/rust-lang/rust/pulls?q=is%3Apr+label%3AF-c_variadic+). ## Unresolved Questions ### `VaArgSafe` and function pointers rust-lang#153646 Currently `VaArgSafe` is not implemented for function pointers, and doing so would be tricky. There is the practical issue of not being able to be generic over the number of arguments, but there are also some complex constraints on the signature, see https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Compatible-Types.html. ### Thanks Many people have worked on this feature over the years, and many more have provided input. I'd like to credit here the people that have been especially involved in this push for stabilization: @workingjubilee, @RalfJung, @beetrees, @joshtriplett and @tgross35. r? @tgross35
…=tgross35,traviscross Stabilize c-variadic function definitions tracking issue: Fixes rust-lang#44930 reference PR: rust-lang/reference#2177 There are some minor things still in flight, but I think we're far enough along now. # Stabilization report ## Summary In C, functions can use a variable argument list `...` to accept an arbitrary number of untyped arguments. Rust is already able to call such functions (e.g. `libc::printf`), the `c_variadic` feature adds the ability to define them. A rust c-variadic function looks like this: ```rust /// SAFETY: must be called with (at least) 2 i32 arguments. unsafe extern "C" fn sum(mut args: ...) -> i32 { let a = args.next_arg::<i32>(); let b = args.next_arg::<i32>(); a + b } fn foo() -> i32 { unsafe { sum(0i32, 2i32) } } ``` This function accepts a variable arguments list `args: ...`, from which it is able to read arguments using the `next_arg` method. The main goal of defining c-variadic functions in rust is interaction with C code. Therefore it is a design goal that the rust types map directly to their C counterparts on all targets. Additionally, we disallow interaction between c-variadic functions and certain rust features that don't make much sense in an FFI context (e.g. `extern "Rust" fn` or `async fn`). ## How variadics work in C The authoritative source for how variadics (also known as "variable arguments") work in C is the C specification. In this document we'll use [section 7.16 of the final draft of the C23 standard](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf#page=302). > A function may be called with a variable number of arguments of varying types if its parameter type list ends with an ellipsis Earlier versions of C furthermore required that the `...` argument is not the first argument of the parameter type list (so at least one other argument was required). Starting in C23 this requirement has been lifted. ### C API surface - `va_list`: an opaque type that stores the information needed to read variadic arguments, or copy the list of variadic arguments. Typically values of this type get the name `ap`. - `va_start`: a `va_list` must be initialized with the `va_start` macro before it can be used. - `va_copy`: the `va_copy` macro copies a `va_list`. The copy starts at the position in the argument list of the original (so **not** at the first variadic argument to the function), and both can be moved forward independently. This means the same argument can be read multiple times. - `va_arg`: reads the next argument from the `va_ist`. - `va_end`: deinitializes a `va_list`. ### Important notes #### Not calling `va_end` is UB Section 7.16.1 > Each invocation of the `va_start` and `va_copy` macros shall be matched by a corresponding invocation of the `va_end` macro in the same function. Section 7.16.1.3: > The `va_end` macro facilitates a normal return from the function whose variable argument list was referred to by the expansion of the `va_start` macro, or the function containing the expansion of the `va_copy` macro, that initialized the `va_list` ap. The `va_end` macro may modify ap so that it is no longer usable (without being reinitialized by the `va_start` or `va_copy` macro). If there is no corresponding invocation of the `va_start` or `va_copy` macro, or if the `va_end` macro is not invoked before the return, the behavior is undefined. We believe that this behavior is this strict because some early C implementations chose to implement `va_list` like so [(source)](https://softwarepreservation.computerhistory.org/c_plus_plus/cfront/release_3.0.3/source/incl-master/proto-headers/stdarg.sol): ```rust #define va_start(ap, parmN) {\ va_buf _va;\ _vastart(ap = (va_list)_va, (char *)&parmN + sizeof parmN) #define va_end(ap) } #define va_arg(ap, mode) *((mode *)_vaarg(ap, sizeof (mode))) ``` To our knowledge no remotely-modern implementation actually implements `va_end` as anything but a no-op. #### A `va_list` may be moved Section 7.16 > The object `ap` may be passed as an argument to another function; if that function invokes the `va_arg` macro with parameter `ap`, the representation of `ap` in the calling function is indeterminate and shall be passed to the `va_end` macro prior to any further reference to `ap`. and > A pointer to a `va_list` can be created and passed to another function, in which case the original function can make further use of the original list after the other function returns So `va_list` can be moved into another function, but `va_end` must still run in the frame that initialized (with `va_start` or `va_copy`) the `va_list` . #### Representation of `va_list` The representation of `va_list` is platform-specific. There are three flavors that are used by current rust targets: - `va_list` is an opaque pointer - `va_list` is a struct - `va_list` is a single-element array, containing a struct The opaque pointer approach is the simplest to implement: the pointer just points to an array of arguments on the caller's stack. The struct and single-element array variants are more complex, but potentially more efficient because the additional state makes it possible to pass c-variadic arguments via registers. #### array-to-pointer decay If the `va_list` is of the single-element array flavor, it is subject to array-to-pointer decay: in C, arrays are passed not by-value, but as pointers. Hence, from an FFI perspective, these two functions are equivalent. ```c #include <stdarg.h> extern int foo(va_list va) { return va_arg(va, int); } extern int bar(va_list *va) { return va_arg(*va, int); } ``` Indeed, they generate the same assembly, see https://godbolt.org/z/n8c4aq5hM. #### other calling conventions Both `clang` and `gcc` refuse to compile a function that uses variadic arguments and a non-default calling convention. See also rust-lang#141618, in particular rust-lang#141618 (comment). The LLVM intrinsics (`va_start`, `va_arg`, etc.) always expand based on the default C ABI on the current platform, not the ABI of the function they are used in. While rust only supports defining C-variadic functions with the `extern "C"` ABI that is fine, but relaxing that restriction would require either changing LLVM to take the calling convention of the current function into account, or implementing `va_start` ourselves for all platforms. ### `va_arg` and argument promotion With some exceptions, the return type of a `va_arg` call must match the type of the supplied argument: Section 7.16.1 > If type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined These default argument promotions are specified in section 6.5.3.3: > The arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type. The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter, if present. The integer promotions are performed on each trailing argument, and trailing arguments that have type float are promoted to double. These are called the default argument promotions. No other conversions are performed implicitly There are a couple of additional conversions that are allowed, such as casting signed to/from unsigned integers. A concrete example of what is not allowed is to use `va_arg` to read (signed or unsigned) `char` or `short`, or `float` arguments. Reading such types is UB. See also rust-lang#61275 (comment). ## How c-variadics work in rust ### Rust API surface The rust API has similar capabilities to C but uses rust names and concepts. ```rust pub struct VaList<'f> { /* ... */ _marker: PhantomCovariantLifetime<'f>, } ``` The `VaList` struct has a lifetime parameter that ensures that the `VaList` cannot outlive the function that created it. Semantically `VaList` contains mutable references (to the caller's and/or callee's stack), so the lifetime is covariant. The `#[rustc_pass_indirectly_in_non_rustic_abis]` attribute is applied to the definition if `va_list` is a single-element array on the target platform. This attribute simulates the array-to-pointer decay that the C `va_list` type is subject to on that target. A goal of `c_variadic` is to be able to write portable code that uses c-variadics. When `va_list` is a pointer or struct FFI compatibility with a rust `VaList` is straightforward, the attribute to pass indirectly is needed to also make the types FFI compatible on targets where array-to-pointer decay kicks in. The `#[rustc_pass_indirectly_in_non_rustic_abis]` attribute works as desired because `VaList` is a by-move type: in rust this is enforced by the type system, in C the specification requires it. Mutations to the passed object are not observable in the caller (because the value is semantically moved and hence inaccessible). The `VaList::next_arg` method can be used to read the next argument. ```rust pub unsafe trait VaArgSafe: Copy + Sealed {} impl<'f> VaList<'f> { pub unsafe fn next_arg<T: VaArgSafe>(&mut self) -> T { /* ... */ } } ``` The implementation is equivalent to C `va_arg`, though for all targets that we propose to stabilize here the `va_arg` logic is implemented in `rustc` itself, and does not rely on LLVM (more information on that below). The return type is constrained by `VaArgSafe` so that only valid argument types can be read. In particular this mechanism prevents subtle issues around implicit numeric promotion in C. Reading an argument is unsafe because reading more arguments than were supplied is UB. Implementors of `VaArgSafe` must implement `Copy` because the `VaList` can be cloned and hence the same variable argument can be read twice, which is only safe if the type is `Copy`. The `VaArgSafe` trait is guaranteed to be implemented for: - `c_int`, `c_long` and `c_longlong` - `c_uint`, `c_ulong` and `c_ulonglong` - `c_double` - `*const T` and `*mut T` Implementations for other types are not guaranteed to be portable, so portable programs should not rely on e.g. `usize` or `f64` implementing this trait directly. C argument types are considered to have the rust type that corresponds to `core::ffi::*`, so a C `int` is mapped to `c_int` and so on. We don't consider the C `_BitInt` or `_Float32` types here, rather we (on most platforms) map `f64` to `double` etc. `_BitInt(32)` and `int` are distinct types. `_BitInt` is furthermore special because it does not participate in integer promotion. Calling `VaList::next_arg` to read an argument of type `T` is only safe if all of the following conditions are satisfied: - There is another c-variadic argument to read. - The actual type of the argument `U` is compatible with `T` (as defined below). - If `U` and `T` are both integer types, then the value passed by the caller must be representable in both types. Types `T` and `U` are compatible when: - `T` and `U` are the same type (up to free lifetimes). - `T` and `U` are integer types of the same size. - `T` and `U` are both pointers, and their target types are compatible. - `T` is a pointer to `c_void` and `U` is a pointer to `i8` or `u8`, or vice versa. ```rust // SAFETY: the caller must supply an argument that is compatible with `u64`, // optionally followed by other arguments that are ignored. unsafe extern "C" fn variadic(mut ap: ...) -> u64 { unsafe { ap.next_arg::<u64>() } } // The type is incompatible, Miri will report UB. unsafe { variadic(1u32) } // The value is not representable by u64, Miri will report UB. unsafe { variadic(-1i64) } // This is fine. unsafe { variadic(42i64) } ``` And an example of when equality up to free lifetimes is relevant. ```rust const unsafe extern "C" fn read_as<T: core::ffi::VaArgSafe>(mut ap: ...) -> T { ap.next_arg::<T>() } unsafe fn read_cast_lifetime() { // Allowed const { read_as::<*const &'static i32>(std::ptr::dangling::<&i32>()) }; // Not allowed const { read_as::<*const fn(&'static ())>(std::ptr::dangling::<for<'a> fn(&'a ())>()) }; //~^ ERROR va_arg type mismatch: requested `*const fn(&())` is incompatible with next argument of type `*const for<'a> fn(&'a ())` } ``` The `VaList` type implements `Clone` and `Drop`: ```rust impl<'f> Clone for VaList<'f> { /* ... */ } impl<'f> Drop for VaList<'f> { fn drop(&mut self) { /* no-op */ } } ``` The `Clone` implementation can be used to duplicate a `VaList`. The copy has the same position as the original, but both can be incremented independently. While all current targets could also implement `Copy` for `VaList`, a future target might not, so for now `Copy` is not implemented. LLVM now guarantee that `memcpy` may be used to duplicate a `VaList` when the operation is equivalent to `memcpy` on the platform, which is true for all current LLVM targets. The `Drop` implementation is a no-op, it does call a function named `va_end`, but that itself is a no-op that exists only for Miri to detect UB. In C,`va_end` must run in the frame where the `va_list` was initialized. Because `VaList` can be moved (like the C `va_list`), the frame in which a `VaList` is dropped may not be the frame in which it was initialized. LLVM now guarantees that `va_end` may be omitted when `va_end` is a no-op, which is the case for all current LLVM targets. The `VaList` type is available on all targets, even on targets that don't actually support c-variadic definitions. On such targets, it is impossible to get a valid `VaList` value, because attempting to define a c-variadic function (using the `...` argument) will throw an error. ### Syntax In rust, a C-variadic function looks like this: ```rust unsafe extern "C" fn foo(a: i32, b: i32, args: ...) { /* body */ } ``` The special `args: ...` argument stands in for an arbitrary number of arguments that the caller may pass. The `...` argument must be the last argument in the parameter list of a function. Like in C23 and later, `...` may be the only argument. The `...` syntax is already stable in foreign functions, `c_variadic` additionally allows it in function definitions. In function definitions, the `...` argument must have a pattern. The argument can be ignored by using `_: ...`. In foreign function declarations the pattern can be omitted. A function definition with a `...` argument must be an `unsafe` function. Passing an incorrect number of arguments, or arguments of the wrong type, is UB, and hence every call site has to satisfy the safety conditions. A special case is a function that ignores its `VaList` entirely using `_: ...`: we may decide to allow such functions to be safe. At the time of writing we see insufficient benefits relative to the additional complexity that this entails. A function with the `...` argument must be an `extern "C"` or `extern "C-unwind"` function. In the future we want to extend the set of accepted ABIs to include all ABIs for which we allow calling a c-variadic function (including e.g. `sysv64` and `win64`). The `...` argument can occur in definitions of functions, inherent methods, and trait methods. When any method on a trait uses a c-variadic argument, the trait is no longer dyn-compatible. The technical reason is that there is no sound way to generate a `ReifyShim` that passes on the c-variadic arguments. ### Desugaring In a function like this: ```rust unsafe extern "C" fn foo(args: ...) { // ... } ``` The `args: ...` is internally desugared into a call to LLVM's `va_start` that initializes `args` as a `VaList`. The `VaList` gets the lifetime of a local variable on `foo`'s stack, so that the `VaList` cannot outlive the function that created it. The desugaring will fail with an error when the current target does not support c-variadic definitions. Currently this is the case for `spriv` and `bpf`: ``` error: the `bpfel` target does not support c-variadic functions --> $DIR/not-supported.rs:23:31 | LL | unsafe extern "C" fn variadic(_: ...) {} | ^^^^^^ ``` ### A note on LLVM `va_arg` The LLVM `va_arg` intrinsic is known to silently miscompile. A [comment in the implementation](https://github.com/llvm/llvm-project/blob/72c8f98f74f2b4f9677d0d5e3dc91bc4d6cb39f4/clang/lib/CodeGen/ABIInfoImpl.cpp#L406-L411) notes: > This default implementation defers to the llvm backend's va_arg instruction. It can handle only passing arguments directly (typically only handled in the backend for primitive types), or aggregates passed indirectly by pointer (NOTE: if the "byval" flag has ABI impact in the callee, this implementation cannot work.) > > Only a few cases are covered here at the moment -- those needed by the default abi. Hence, like `clang`, `rustc` implements `va_arg` for the vast majority of targets (specifically including all tier-1 targets) in [`va_arg.rs`](https://github.com/rust-lang/rust/blob/master/compiler/rustc_codegen_llvm/src/va_arg.rs). Note that the match on the architecture is exhaustive, however the behavior on targets where we've not validated the correctness of the implementation is an unresolved question (see below). ## Future extensions ### C-variadics and `const fn` Support for c-variadic `const fn` (and by extension, support in Miri) is implemented in rust-lang#150601 and gated by `const_c_variadic`. Practical usage requires `const_destruct` too because `VaList` has a custom `Drop` implementation. ```rust #![feature(c_variadic, const_c_variadic, const_destruct)] const unsafe extern "C" fn variadic(mut ap: ...) -> i32 { ap.next_arg() } ``` ### Naked variadic functions Currently only `C` and `C-unwind` are valid ABIs for all c-variadic function definitions. With naked functions it is possible to define e.g. a `win64` c-variadic function in a program where `sysv64` is the default. This feature is tracked as [`c_variadic_naked_functions`](rust-lang#148767). ```rust #![feature(c_variadic, c_variadic_naked_functions)] #[unsafe(naked)] unsafe extern "win64" fn variadic_win64(_: u32, _: ...) -> u32 { core::arch::naked_asm!( r#" push rax mov qword ptr [rsp + 40], r9 mov qword ptr [rsp + 24], rdx mov qword ptr [rsp + 32], r8 lea rax, [rsp + 40] mov qword ptr [rsp], rax lea eax, [rdx + rcx] add eax, r8d pop rcx ret "#, ) } ``` ### C-variadics and coroutines An `async fn` or any other type of coroutine cannot be c-variadic. We see no reason to support this. ### Defining safe C-variadic functions In `extern` blocks, it is valid to mark C-variadic functions as safe, under the assumption that the function completely ignores the variable arguments list: ```rust unsafe extern "C" { safe fn foo(...); } ``` Normally, C-variadic function definitions must be unsafe, because calling the function with unexpected (in type or number) elements is UB. We could relax this constraint on C-variadic functions that ignore their C variable argument list, e.g.: ```rust // NOTE: not unsafe extern "C" fn(x: i32, _: ...) -> i32 { x } ``` At the moment we don't have a good reason to add this behavior. It is completely backwards compatible, so if a need arises in the future we can revisit this. ### Accepting more `va_arg` return types Discussed in rust-lang#44930 (comment). We only want to implement `VaArgSafe` for types that have a clear counterpart in C. That rules out types like `Option<NonNull>` or `NonZeroI32`. We might add `MaybeUninit<c_int>` and so on in the future if a use case comes up: this would map to a C `union`. The only planned extension right now is support for `i128` and `u128` where they can be mapped to (unsigned) `__int128`. However `rustc` currently does not know on what targets this type is available, and hence we leave it out of the stabilization for now. Adding 128-bit support is in progress in rust-lang#155429. ### C-variadic support on untested targets Because c-variadic is a platform-specific feature, and extremely unsafe, we're apprehensive about stabilizing it for targets where that implementation has not actually been validated. The [`#[c_variadic_experimental_arch]`](rust-lang#155973) feature gates such targets. The current list of targets which are kept unstable by this PR is: - `riscv32e-unknown-none-elf` because its ABI may change in the future - `sparc` because compilers for it are no longer distributed - `avr`, `m68k` and `msp430` because they are hard to validate - targets categorized as `Other`, e.g. through a custom `target.json` ### Multiple C-variadic ABIs in the same program rust-lang#141618 Both `clang` and `gcc` reject using `...` in functions with a non-default ABI for the target. That makes the layout of `VaList` and expansion of `va_start`, `va_arg` etc. unambiguous. For now we impose a similar restriction for the rust implementation. This restriction could be lifted in the future, but this would requite that `VaList` somehow "stores" its ABI. One approach is to add a type parameter to `VaList` that default's to the platform's default ABI. Each c-variadic argument would then desugar to use the ABI of the c-variadic function that creates it. ## History [RFC 2137](rust-lang/rfcs#2137) proposes to "support defining C-compatible variadic functions in rust" in 2017, and it is still the core of the implementation today. The text lays out a basic rust API and highlights potential issues (e.g. some solution is needed to match C's array-to-pointer decay), but does not always provide concrete solutions. In 2019 rust-lang#59625 introduces a wrapper type to simulate array-to-pointer decay. With this API the C semantics can be matched, but doing so correctly takes a great deal of care. The `VaList` type also has two lifetime arguments in this version, which is inelegant. Then, little seems to have happened for 6 years, until the recent burst of activity that resulted in the current proposal. - [#t-compiler > c_variadic API and ABI](https://rust-lang.zulipchat.com/#narrow/channel/131828-t-compiler/topic/c_variadic.20API.20and.20ABI/with/527115587) - rust-lang#141524 **implementation history** The list of PRs is long, but they have all been labled with [`F-c_variadic`](https://github.com/rust-lang/rust/pulls?q=is%3Apr+label%3AF-c_variadic+). ## Unresolved Questions ### `VaArgSafe` and function pointers rust-lang#153646 Currently `VaArgSafe` is not implemented for function pointers, and doing so would be tricky. There is the practical issue of not being able to be generic over the number of arguments, but there are also some complex constraints on the signature, see https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Compatible-Types.html. ### Thanks Many people have worked on this feature over the years, and many more have provided input. I'd like to credit here the people that have been especially involved in this push for stabilization: @workingjubilee, @RalfJung, @beetrees, @joshtriplett and @tgross35. r? @tgross35
Rollup merge of #155697 - folkertdev:stabilize-c-variadic, r=tgross35,traviscross Stabilize c-variadic function definitions tracking issue: Fixes #44930 reference PR: rust-lang/reference#2177 There are some minor things still in flight, but I think we're far enough along now. # Stabilization report ## Summary In C, functions can use a variable argument list `...` to accept an arbitrary number of untyped arguments. Rust is already able to call such functions (e.g. `libc::printf`), the `c_variadic` feature adds the ability to define them. A rust c-variadic function looks like this: ```rust /// SAFETY: must be called with (at least) 2 i32 arguments. unsafe extern "C" fn sum(mut args: ...) -> i32 { let a = args.next_arg::<i32>(); let b = args.next_arg::<i32>(); a + b } fn foo() -> i32 { unsafe { sum(0i32, 2i32) } } ``` This function accepts a variable arguments list `args: ...`, from which it is able to read arguments using the `next_arg` method. The main goal of defining c-variadic functions in rust is interaction with C code. Therefore it is a design goal that the rust types map directly to their C counterparts on all targets. Additionally, we disallow interaction between c-variadic functions and certain rust features that don't make much sense in an FFI context (e.g. `extern "Rust" fn` or `async fn`). ## How variadics work in C The authoritative source for how variadics (also known as "variable arguments") work in C is the C specification. In this document we'll use [section 7.16 of the final draft of the C23 standard](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf#page=302). > A function may be called with a variable number of arguments of varying types if its parameter type list ends with an ellipsis Earlier versions of C furthermore required that the `...` argument is not the first argument of the parameter type list (so at least one other argument was required). Starting in C23 this requirement has been lifted. ### C API surface - `va_list`: an opaque type that stores the information needed to read variadic arguments, or copy the list of variadic arguments. Typically values of this type get the name `ap`. - `va_start`: a `va_list` must be initialized with the `va_start` macro before it can be used. - `va_copy`: the `va_copy` macro copies a `va_list`. The copy starts at the position in the argument list of the original (so **not** at the first variadic argument to the function), and both can be moved forward independently. This means the same argument can be read multiple times. - `va_arg`: reads the next argument from the `va_ist`. - `va_end`: deinitializes a `va_list`. ### Important notes #### Not calling `va_end` is UB Section 7.16.1 > Each invocation of the `va_start` and `va_copy` macros shall be matched by a corresponding invocation of the `va_end` macro in the same function. Section 7.16.1.3: > The `va_end` macro facilitates a normal return from the function whose variable argument list was referred to by the expansion of the `va_start` macro, or the function containing the expansion of the `va_copy` macro, that initialized the `va_list` ap. The `va_end` macro may modify ap so that it is no longer usable (without being reinitialized by the `va_start` or `va_copy` macro). If there is no corresponding invocation of the `va_start` or `va_copy` macro, or if the `va_end` macro is not invoked before the return, the behavior is undefined. We believe that this behavior is this strict because some early C implementations chose to implement `va_list` like so [(source)](https://softwarepreservation.computerhistory.org/c_plus_plus/cfront/release_3.0.3/source/incl-master/proto-headers/stdarg.sol): ```rust #define va_start(ap, parmN) {\ va_buf _va;\ _vastart(ap = (va_list)_va, (char *)&parmN + sizeof parmN) #define va_end(ap) } #define va_arg(ap, mode) *((mode *)_vaarg(ap, sizeof (mode))) ``` To our knowledge no remotely-modern implementation actually implements `va_end` as anything but a no-op. #### A `va_list` may be moved Section 7.16 > The object `ap` may be passed as an argument to another function; if that function invokes the `va_arg` macro with parameter `ap`, the representation of `ap` in the calling function is indeterminate and shall be passed to the `va_end` macro prior to any further reference to `ap`. and > A pointer to a `va_list` can be created and passed to another function, in which case the original function can make further use of the original list after the other function returns So `va_list` can be moved into another function, but `va_end` must still run in the frame that initialized (with `va_start` or `va_copy`) the `va_list` . #### Representation of `va_list` The representation of `va_list` is platform-specific. There are three flavors that are used by current rust targets: - `va_list` is an opaque pointer - `va_list` is a struct - `va_list` is a single-element array, containing a struct The opaque pointer approach is the simplest to implement: the pointer just points to an array of arguments on the caller's stack. The struct and single-element array variants are more complex, but potentially more efficient because the additional state makes it possible to pass c-variadic arguments via registers. #### array-to-pointer decay If the `va_list` is of the single-element array flavor, it is subject to array-to-pointer decay: in C, arrays are passed not by-value, but as pointers. Hence, from an FFI perspective, these two functions are equivalent. ```c #include <stdarg.h> extern int foo(va_list va) { return va_arg(va, int); } extern int bar(va_list *va) { return va_arg(*va, int); } ``` Indeed, they generate the same assembly, see https://godbolt.org/z/n8c4aq5hM. #### other calling conventions Both `clang` and `gcc` refuse to compile a function that uses variadic arguments and a non-default calling convention. See also #141618, in particular #141618 (comment). The LLVM intrinsics (`va_start`, `va_arg`, etc.) always expand based on the default C ABI on the current platform, not the ABI of the function they are used in. While rust only supports defining C-variadic functions with the `extern "C"` ABI that is fine, but relaxing that restriction would require either changing LLVM to take the calling convention of the current function into account, or implementing `va_start` ourselves for all platforms. ### `va_arg` and argument promotion With some exceptions, the return type of a `va_arg` call must match the type of the supplied argument: Section 7.16.1 > If type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined These default argument promotions are specified in section 6.5.3.3: > The arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type. The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter, if present. The integer promotions are performed on each trailing argument, and trailing arguments that have type float are promoted to double. These are called the default argument promotions. No other conversions are performed implicitly There are a couple of additional conversions that are allowed, such as casting signed to/from unsigned integers. A concrete example of what is not allowed is to use `va_arg` to read (signed or unsigned) `char` or `short`, or `float` arguments. Reading such types is UB. See also #61275 (comment). ## How c-variadics work in rust ### Rust API surface The rust API has similar capabilities to C but uses rust names and concepts. ```rust pub struct VaList<'f> { /* ... */ _marker: PhantomCovariantLifetime<'f>, } ``` The `VaList` struct has a lifetime parameter that ensures that the `VaList` cannot outlive the function that created it. Semantically `VaList` contains mutable references (to the caller's and/or callee's stack), so the lifetime is covariant. The `#[rustc_pass_indirectly_in_non_rustic_abis]` attribute is applied to the definition if `va_list` is a single-element array on the target platform. This attribute simulates the array-to-pointer decay that the C `va_list` type is subject to on that target. A goal of `c_variadic` is to be able to write portable code that uses c-variadics. When `va_list` is a pointer or struct FFI compatibility with a rust `VaList` is straightforward, the attribute to pass indirectly is needed to also make the types FFI compatible on targets where array-to-pointer decay kicks in. The `#[rustc_pass_indirectly_in_non_rustic_abis]` attribute works as desired because `VaList` is a by-move type: in rust this is enforced by the type system, in C the specification requires it. Mutations to the passed object are not observable in the caller (because the value is semantically moved and hence inaccessible). The `VaList::next_arg` method can be used to read the next argument. ```rust pub unsafe trait VaArgSafe: Copy + Sealed {} impl<'f> VaList<'f> { pub unsafe fn next_arg<T: VaArgSafe>(&mut self) -> T { /* ... */ } } ``` The implementation is equivalent to C `va_arg`, though for all targets that we propose to stabilize here the `va_arg` logic is implemented in `rustc` itself, and does not rely on LLVM (more information on that below). The return type is constrained by `VaArgSafe` so that only valid argument types can be read. In particular this mechanism prevents subtle issues around implicit numeric promotion in C. Reading an argument is unsafe because reading more arguments than were supplied is UB. Implementors of `VaArgSafe` must implement `Copy` because the `VaList` can be cloned and hence the same variable argument can be read twice, which is only safe if the type is `Copy`. The `VaArgSafe` trait is guaranteed to be implemented for: - `c_int`, `c_long` and `c_longlong` - `c_uint`, `c_ulong` and `c_ulonglong` - `c_double` - `*const T` and `*mut T` Implementations for other types are not guaranteed to be portable, so portable programs should not rely on e.g. `usize` or `f64` implementing this trait directly. C argument types are considered to have the rust type that corresponds to `core::ffi::*`, so a C `int` is mapped to `c_int` and so on. We don't consider the C `_BitInt` or `_Float32` types here, rather we (on most platforms) map `f64` to `double` etc. `_BitInt(32)` and `int` are distinct types. `_BitInt` is furthermore special because it does not participate in integer promotion. Calling `VaList::next_arg` to read an argument of type `T` is only safe if all of the following conditions are satisfied: - There is another c-variadic argument to read. - The actual type of the argument `U` is compatible with `T` (as defined below). - If `U` and `T` are both integer types, then the value passed by the caller must be representable in both types. Types `T` and `U` are compatible when: - `T` and `U` are the same type (up to free lifetimes). - `T` and `U` are integer types of the same size. - `T` and `U` are both pointers, and their target types are compatible. - `T` is a pointer to `c_void` and `U` is a pointer to `i8` or `u8`, or vice versa. ```rust // SAFETY: the caller must supply an argument that is compatible with `u64`, // optionally followed by other arguments that are ignored. unsafe extern "C" fn variadic(mut ap: ...) -> u64 { unsafe { ap.next_arg::<u64>() } } // The type is incompatible, Miri will report UB. unsafe { variadic(1u32) } // The value is not representable by u64, Miri will report UB. unsafe { variadic(-1i64) } // This is fine. unsafe { variadic(42i64) } ``` And an example of when equality up to free lifetimes is relevant. ```rust const unsafe extern "C" fn read_as<T: core::ffi::VaArgSafe>(mut ap: ...) -> T { ap.next_arg::<T>() } unsafe fn read_cast_lifetime() { // Allowed const { read_as::<*const &'static i32>(std::ptr::dangling::<&i32>()) }; // Not allowed const { read_as::<*const fn(&'static ())>(std::ptr::dangling::<for<'a> fn(&'a ())>()) }; //~^ ERROR va_arg type mismatch: requested `*const fn(&())` is incompatible with next argument of type `*const for<'a> fn(&'a ())` } ``` The `VaList` type implements `Clone` and `Drop`: ```rust impl<'f> Clone for VaList<'f> { /* ... */ } impl<'f> Drop for VaList<'f> { fn drop(&mut self) { /* no-op */ } } ``` The `Clone` implementation can be used to duplicate a `VaList`. The copy has the same position as the original, but both can be incremented independently. While all current targets could also implement `Copy` for `VaList`, a future target might not, so for now `Copy` is not implemented. LLVM now guarantee that `memcpy` may be used to duplicate a `VaList` when the operation is equivalent to `memcpy` on the platform, which is true for all current LLVM targets. The `Drop` implementation is a no-op, it does call a function named `va_end`, but that itself is a no-op that exists only for Miri to detect UB. In C,`va_end` must run in the frame where the `va_list` was initialized. Because `VaList` can be moved (like the C `va_list`), the frame in which a `VaList` is dropped may not be the frame in which it was initialized. LLVM now guarantees that `va_end` may be omitted when `va_end` is a no-op, which is the case for all current LLVM targets. The `VaList` type is available on all targets, even on targets that don't actually support c-variadic definitions. On such targets, it is impossible to get a valid `VaList` value, because attempting to define a c-variadic function (using the `...` argument) will throw an error. ### Syntax In rust, a C-variadic function looks like this: ```rust unsafe extern "C" fn foo(a: i32, b: i32, args: ...) { /* body */ } ``` The special `args: ...` argument stands in for an arbitrary number of arguments that the caller may pass. The `...` argument must be the last argument in the parameter list of a function. Like in C23 and later, `...` may be the only argument. The `...` syntax is already stable in foreign functions, `c_variadic` additionally allows it in function definitions. In function definitions, the `...` argument must have a pattern. The argument can be ignored by using `_: ...`. In foreign function declarations the pattern can be omitted. A function definition with a `...` argument must be an `unsafe` function. Passing an incorrect number of arguments, or arguments of the wrong type, is UB, and hence every call site has to satisfy the safety conditions. A special case is a function that ignores its `VaList` entirely using `_: ...`: we may decide to allow such functions to be safe. At the time of writing we see insufficient benefits relative to the additional complexity that this entails. A function with the `...` argument must be an `extern "C"` or `extern "C-unwind"` function. In the future we want to extend the set of accepted ABIs to include all ABIs for which we allow calling a c-variadic function (including e.g. `sysv64` and `win64`). The `...` argument can occur in definitions of functions, inherent methods, and trait methods. When any method on a trait uses a c-variadic argument, the trait is no longer dyn-compatible. The technical reason is that there is no sound way to generate a `ReifyShim` that passes on the c-variadic arguments. ### Desugaring In a function like this: ```rust unsafe extern "C" fn foo(args: ...) { // ... } ``` The `args: ...` is internally desugared into a call to LLVM's `va_start` that initializes `args` as a `VaList`. The `VaList` gets the lifetime of a local variable on `foo`'s stack, so that the `VaList` cannot outlive the function that created it. The desugaring will fail with an error when the current target does not support c-variadic definitions. Currently this is the case for `spriv` and `bpf`: ``` error: the `bpfel` target does not support c-variadic functions --> $DIR/not-supported.rs:23:31 | LL | unsafe extern "C" fn variadic(_: ...) {} | ^^^^^^ ``` ### A note on LLVM `va_arg` The LLVM `va_arg` intrinsic is known to silently miscompile. A [comment in the implementation](https://github.com/llvm/llvm-project/blob/72c8f98f74f2b4f9677d0d5e3dc91bc4d6cb39f4/clang/lib/CodeGen/ABIInfoImpl.cpp#L406-L411) notes: > This default implementation defers to the llvm backend's va_arg instruction. It can handle only passing arguments directly (typically only handled in the backend for primitive types), or aggregates passed indirectly by pointer (NOTE: if the "byval" flag has ABI impact in the callee, this implementation cannot work.) > > Only a few cases are covered here at the moment -- those needed by the default abi. Hence, like `clang`, `rustc` implements `va_arg` for the vast majority of targets (specifically including all tier-1 targets) in [`va_arg.rs`](https://github.com/rust-lang/rust/blob/master/compiler/rustc_codegen_llvm/src/va_arg.rs). Note that the match on the architecture is exhaustive, however the behavior on targets where we've not validated the correctness of the implementation is an unresolved question (see below). ## Future extensions ### C-variadics and `const fn` Support for c-variadic `const fn` (and by extension, support in Miri) is implemented in #150601 and gated by `const_c_variadic`. Practical usage requires `const_destruct` too because `VaList` has a custom `Drop` implementation. ```rust #![feature(c_variadic, const_c_variadic, const_destruct)] const unsafe extern "C" fn variadic(mut ap: ...) -> i32 { ap.next_arg() } ``` ### Naked variadic functions Currently only `C` and `C-unwind` are valid ABIs for all c-variadic function definitions. With naked functions it is possible to define e.g. a `win64` c-variadic function in a program where `sysv64` is the default. This feature is tracked as [`c_variadic_naked_functions`](#148767). ```rust #![feature(c_variadic, c_variadic_naked_functions)] #[unsafe(naked)] unsafe extern "win64" fn variadic_win64(_: u32, _: ...) -> u32 { core::arch::naked_asm!( r#" push rax mov qword ptr [rsp + 40], r9 mov qword ptr [rsp + 24], rdx mov qword ptr [rsp + 32], r8 lea rax, [rsp + 40] mov qword ptr [rsp], rax lea eax, [rdx + rcx] add eax, r8d pop rcx ret "#, ) } ``` ### C-variadics and coroutines An `async fn` or any other type of coroutine cannot be c-variadic. We see no reason to support this. ### Defining safe C-variadic functions In `extern` blocks, it is valid to mark C-variadic functions as safe, under the assumption that the function completely ignores the variable arguments list: ```rust unsafe extern "C" { safe fn foo(...); } ``` Normally, C-variadic function definitions must be unsafe, because calling the function with unexpected (in type or number) elements is UB. We could relax this constraint on C-variadic functions that ignore their C variable argument list, e.g.: ```rust // NOTE: not unsafe extern "C" fn(x: i32, _: ...) -> i32 { x } ``` At the moment we don't have a good reason to add this behavior. It is completely backwards compatible, so if a need arises in the future we can revisit this. ### Accepting more `va_arg` return types Discussed in #44930 (comment). We only want to implement `VaArgSafe` for types that have a clear counterpart in C. That rules out types like `Option<NonNull>` or `NonZeroI32`. We might add `MaybeUninit<c_int>` and so on in the future if a use case comes up: this would map to a C `union`. The only planned extension right now is support for `i128` and `u128` where they can be mapped to (unsigned) `__int128`. However `rustc` currently does not know on what targets this type is available, and hence we leave it out of the stabilization for now. Adding 128-bit support is in progress in #155429. ### C-variadic support on untested targets Because c-variadic is a platform-specific feature, and extremely unsafe, we're apprehensive about stabilizing it for targets where that implementation has not actually been validated. The [`#[c_variadic_experimental_arch]`](#155973) feature gates such targets. The current list of targets which are kept unstable by this PR is: - `riscv32e-unknown-none-elf` because its ABI may change in the future - `sparc` because compilers for it are no longer distributed - `avr`, `m68k` and `msp430` because they are hard to validate - targets categorized as `Other`, e.g. through a custom `target.json` ### Multiple C-variadic ABIs in the same program #141618 Both `clang` and `gcc` reject using `...` in functions with a non-default ABI for the target. That makes the layout of `VaList` and expansion of `va_start`, `va_arg` etc. unambiguous. For now we impose a similar restriction for the rust implementation. This restriction could be lifted in the future, but this would requite that `VaList` somehow "stores" its ABI. One approach is to add a type parameter to `VaList` that default's to the platform's default ABI. Each c-variadic argument would then desugar to use the ABI of the c-variadic function that creates it. ## History [RFC 2137](rust-lang/rfcs#2137) proposes to "support defining C-compatible variadic functions in rust" in 2017, and it is still the core of the implementation today. The text lays out a basic rust API and highlights potential issues (e.g. some solution is needed to match C's array-to-pointer decay), but does not always provide concrete solutions. In 2019 #59625 introduces a wrapper type to simulate array-to-pointer decay. With this API the C semantics can be matched, but doing so correctly takes a great deal of care. The `VaList` type also has two lifetime arguments in this version, which is inelegant. Then, little seems to have happened for 6 years, until the recent burst of activity that resulted in the current proposal. - [#t-compiler > c_variadic API and ABI](https://rust-lang.zulipchat.com/#narrow/channel/131828-t-compiler/topic/c_variadic.20API.20and.20ABI/with/527115587) - #141524 **implementation history** The list of PRs is long, but they have all been labled with [`F-c_variadic`](https://github.com/rust-lang/rust/pulls?q=is%3Apr+label%3AF-c_variadic+). ## Unresolved Questions ### `VaArgSafe` and function pointers #153646 Currently `VaArgSafe` is not implemented for function pointers, and doing so would be tricky. There is the practical issue of not being able to be generic over the number of arguments, but there are also some complex constraints on the signature, see https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Compatible-Types.html. ### Thanks Many people have worked on this feature over the years, and many more have provided input. I'd like to credit here the people that have been especially involved in this push for stabilization: @workingjubilee, @RalfJung, @beetrees, @joshtriplett and @tgross35. r? @tgross35
…,traviscross Stabilize c-variadic function definitions tracking issue: Fixes rust-lang/rust#44930 reference PR: rust-lang/reference#2177 There are some minor things still in flight, but I think we're far enough along now. # Stabilization report ## Summary In C, functions can use a variable argument list `...` to accept an arbitrary number of untyped arguments. Rust is already able to call such functions (e.g. `libc::printf`), the `c_variadic` feature adds the ability to define them. A rust c-variadic function looks like this: ```rust /// SAFETY: must be called with (at least) 2 i32 arguments. unsafe extern "C" fn sum(mut args: ...) -> i32 { let a = args.next_arg::<i32>(); let b = args.next_arg::<i32>(); a + b } fn foo() -> i32 { unsafe { sum(0i32, 2i32) } } ``` This function accepts a variable arguments list `args: ...`, from which it is able to read arguments using the `next_arg` method. The main goal of defining c-variadic functions in rust is interaction with C code. Therefore it is a design goal that the rust types map directly to their C counterparts on all targets. Additionally, we disallow interaction between c-variadic functions and certain rust features that don't make much sense in an FFI context (e.g. `extern "Rust" fn` or `async fn`). ## How variadics work in C The authoritative source for how variadics (also known as "variable arguments") work in C is the C specification. In this document we'll use [section 7.16 of the final draft of the C23 standard](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf#page=302). > A function may be called with a variable number of arguments of varying types if its parameter type list ends with an ellipsis Earlier versions of C furthermore required that the `...` argument is not the first argument of the parameter type list (so at least one other argument was required). Starting in C23 this requirement has been lifted. ### C API surface - `va_list`: an opaque type that stores the information needed to read variadic arguments, or copy the list of variadic arguments. Typically values of this type get the name `ap`. - `va_start`: a `va_list` must be initialized with the `va_start` macro before it can be used. - `va_copy`: the `va_copy` macro copies a `va_list`. The copy starts at the position in the argument list of the original (so **not** at the first variadic argument to the function), and both can be moved forward independently. This means the same argument can be read multiple times. - `va_arg`: reads the next argument from the `va_ist`. - `va_end`: deinitializes a `va_list`. ### Important notes #### Not calling `va_end` is UB Section 7.16.1 > Each invocation of the `va_start` and `va_copy` macros shall be matched by a corresponding invocation of the `va_end` macro in the same function. Section 7.16.1.3: > The `va_end` macro facilitates a normal return from the function whose variable argument list was referred to by the expansion of the `va_start` macro, or the function containing the expansion of the `va_copy` macro, that initialized the `va_list` ap. The `va_end` macro may modify ap so that it is no longer usable (without being reinitialized by the `va_start` or `va_copy` macro). If there is no corresponding invocation of the `va_start` or `va_copy` macro, or if the `va_end` macro is not invoked before the return, the behavior is undefined. We believe that this behavior is this strict because some early C implementations chose to implement `va_list` like so [(source)](https://softwarepreservation.computerhistory.org/c_plus_plus/cfront/release_3.0.3/source/incl-master/proto-headers/stdarg.sol): ```rust #define va_start(ap, parmN) {\ va_buf _va;\ _vastart(ap = (va_list)_va, (char *)&parmN + sizeof parmN) #define va_end(ap) } #define va_arg(ap, mode) *((mode *)_vaarg(ap, sizeof (mode))) ``` To our knowledge no remotely-modern implementation actually implements `va_end` as anything but a no-op. #### A `va_list` may be moved Section 7.16 > The object `ap` may be passed as an argument to another function; if that function invokes the `va_arg` macro with parameter `ap`, the representation of `ap` in the calling function is indeterminate and shall be passed to the `va_end` macro prior to any further reference to `ap`. and > A pointer to a `va_list` can be created and passed to another function, in which case the original function can make further use of the original list after the other function returns So `va_list` can be moved into another function, but `va_end` must still run in the frame that initialized (with `va_start` or `va_copy`) the `va_list` . #### Representation of `va_list` The representation of `va_list` is platform-specific. There are three flavors that are used by current rust targets: - `va_list` is an opaque pointer - `va_list` is a struct - `va_list` is a single-element array, containing a struct The opaque pointer approach is the simplest to implement: the pointer just points to an array of arguments on the caller's stack. The struct and single-element array variants are more complex, but potentially more efficient because the additional state makes it possible to pass c-variadic arguments via registers. #### array-to-pointer decay If the `va_list` is of the single-element array flavor, it is subject to array-to-pointer decay: in C, arrays are passed not by-value, but as pointers. Hence, from an FFI perspective, these two functions are equivalent. ```c #include <stdarg.h> extern int foo(va_list va) { return va_arg(va, int); } extern int bar(va_list *va) { return va_arg(*va, int); } ``` Indeed, they generate the same assembly, see https://godbolt.org/z/n8c4aq5hM. #### other calling conventions Both `clang` and `gcc` refuse to compile a function that uses variadic arguments and a non-default calling convention. See also rust-lang/rust#141618, in particular rust-lang/rust#141618 (comment). The LLVM intrinsics (`va_start`, `va_arg`, etc.) always expand based on the default C ABI on the current platform, not the ABI of the function they are used in. While rust only supports defining C-variadic functions with the `extern "C"` ABI that is fine, but relaxing that restriction would require either changing LLVM to take the calling convention of the current function into account, or implementing `va_start` ourselves for all platforms. ### `va_arg` and argument promotion With some exceptions, the return type of a `va_arg` call must match the type of the supplied argument: Section 7.16.1 > If type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined These default argument promotions are specified in section 6.5.3.3: > The arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type. The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter, if present. The integer promotions are performed on each trailing argument, and trailing arguments that have type float are promoted to double. These are called the default argument promotions. No other conversions are performed implicitly There are a couple of additional conversions that are allowed, such as casting signed to/from unsigned integers. A concrete example of what is not allowed is to use `va_arg` to read (signed or unsigned) `char` or `short`, or `float` arguments. Reading such types is UB. See also rust-lang/rust#61275 (comment). ## How c-variadics work in rust ### Rust API surface The rust API has similar capabilities to C but uses rust names and concepts. ```rust pub struct VaList<'f> { /* ... */ _marker: PhantomCovariantLifetime<'f>, } ``` The `VaList` struct has a lifetime parameter that ensures that the `VaList` cannot outlive the function that created it. Semantically `VaList` contains mutable references (to the caller's and/or callee's stack), so the lifetime is covariant. The `#[rustc_pass_indirectly_in_non_rustic_abis]` attribute is applied to the definition if `va_list` is a single-element array on the target platform. This attribute simulates the array-to-pointer decay that the C `va_list` type is subject to on that target. A goal of `c_variadic` is to be able to write portable code that uses c-variadics. When `va_list` is a pointer or struct FFI compatibility with a rust `VaList` is straightforward, the attribute to pass indirectly is needed to also make the types FFI compatible on targets where array-to-pointer decay kicks in. The `#[rustc_pass_indirectly_in_non_rustic_abis]` attribute works as desired because `VaList` is a by-move type: in rust this is enforced by the type system, in C the specification requires it. Mutations to the passed object are not observable in the caller (because the value is semantically moved and hence inaccessible). The `VaList::next_arg` method can be used to read the next argument. ```rust pub unsafe trait VaArgSafe: Copy + Sealed {} impl<'f> VaList<'f> { pub unsafe fn next_arg<T: VaArgSafe>(&mut self) -> T { /* ... */ } } ``` The implementation is equivalent to C `va_arg`, though for all targets that we propose to stabilize here the `va_arg` logic is implemented in `rustc` itself, and does not rely on LLVM (more information on that below). The return type is constrained by `VaArgSafe` so that only valid argument types can be read. In particular this mechanism prevents subtle issues around implicit numeric promotion in C. Reading an argument is unsafe because reading more arguments than were supplied is UB. Implementors of `VaArgSafe` must implement `Copy` because the `VaList` can be cloned and hence the same variable argument can be read twice, which is only safe if the type is `Copy`. The `VaArgSafe` trait is guaranteed to be implemented for: - `c_int`, `c_long` and `c_longlong` - `c_uint`, `c_ulong` and `c_ulonglong` - `c_double` - `*const T` and `*mut T` Implementations for other types are not guaranteed to be portable, so portable programs should not rely on e.g. `usize` or `f64` implementing this trait directly. C argument types are considered to have the rust type that corresponds to `core::ffi::*`, so a C `int` is mapped to `c_int` and so on. We don't consider the C `_BitInt` or `_Float32` types here, rather we (on most platforms) map `f64` to `double` etc. `_BitInt(32)` and `int` are distinct types. `_BitInt` is furthermore special because it does not participate in integer promotion. Calling `VaList::next_arg` to read an argument of type `T` is only safe if all of the following conditions are satisfied: - There is another c-variadic argument to read. - The actual type of the argument `U` is compatible with `T` (as defined below). - If `U` and `T` are both integer types, then the value passed by the caller must be representable in both types. Types `T` and `U` are compatible when: - `T` and `U` are the same type (up to free lifetimes). - `T` and `U` are integer types of the same size. - `T` and `U` are both pointers, and their target types are compatible. - `T` is a pointer to `c_void` and `U` is a pointer to `i8` or `u8`, or vice versa. ```rust // SAFETY: the caller must supply an argument that is compatible with `u64`, // optionally followed by other arguments that are ignored. unsafe extern "C" fn variadic(mut ap: ...) -> u64 { unsafe { ap.next_arg::<u64>() } } // The type is incompatible, Miri will report UB. unsafe { variadic(1u32) } // The value is not representable by u64, Miri will report UB. unsafe { variadic(-1i64) } // This is fine. unsafe { variadic(42i64) } ``` And an example of when equality up to free lifetimes is relevant. ```rust const unsafe extern "C" fn read_as<T: core::ffi::VaArgSafe>(mut ap: ...) -> T { ap.next_arg::<T>() } unsafe fn read_cast_lifetime() { // Allowed const { read_as::<*const &'static i32>(std::ptr::dangling::<&i32>()) }; // Not allowed const { read_as::<*const fn(&'static ())>(std::ptr::dangling::<for<'a> fn(&'a ())>()) }; //~^ ERROR va_arg type mismatch: requested `*const fn(&())` is incompatible with next argument of type `*const for<'a> fn(&'a ())` } ``` The `VaList` type implements `Clone` and `Drop`: ```rust impl<'f> Clone for VaList<'f> { /* ... */ } impl<'f> Drop for VaList<'f> { fn drop(&mut self) { /* no-op */ } } ``` The `Clone` implementation can be used to duplicate a `VaList`. The copy has the same position as the original, but both can be incremented independently. While all current targets could also implement `Copy` for `VaList`, a future target might not, so for now `Copy` is not implemented. LLVM now guarantee that `memcpy` may be used to duplicate a `VaList` when the operation is equivalent to `memcpy` on the platform, which is true for all current LLVM targets. The `Drop` implementation is a no-op, it does call a function named `va_end`, but that itself is a no-op that exists only for Miri to detect UB. In C,`va_end` must run in the frame where the `va_list` was initialized. Because `VaList` can be moved (like the C `va_list`), the frame in which a `VaList` is dropped may not be the frame in which it was initialized. LLVM now guarantees that `va_end` may be omitted when `va_end` is a no-op, which is the case for all current LLVM targets. The `VaList` type is available on all targets, even on targets that don't actually support c-variadic definitions. On such targets, it is impossible to get a valid `VaList` value, because attempting to define a c-variadic function (using the `...` argument) will throw an error. ### Syntax In rust, a C-variadic function looks like this: ```rust unsafe extern "C" fn foo(a: i32, b: i32, args: ...) { /* body */ } ``` The special `args: ...` argument stands in for an arbitrary number of arguments that the caller may pass. The `...` argument must be the last argument in the parameter list of a function. Like in C23 and later, `...` may be the only argument. The `...` syntax is already stable in foreign functions, `c_variadic` additionally allows it in function definitions. In function definitions, the `...` argument must have a pattern. The argument can be ignored by using `_: ...`. In foreign function declarations the pattern can be omitted. A function definition with a `...` argument must be an `unsafe` function. Passing an incorrect number of arguments, or arguments of the wrong type, is UB, and hence every call site has to satisfy the safety conditions. A special case is a function that ignores its `VaList` entirely using `_: ...`: we may decide to allow such functions to be safe. At the time of writing we see insufficient benefits relative to the additional complexity that this entails. A function with the `...` argument must be an `extern "C"` or `extern "C-unwind"` function. In the future we want to extend the set of accepted ABIs to include all ABIs for which we allow calling a c-variadic function (including e.g. `sysv64` and `win64`). The `...` argument can occur in definitions of functions, inherent methods, and trait methods. When any method on a trait uses a c-variadic argument, the trait is no longer dyn-compatible. The technical reason is that there is no sound way to generate a `ReifyShim` that passes on the c-variadic arguments. ### Desugaring In a function like this: ```rust unsafe extern "C" fn foo(args: ...) { // ... } ``` The `args: ...` is internally desugared into a call to LLVM's `va_start` that initializes `args` as a `VaList`. The `VaList` gets the lifetime of a local variable on `foo`'s stack, so that the `VaList` cannot outlive the function that created it. The desugaring will fail with an error when the current target does not support c-variadic definitions. Currently this is the case for `spriv` and `bpf`: ``` error: the `bpfel` target does not support c-variadic functions --> $DIR/not-supported.rs:23:31 | LL | unsafe extern "C" fn variadic(_: ...) {} | ^^^^^^ ``` ### A note on LLVM `va_arg` The LLVM `va_arg` intrinsic is known to silently miscompile. A [comment in the implementation](https://github.com/llvm/llvm-project/blob/72c8f98f74f2b4f9677d0d5e3dc91bc4d6cb39f4/clang/lib/CodeGen/ABIInfoImpl.cpp#L406-L411) notes: > This default implementation defers to the llvm backend's va_arg instruction. It can handle only passing arguments directly (typically only handled in the backend for primitive types), or aggregates passed indirectly by pointer (NOTE: if the "byval" flag has ABI impact in the callee, this implementation cannot work.) > > Only a few cases are covered here at the moment -- those needed by the default abi. Hence, like `clang`, `rustc` implements `va_arg` for the vast majority of targets (specifically including all tier-1 targets) in [`va_arg.rs`](https://github.com/rust-lang/rust/blob/master/compiler/rustc_codegen_llvm/src/va_arg.rs). Note that the match on the architecture is exhaustive, however the behavior on targets where we've not validated the correctness of the implementation is an unresolved question (see below). ## Future extensions ### C-variadics and `const fn` Support for c-variadic `const fn` (and by extension, support in Miri) is implemented in rust-lang/rust#150601 and gated by `const_c_variadic`. Practical usage requires `const_destruct` too because `VaList` has a custom `Drop` implementation. ```rust #![feature(c_variadic, const_c_variadic, const_destruct)] const unsafe extern "C" fn variadic(mut ap: ...) -> i32 { ap.next_arg() } ``` ### Naked variadic functions Currently only `C` and `C-unwind` are valid ABIs for all c-variadic function definitions. With naked functions it is possible to define e.g. a `win64` c-variadic function in a program where `sysv64` is the default. This feature is tracked as [`c_variadic_naked_functions`](rust-lang/rust#148767). ```rust #![feature(c_variadic, c_variadic_naked_functions)] #[unsafe(naked)] unsafe extern "win64" fn variadic_win64(_: u32, _: ...) -> u32 { core::arch::naked_asm!( r#" push rax mov qword ptr [rsp + 40], r9 mov qword ptr [rsp + 24], rdx mov qword ptr [rsp + 32], r8 lea rax, [rsp + 40] mov qword ptr [rsp], rax lea eax, [rdx + rcx] add eax, r8d pop rcx ret "#, ) } ``` ### C-variadics and coroutines An `async fn` or any other type of coroutine cannot be c-variadic. We see no reason to support this. ### Defining safe C-variadic functions In `extern` blocks, it is valid to mark C-variadic functions as safe, under the assumption that the function completely ignores the variable arguments list: ```rust unsafe extern "C" { safe fn foo(...); } ``` Normally, C-variadic function definitions must be unsafe, because calling the function with unexpected (in type or number) elements is UB. We could relax this constraint on C-variadic functions that ignore their C variable argument list, e.g.: ```rust // NOTE: not unsafe extern "C" fn(x: i32, _: ...) -> i32 { x } ``` At the moment we don't have a good reason to add this behavior. It is completely backwards compatible, so if a need arises in the future we can revisit this. ### Accepting more `va_arg` return types Discussed in rust-lang/rust#44930 (comment). We only want to implement `VaArgSafe` for types that have a clear counterpart in C. That rules out types like `Option<NonNull>` or `NonZeroI32`. We might add `MaybeUninit<c_int>` and so on in the future if a use case comes up: this would map to a C `union`. The only planned extension right now is support for `i128` and `u128` where they can be mapped to (unsigned) `__int128`. However `rustc` currently does not know on what targets this type is available, and hence we leave it out of the stabilization for now. Adding 128-bit support is in progress in rust-lang/rust#155429. ### C-variadic support on untested targets Because c-variadic is a platform-specific feature, and extremely unsafe, we're apprehensive about stabilizing it for targets where that implementation has not actually been validated. The [`#[c_variadic_experimental_arch]`](rust-lang/rust#155973) feature gates such targets. The current list of targets which are kept unstable by this PR is: - `riscv32e-unknown-none-elf` because its ABI may change in the future - `sparc` because compilers for it are no longer distributed - `avr`, `m68k` and `msp430` because they are hard to validate - targets categorized as `Other`, e.g. through a custom `target.json` ### Multiple C-variadic ABIs in the same program rust-lang/rust#141618 Both `clang` and `gcc` reject using `...` in functions with a non-default ABI for the target. That makes the layout of `VaList` and expansion of `va_start`, `va_arg` etc. unambiguous. For now we impose a similar restriction for the rust implementation. This restriction could be lifted in the future, but this would requite that `VaList` somehow "stores" its ABI. One approach is to add a type parameter to `VaList` that default's to the platform's default ABI. Each c-variadic argument would then desugar to use the ABI of the c-variadic function that creates it. ## History [RFC 2137](rust-lang/rfcs#2137) proposes to "support defining C-compatible variadic functions in rust" in 2017, and it is still the core of the implementation today. The text lays out a basic rust API and highlights potential issues (e.g. some solution is needed to match C's array-to-pointer decay), but does not always provide concrete solutions. In 2019 rust-lang/rust#59625 introduces a wrapper type to simulate array-to-pointer decay. With this API the C semantics can be matched, but doing so correctly takes a great deal of care. The `VaList` type also has two lifetime arguments in this version, which is inelegant. Then, little seems to have happened for 6 years, until the recent burst of activity that resulted in the current proposal. - [#t-compiler > c_variadic API and ABI](https://rust-lang.zulipchat.com/#narrow/channel/131828-t-compiler/topic/c_variadic.20API.20and.20ABI/with/527115587) - rust-lang/rust#141524 **implementation history** The list of PRs is long, but they have all been labled with [`F-c_variadic`](https://github.com/rust-lang/rust/pulls?q=is%3Apr+label%3AF-c_variadic+). ## Unresolved Questions ### `VaArgSafe` and function pointers rust-lang/rust#153646 Currently `VaArgSafe` is not implemented for function pointers, and doing so would be tricky. There is the practical issue of not being able to be generic over the number of arguments, but there are also some complex constraints on the signature, see https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Compatible-Types.html. ### Thanks Many people have worked on this feature over the years, and many more have provided input. I'd like to credit here the people that have been especially involved in this push for stabilization: @workingjubilee, @RalfJung, @beetrees, @joshtriplett and @tgross35. r? @tgross35
Support defining C-compatible variadic functions in Rust, via new intrinsics.
Rust currently supports declaring external variadic functions and calling them
from unsafe code, but does not support writing such functions directly in Rust.
Adding such support will allow Rust to replace a larger variety of C libraries,
avoid requiring C stubs and error-prone reimplementation of platform-specific
code, improve incremental translation of C codebases to Rust, and allow
implementation of variadic callbacks.
This RFC does not propose an interface intended for native Rust code to pass
variable numbers of arguments to a native Rust function, nor an interface that
provides any kind of type safety. This proposal exists primarily to allow Rust
to provide interfaces callable from C code.
Rendered