-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support defining C-compatible variadic functions in Rust #2137
Changes from 20 commits
019dff4
3039f17
12eb69f
e6b5cfb
e9fe291
247991e
0cca2ce
3161c45
7b7b3ff
9d060c4
3769670
63b5545
5437ab2
7bd8d7d
96f80ac
18a8b36
7e5698e
105f764
6bdb95c
1546c82
eca3eae
e3d1f5c
b2acc33
4042834
eb9c392
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,250 @@ | ||
- Feature Name: variadic | ||
- Start Date: 2017-08-21 | ||
- RFC PR: (leave this empty) | ||
- Rust Issue: (leave this empty) | ||
|
||
# Summary | ||
[summary]: #summary | ||
|
||
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. | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
Rust can currently call any possible C interface, and export *almost* any | ||
interface for C to call. Variadic functions represent one of the last remaining | ||
gaps in the latter. Currently, providing a variadic function callable from C | ||
requires writing a stub function in C, linking that function into the Rust | ||
program, and arranging for that stub to subsequently call into Rust. | ||
Furthermore, even with the arguments packaged into a `va_list` structure by C | ||
code, extracting arguments from that structure requires exceptionally | ||
error-prone, platform-specific code, for which the crates.io ecosystem provides | ||
only partial solutions for a few target architectures. | ||
|
||
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. | ||
|
||
# Guide-level explanation | ||
[guide-level-explanation]: #guide-level-explanation | ||
|
||
C code allows declaring a function callable with a variable number of | ||
arguments, using an ellipsis (`...`) at the end of the argument list. For | ||
compatibility, unsafe Rust code may export a function compatible with this | ||
mechanism. | ||
|
||
Such a declaration looks like this: | ||
|
||
```rust | ||
pub unsafe extern "C" fn func(arg: T, arg2: T2, mut args: ...) { | ||
// implementation | ||
} | ||
``` | ||
|
||
The use of `...` as the type of `args` at the end of the argument list declares | ||
the function as variadic. This must appear as the last argument of the | ||
function. The function must use `extern "C"`, and must use `unsafe`. To expose | ||
such a function as a symbol for C code to call directly, the function may want | ||
to use `#[no_mangle]` as well; however, Rust code may also pass the function to | ||
C code expecting a function pointer to a variadic function. | ||
|
||
The `args` named in the function declaration has the type | ||
`core::intrinsics::VaList<'a>`, where the compiler supplies a lifetime `'a` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should a feature with its own syntax use a type provided by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @xfix We can always define a different meaning for the |
||
that prevents the arguments from outliving the variadic function. | ||
|
||
To access the arguments, Rust provides the following public interfaces in | ||
`core::intrinsics` (also available via `std::intrinsics`): | ||
|
||
```rust | ||
/// The argument list of a C-compatible variadic function, corresponding to the | ||
/// underlying C `va_list`. Opaque. | ||
pub struct VaList<'a> { /* fields omitted */ } | ||
|
||
impl<'a> VaList<'a> { | ||
/// Extract the next argument from the argument list. T must have a type | ||
/// usable in an FFI interface. | ||
pub unsafe fn arg<T>(&mut self) -> T; | ||
} | ||
|
||
impl<'a> Clone for VaList<'a>; | ||
``` | ||
|
||
The type returned from `VaList::arg` must have a type usable in an `extern "C"` | ||
FFI interface; the compiler allows all the same types returned from | ||
`VaList::arg` that it allows in the function signature of an `extern "C"` | ||
function. | ||
|
||
All of the corresponding C integer and float types defined in the `libc` crate | ||
consist of aliases for the underlying Rust types, so `VaList::arg` can also | ||
extract those types. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sentence doesn't appear to be necessary with 'usable in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not necessary; I thought it was useful as documentation, though. |
||
|
||
Note that extracting an argument from a `VaList` follows the C rules for | ||
argument passing and promotion. In particular, C code will promote any argument | ||
smaller than a C `int` to an `int`, and promote `float` to `double`. Thus, | ||
Rust's argument extractions for the corresponding types will extract an `int` | ||
or `double` as appropriate, and convert appropriately. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently, when passing arguments to external variadic functions, Rust disallows There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. C does this kind of promotion already; it seems less error-prone to allow Rust code to extract the corresponding type of argument (e.g. |
||
|
||
Like the underlying platform `va_list` structure in C, `VaList` has an opaque, | ||
platform-specific representation. | ||
|
||
A variadic function may pass the `VaList` to another function. However, the | ||
lifetime attached to the `VaList` will prevent the variadic function from | ||
returning the `VaList` or otherwise allowing it to outlive that call to the | ||
variadic function. | ||
|
||
A function declared with `extern "C"` may accept a `VaList` parameter, | ||
corresponding to a `va_list` parameter in the corresponding C function. For | ||
instance, the `libc` crate could define the `va_list` variants of `printf` as | ||
follows: | ||
|
||
```rust | ||
extern "C" { | ||
pub unsafe fn vprintf(format: *const c_char, ap: VaList) -> c_int; | ||
pub unsafe fn vfprintf(stream: *mut FILE, format: *const c_char, ap: VaList) -> c_int; | ||
pub unsafe fn vsprintf(s: *mut c_char, format: *const c_char, ap: VaList) -> c_int; | ||
pub unsafe fn vsnprintf(s: *mut c_char, n: size_t, format: *const c_char, ap: VaList) -> c_int; | ||
} | ||
``` | ||
|
||
Note that, per the C semantics, after passing `VaList` to these functions, the | ||
caller can no longer use it, hence the use of the `VaList` type to take | ||
ownership of the object. To continue using the object after a call to these | ||
functions, pass a clone of it instead. | ||
|
||
Conversely, an `unsafe extern "C"` function written in Rust may accept a | ||
`VaList` parameter, to allow implementing the `v` variants of such functions in | ||
Rust. Such a function must not specify the lifetime. | ||
|
||
Defining a variadic function, or calling any of these new functions, requires a | ||
feature-gate, `c_variadic`. | ||
|
||
Sample Rust code exposing a variadic function: | ||
|
||
```rust | ||
#![feature(c_variadic)] | ||
|
||
#[no_mangle] | ||
pub unsafe extern "C" fn func(fixed: u32, mut args: ...) { | ||
let x: u8 = args.arg(); | ||
let y: u16 = args.arg(); | ||
let z: u32 = args.arg(); | ||
println!("{} {} {} {}", fixed, x, y, z); | ||
} | ||
``` | ||
|
||
Sample C code calling that function: | ||
|
||
```c | ||
#include <stdint.h> | ||
|
||
void func(uint32_t fixed, ...); | ||
|
||
int main(void) | ||
{ | ||
uint8_t x = 10; | ||
uint16_t y = 15; | ||
uint32_t z = 20; | ||
func(5, x, y, z); | ||
return 0; | ||
} | ||
``` | ||
|
||
Compiling and linking these two together will produce a program that prints: | ||
|
||
```text | ||
5 10 15 20 | ||
``` | ||
|
||
# Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation | ||
|
||
LLVM already provides a set of intrinsics, implementing `va_start`, `va_arg`, | ||
`va_end`, and `va_copy`. The compiler will insert a call to the `va_start` | ||
intrinsic at the start of the function to provide the `VaList` argument (if | ||
used). The implementation of `VaList::arg` will call `va_arg`. The | ||
implementation of `Clone` for `VaList` wil call `va_copy`. The compiler will | ||
ensure that a call to `va_end` occurs exactly once on every `VaList` at an | ||
appropriate time, similar to drop semantics. (This may occur via an | ||
implementation of `Drop`, but this must take into account the semantics of | ||
passing a `VaList` to or from an `extern "C"` function.) | ||
|
||
`VaList` may become a language item (`#[lang="VaList"]`) to attach the | ||
appropriate compiler handling. | ||
|
||
The compiler may need to handle the type `VaList` specially, in order to | ||
provide the desired drop semantics at FFI boundaries. In particular, some | ||
platforms pass `va_list` by value, and others pass it by pointer; the standard | ||
leaves unspecified whether changes made in the called function appear in the | ||
caller's copy. Rust must match the underlying C semantics, to allow passing | ||
VaList values between C and Rust. To avoid forcing variadic functions to cope | ||
with these platform-specific differences, the compiler should ensure that the | ||
type behaves as if it has `Drop` semantics, and has `va_end` called on the | ||
original `VaList` as well as any clones of it. For instance, passing a `VaList` | ||
to a `vprintf` function as declared in this RFC semantically passes ownership | ||
of that `VaList`, and prevents calling the `arg` function again in the caller, | ||
but it remains the caller's responsibility to call `va_end`, so the compiler | ||
needs to insert the appropriate drop glue at the FFI boundary. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not entirely sure what this means, but it's worth noting that if pub unsafe extern "C" fn vprintf(format: *const c_char, args: VaList) {
let args_clone = args.clone();
}
void vprintf(const char *format, va_list args) {
va_list args_clone;
va_copy(args_clone, args);
va_end(args_clone);
} Of course, this matches C semantics, so it may be different for LLVM. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added some text to clarify this, thank you. I've also added an explanation of relevant C standards requirements. |
||
|
||
Note that on some platforms, these LLVM intrinsics do not fully implement the | ||
necessary functionality, expecting the invoker of the intrinsic to provide | ||
additional LLVM IR code. On such platforms, rustc will need to provide the | ||
appropriate additional code, just as clang does. | ||
|
||
This RFC intentionally does not specify or expose the mechanism used to limit | ||
the use of `VaList::arg` only to specific types. The compiler should provide | ||
errors similar to those associated with passing types through FFI function | ||
calls. | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
This feature is highly unsafe, and requires carefully written code to extract | ||
the appropriate argument types provided by the caller, based on whatever | ||
arbitrary runtime information determines those types. However, in this regard, | ||
this feature provides no more unsafety than the equivalent C code, and in fact | ||
provides several additional safety mechanisms, such as automatic handling of | ||
type promotions, lifetimes, copies, and destruction. | ||
|
||
# Rationale and Alternatives | ||
[alternatives]: #alternatives | ||
|
||
This represents one of the few C-compatible interfaces that Rust does not | ||
provide. Currently, Rust code wishing to interoperate with C has no alternative | ||
to this mechanism, other than hand-written C stubs. This also limits the | ||
ability to incrementally translate C to Rust, or to bind to C interfaces that | ||
expect variadic callbacks. | ||
|
||
Rather than having the compiler invent an appropriate lifetime parameter, we | ||
could simply require the unsafe code implementing a variadic function to avoid | ||
ever allowing the `VaList` structure to outlive it. However, if we can provide | ||
an appropriate compile-time lifetime check, doing would make it easier to | ||
correctly write the appropriate unsafe code. | ||
|
||
Rather than naming the argument in the variadic function signature, we could | ||
provide a `VaList::start` function to return one. This would also allow calling | ||
`start` more than once. However, this would complicate the lifetime handling | ||
required to ensure that the `VaList` does not outlive the call to the variadic | ||
function. | ||
|
||
We could use several alternative syntaxes to declare the argument in the | ||
signature, including `...args`, or listing the `VaList` or `VaList<'a>` type | ||
explicitly. The latter, however, would require care to ensure that code could | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As someone who is not expecting to write many implementations of variadic functions, my main interest is in minimising the changes to the 'global' parts of the Rust language that this (indisputably necessary) feature involves. I, personally, would therefore prefer taking a VaList explicitly to adding this new ... syntax. If we take a VaList by value, there's no worry about lifetimes. Rather, there is a worry about moving that VaList to somewhere it shouldn't go. Is there something we can do to make in unmoveable, so it can't escape the function which receives it? Or is that barking up completely the wrong tree? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tomwhoiscontrary We'd still need a syntax that distinguishes between passing around a Also, we do want the ability to pass around the |
||
not reference or alias the lifetime. | ||
|
||
# Unresolved questions | ||
[unresolved]: #unresolved-questions | ||
|
||
When implementing this feature, we will need to determine whether the compiler | ||
can provide an appropriate lifetime that prevents a `VaList` from outliving its | ||
corresponding variadic function. | ||
|
||
Currently, Rust does not allow passing a closure to C code expecting a pointer | ||
to an `extern "C"` function. If this becomes possible in the future, then | ||
variadic closures would become useful, and we should add them at that time. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be a requirement of at least one parameter before
...
? Such a requirement exists in C (not C++ however), and considering this is a FFI feature, it sounds reasonable to copy this requirement.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The LLVM intrinsics don't require this, but it seems reasonable as a precaution, sure. Done.