Skip to content
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

Tracking issue for RFC 2137: Support defining C-compatible variadic functions in Rust #44930

Open
1 of 3 tasks
Tracked by #1568
aturon opened this issue Sep 29, 2017 · 124 comments
Open
1 of 3 tasks
Tracked by #1568
Labels
A-ffi Area: Foreign Function Interface (FFI) B-RFC-approved Blocker: Approved by a merged RFC but not yet implemented. B-unstable Blocker: Implemented in the nightly compiler and unstable. C-tracking-issue Category: A tracking issue for an RFC or an unstable feature. F-c_variadic `#![feature(c_variadic)]` S-tracking-ready-to-stabilize Status: This is ready to stabilize; it may need a stabilization report and a PR T-lang Relevant to the language team, which will review and decide on the PR/issue.

Comments

@aturon
Copy link
Member

aturon commented Sep 29, 2017

This is a tracking issue for the RFC "Support defining C-compatible variadic functions in Rust" (rust-lang/rfcs#2137).

Steps:

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."
  • Continuing bikeshed on the ... syntax.
  • Ensure that even when this gets stabilized for regular functions, it is still rejected on const fn.
  • What even is the semantics of this? What exactly is allowed and disallowed? Would be good to have a Miri implementation.
  • borrowck problems: Poor interaction between NLL-borrowck, async, and c_variadic's ... desugaring (VaListImpl<'_>) #125431
@aturon aturon added B-RFC-approved Blocker: Approved by a merged RFC but not yet implemented. C-tracking-issue Category: A tracking issue for an RFC or an unstable feature. T-lang Relevant to the language team, which will review and decide on the PR/issue. labels Sep 29, 2017
@plietar
Copy link
Contributor

plietar commented Sep 29, 2017

I'd like to work on this, I already have some prototype

@aturon
Copy link
Member Author

aturon commented Sep 29, 2017

Awesome @plietar! It'd probably be good to bring this up in the "middle-end" compiler working group channel, which would also be a good place to get any help you might need.

@joshtriplett
Copy link
Member

@plietar How goes the implementation? I remember you showing a mostly complete prototype on IRC.

@thedataking
Copy link

@plietar any news to share? This is a blocker for teams working on C to Rust transpilers, so this addition would be very welcome. (I'm part of one such team).

@plietar
Copy link
Contributor

plietar commented Feb 22, 2018

Hey,
Sorry I've been busy and then forgot about this. I'll get my prototype back in shape, hopefully by this weekend.

@harpocrates
Copy link
Contributor

@plietar Any update on this? Do you have a WIP branch I can check out to try / fiddle with this?

@dlrobertson
Copy link
Contributor

Looks like I'm a little late to the party 😄 ... sorry about that

A few questions:

  1. How would functions that use a va_list multiple times work without the ability to explicitly use va_start and va_end? Are they expected to use copy? E.g. execl typically loops through the arguments to to get argc, creats a array argv of size argc, loops through the list again populating argv, and finally call execv.

  2. The structure of a va_list varies greatly between the architectures. The intrinsic functions work with the architecture specific structure bitcast to an i8*, but AFAIK we'll still need to define the structure. Which architectures will be expected to be supported in the first iteration? Or am I mistaken that we'll have to define the structure?

@plietar if you don't have the time to work on this any more or if there is any way I could help out, I'd be more than happy to do so. I haven't worked on rustc much, but I'd be happy to help however I can with the implementation of this.

@nikomatsakis
Copy link
Contributor

@dlrobertson

Seems likely that @plietar doesn't have much time, though they can speak for themselves.

I've not really looked closely at what would be needed to implement this, but if you need any help, please ping me, or reach out on gitter/IRC.

@dlrobertson
Copy link
Contributor

dlrobertson commented Apr 4, 2018

I've been working on this for the past two week and have

  • Added va_list_kind to the target specification which closely mirrors clangs TargetInfo::BuiltinVaListKind
  • Added Type::va_list which builds the correct structure based on the targets va_list_kind

I'm struggling a bit with understanding how to write something in libcore and link that to Type::va_list in trans. I'm currently attempting to add #[lang = "va_list"] so that I can check the def.did against the lang_items().va_list_impl() id. Does this seem correct?

Also how would you like me to break this up into PRs? My current plan was to submit a PR once I got VaList implemented (meaning functions like vprintf could be defined and tested). Then submit a second PR for implementing support for functions like printf.

bors added a commit that referenced this issue Sep 28, 2018
libcore: Add VaList and variadic arg handling intrinsics

## Summary

 - Add intrinsics for `va_start`, `va_end`, `va_copy`, and `va_arg`.
 - Add `core::va_list::VaList` to `libcore`.

Part 1 of (at least) 3 for #44930

Comments and critiques are very much welcomed 😄
bors added a commit that referenced this issue Nov 29, 2018
libcore: Add VaList and variadic arg handling intrinsics

## Summary

 - Add intrinsics for `va_start`, `va_end`, `va_copy`, and `va_arg`.
 - Add `core::va_list::VaList` to `libcore`.

Part 1 of (at least) 3 for #44930

Comments and critiques are very much welcomed 😄
@dlrobertson
Copy link
Contributor

dlrobertson commented Nov 29, 2018

Now that the VaList structure is implemented and merged into master, I'm moving on to implementing variadic functions.

When any issues with the implementation of VaList are found, please ping me.

@alexreg
Copy link
Contributor

alexreg commented Dec 9, 2018

@dlrobertson That's super. Are you implementing the ... syntax given that seems to be the consensus so far, and bikeshedding hasn't revealed a better one? (Unless I'm behind on things.)

@pervognsen
Copy link

pervognsen commented Apr 14, 2024

Is there a way to "forward" the variadic arguments to another variadic function, supposing I know the size in bytes of the arguments?

Use-case: Implementing library interposition (aka LD_PRELOAD wrapper) around a C interface with variadic functions. For a (rough) example:

pub unsafe extern "C" fn printf(fmt: *const libc::c_char, mut args: ...) -> usize {
    let real_printf = dlsym(RTLD_NEXT, "printf");
    let size = /*
        compute size of args based on %-codes in fmt until reaching null byte
    */;
    real_printf(fmt, *args) /* How to forward args to real_printf? */
}

I need to do this too. Between reading the RFC (which appears outdated since the current implementation now has a split between VaList and VaListImpl) and looking at the scant API docs, I came up with this example code which passes my basic tests: https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=a536485d2935d6138b6bd8425d5f43a0. However, it would be nice to have confirmation from someone who understands the intended semantics.

@charmoniumQ
Copy link

We're talking about slightly different things.

Function we are defining Function we are calling
My request variadic (e.g., `printf_wrapper`) variadic (e.g., `printf`)
Your request variadic (e.g., `printf_wrapper`) not variadic (e.g., `vprintf`)

In GCC, forwarding variadic functions to a variadic function can be done using __builtin_apply. I don't think there is a Rust equivalent of this.

@pervognsen
Copy link

pervognsen commented Apr 14, 2024

We're talking about slightly different things.
Function we are defining Function we are calling
My request variadic (e.g., printf_wrapper) variadic (e.g., printf)
Your request variadic (e.g., printf_wrapper) not variadic (e.g., vprintf)

In GCC, forwarding variadic functions to a variadic function can be done using __builtin_apply. I don't think there is a Rust equivalent of this.

Ah. It doesn't even look like clang supports __builtin_apply, which does not make me hopeful there would be a 1:1 equivalent in LLVM. Since the '...' in 'args: ...' currently stands for a platform/ABI-specific opaque VaListImpl, you can get 'naive forwarding' to pass Rust's type checks and it misleadingly passes basic tests as long as you stay within the ABI argument limit that stays in registers, but it's revealed as incorrect when you exercise a longer argument list: https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=3f7e586b0d35e50626cf290097731d6a. I'm assuming you got at least this far in your own investigation; I'm just leaving this here in case anyone else comes across this thread.

@bjorn3
Copy link
Member

bjorn3 commented May 24, 2024

Something to note in the unresolved questions:

libcore currently hard codes the layout of va_list that llvm uses for each individual target. This probably won't work with other backends. How should we handle the target and backend dependent VaListImpl layout?

@lolbinarycat
Copy link

Bikeshed: the ellipsis in mut args: ... has no precedent anywhere else in the language. The syntax could be something more reminiscent of pattern matching, like mut args @ ...

untrue, it is already used when declaring extern variadic functions.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=724901e4535cd7c5981de0eff52a978f

@bjorn3
Copy link
Member

bjorn3 commented May 31, 2024

For declaring variadic functions the ... is not in the type position like with mut foo: ..., instead it is a special case only valid inside function declarations.

@lolbinarycat
Copy link

presumably the syntax for defining variadic functions would also be a special case.

regardless, my point is that it does have precedent, and indeed is already associated with variadics.

@beetrees
Copy link
Contributor

beetrees commented Jun 1, 2024

Something to note in the unresolved questions:

libcore currently hard codes the layout of va_list that llvm uses for each individual target. This probably won't work with other backends. How should we handle the target and backend dependent VaListImpl layout?

@bjorn3 The layout of va_list is part of the C ABI for a target, so it doesn't vary by backend. For instance, on x86-64 Unix, it is defined to always be:

typedef struct {
    unsigned int gp_offset;
    unsigned int fp_offset;
    void *overflow_arg_area;
    void *reg_save_area;
} va_list[1];

in the ABI specification.

@bjorn3
Copy link
Member

bjorn3 commented Jun 1, 2024

For C-SKY the abi specification doesn't seem to dictate any specific layout for va_list. It suggests a possible implementation for va_start and va_arg in section 2.2.4, but does not say that this is the only valid implementation. And the Hexagon abi specification doesn't even mention va_list, va_start or va_arg. It only explains the calling convention in section 5.2.

You are right that many other abi specifications do explicitly mention the layout. I wasn't aware of this.

References:
C-SKY: https://github.com/c-sky/csky-doc/blob/master/C-SKY_V2_CPU_Applications_Binary_Interface_Standards_Manual.pdf
Hexagon: https://lists.llvm.org/pipermail/llvm-dev/attachments/20190916/21516a52/attachment-0001.pdf

@beetrees
Copy link
Contributor

beetrees commented Jun 1, 2024

I see what you mean. In the cases of C-SKY and Hexagon, this seems to be a case of the ABI specification under-specifying; as va_list can be passed between C functions (such as when calling vprintf), compilers that can link to each others code (e.g. clang and gcc) must agree on the representation of va_list.

@workingjubilee
Copy link
Contributor

workingjubilee commented Jun 24, 2024

On May 28, 2019 2:34:10 PM PDT, Alexander Regueiro @.***> wrote: > As far as I understand, the thing is that f32 is being promoted to a double (f64) when passed as a vararg. Therefore there's no actual possibility to retrieve an f32 value from a va_list because of the promotion. This is what I understood too. In that case, there's no possibility of retrieving anything other than a C int, unsigned int, or double... so why the impls for the other types?
Precisely because those impls are supposed to handle the promotion behavior correctly.

I do not believe this should be done. The Rust compiler does not even correctly check passing varargs currently when generic types become involved, see #61275 about that. When combined with accepting these types, this would make it very difficult to reason correctly about the type conversions involved, because we would be adding in our own special rules on top of the already Byzantine rules for C's variable arguments. This would make it much harder to correctly add support for passing e.g. structs over variadic arguments, down the line1. I don't believe our current implementations for variadic arguments are adequately or thoroughly tested. We have had a bad habit of not testing many edge-cases that could potentially get around naive checks.

In other words, if you extract a different type from the va_list than the types that are passed into the variable arguments (barring some very specific equivalencies that are preserved between types of the same size), it's UB in C. It should be UB in Rust, too.

I have found way too many bugs in how we handle low-level compilation details, including ABI problems, to believe this is a good idea.

Footnotes

  1. This is partly "because we might accidentally introduce behaviors that conflict with other ABI handling details", and also partly because our energies for handling ABI correctly are extremely limited.

workingjubilee added a commit to workingjubilee/rustc that referenced this issue Jun 25, 2024
…, r=joboet

core: VaArgSafe is an unsafe trait

`T: VaArgSafe` is relied on for soundness. Safe impls promise nothing. Therefore this must be an unsafe trait. Slightly pedantic, as only core can impl this, but we *could* choose to unseal the trait. That would allow soundly (but unsafely) implementing this for e.g. a `#[repr(C)] struct` that should be passable by varargs.

Relates to rust-lang#44930
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Jun 25, 2024
…, r=joboet

core: VaArgSafe is an unsafe trait

`T: VaArgSafe` is relied on for soundness. Safe impls promise nothing. Therefore this must be an unsafe trait. Slightly pedantic, as only core can impl this, but we *could* choose to unseal the trait. That would allow soundly (but unsafely) implementing this for e.g. a `#[repr(C)] struct` that should be passable by varargs.

Relates to rust-lang#44930
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Jun 25, 2024
Rollup merge of rust-lang#126927 - workingjubilee:vaargsafe-is-unsafe, r=joboet

core: VaArgSafe is an unsafe trait

`T: VaArgSafe` is relied on for soundness. Safe impls promise nothing. Therefore this must be an unsafe trait. Slightly pedantic, as only core can impl this, but we *could* choose to unseal the trait. That would allow soundly (but unsafely) implementing this for e.g. a `#[repr(C)] struct` that should be passable by varargs.

Relates to rust-lang#44930
@workingjubilee
Copy link
Contributor

workingjubilee commented Jun 27, 2024

It also seems incoherent to provide a demotion behavior for all of those types and then refuse to demote f32, I would think, as there is an expected lossless translation from f32 to f64 for non-NAN bitpatterns and a lossy translation from f64 to f32 for non-NAN bitpatterns that provides the rounded inverse of that. Importantly, if someone passed an f32, they'd get the same f32 back (except NANs might get weird, but we can attempt to define the promotion/demotion behavior to preserve NAN bitpatterns in the case where there's no optimizations).

Or f16, for that matter.

I still think we shouldn't do this because it invites confusion and misunderstanding what the Rust compiler is doing (because people don't really understand what the C compilers do, either).

@workingjubilee
Copy link
Contributor

@programmerjake has informed me we cannot even rely anymore on things like "all C types will get promoted to at least a certain size" (even relative to the C ABI!) casting further doubt on that idea.

@programmerjake
Copy link
Member

yes, afaict _BitInt(8) doesn't get promoted to int but instead gets passed as an 8-bit integer (verified using clang -emit-llvm), this also applies to FP types -- _Float32 doesn't get promoted to double (didn't check what clang does). just the older C types get promoted: e.g. signed char to int and float to double.

@RalfJung
Copy link
Member

In other words, if you extract a different type from the va_list than the types that are passed into the variable arguments (barring some very specific equivalencies that are preserved between types of the same size), it's UB in C. It should be UB in Rust, too.

Definitely fully agreed. Whether an f32 gets promoted to something else or not should not be the programmer's concern. Especially if that is already how C works (if the argument has type f32, it needs to be extracted from the va_list at type f32), then we should do the same in Rust.

It follows that the entire discussion about what gets promoted is unnecessary. It's the backends responsibility to handle this correctly.

@programmerjake
Copy link
Member

Whether an f32 gets promoted to something else or not should not be the programmer's concern.

I think Rust shouldn't do promotions for f32 since there are multiple corresponding C types: float and _Float32. float gets promoted but _Float32 does not, so I think the programmer just has to manually promote to f64 if that's what the function expects, Rust doesn't know wether float or _Float32 is what the API expects so can't do promotions for you.

@RalfJung
Copy link
Member

RalfJung commented Jun 30, 2024

As an example of the kind of "fun" one can have with this, see #71915. The man page says the argument has type mode_t, which is unsigned short, but the actual function signature is a variadic so on the ABI level the argument is passed as int... and if you pass this as a mode_t from Rust you risk UB.

@programmerjake
Copy link
Member

maybe a good solution would be to warn when passing or accepting types that are usually subject to promotion since _BitInt(N) and _Float32 are much rarer than char, short, and float. when you actually need to pass/accept _BitInt(8) or _Float32 you can use an attribute to silence the warning, maybe #[uncommon_c_types]:

unsafe extern "C" {
    unsafe fn f(c_int, ...);
}

pub g(v: f32) {
    unsafe {
        f(0, #[uncommon_c_types] v)
    }
}

@lolbinarycat
Copy link

why are you suggesting a special attribute for silencing a warning, we already have allow. such a warning does seem like a good idea though, mixing up u8 and char in signatures does seem like it could be problematic.

@programmerjake
Copy link
Member

why are you suggesting a special attribute for silencing a warning, we already have allow.

because iirc a lot of people don't want rustc to emit warnings on code when it's the only valid way to implement something and you can't rewrite it to be warning-free.

@lolbinarycat
Copy link

because iirc a lot of people don't want rustc to emit warnings on code when it's the only valid way to implement something and you can't rewrite it to be warning-free.

this is still doing that, just providing an alias for allow(whatever) doesn't change that. i would argue such a warning should probably be part of clippy instead. also the attribute feels badly named, since it doesn't mention the important part, is which is that this has to do with variadics and promotion.

@workingjubilee
Copy link
Contributor

workingjubilee commented Jun 30, 2024

Definitely fully agreed. Whether an f32 gets promoted to something else or not should not be the programmer's concern. Especially if that is already how C works (if the argument has type f32, it needs to be extracted from the va_list at type f32), then we should do the same in Rust.

To be clear, @RalfJung, this is actually part of the issue: in C, you are expected to have memorized the arcane promotion rules, and thus the author of the variadic function has to specify va_arg to name exactly the type that it has been promoted to. To do otherwise is UB.

This is because the C promotion rules actually are not about the ABI. As demonstrated by _BitInt(8), it is legal, in some cases, to pass smaller-than-c_int args. Instead, it has to do with C source code semantics: all of these transitions are handled by clang's frontend, before it ever reaches LLVM!

So, I think that in Rust, we should not try to squirm out from under this. We should make apparent the bizarreness of the results.

The example you cite, in any case, actually would not compile if you had tried to make the change: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=26aa043833877711d0aba4e26abdebae

@programmerjake
Copy link
Member

this is still doing that, just providing an alias for allow(whatever) doesn't change that.

I guess...

i would argue such a warning should probably be part of clippy instead.

I strongly think it should be in rustc because >99.9% of the time the warning is correct and C APIs are almost always documented as you just pass the smaller type, but rust needs the argument to be cast to the promoted type.

rustc already has lints that will be incorrect more often than this, e.g.: temporary_cstring_as_ptr

also the attribute feels badly named, since it doesn't mention the important part, is which is that this has to do with variadics and promotion.

yeah, picking a different name is fine with me.

@RalfJung
Copy link
Member

RalfJung commented Jul 1, 2024

in C, you are expected to have memorized the arcane promotion rules, and thus the author of the variadic function has to specify va_arg to name exactly the type that it has been promoted to. To do otherwise is UB.

Fun!

The example you cite, in any case, actually would not compile if you had tried to make the change: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=26aa043833877711d0aba4e26abdebae

Ah, neat. I had not seen that as I was just working on the other end -- implementing open in Miri. The error message itself is not very clear about why I need to do this though...
Interestingly, the detailed explanation for that error then says

Certain Rust types must be cast before passing them to a variadic function, because of arcane ABI rules dictated by the C standard.

which seems to contradict a little your statement that this is not about ABI.

also the attribute feels badly named, since it doesn't mention the important part, is which is that this has to do with variadics and promotion.

Note that Rust also has a concept called "promotion", and it is entirely unrelated. So we need to be careful with how we use that term in documentation.

@workingjubilee
Copy link
Contributor

I don't think the expanded error message is correct, no.

It was written by someone who had far more SAN remaining than me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ffi Area: Foreign Function Interface (FFI) B-RFC-approved Blocker: Approved by a merged RFC but not yet implemented. B-unstable Blocker: Implemented in the nightly compiler and unstable. C-tracking-issue Category: A tracking issue for an RFC or an unstable feature. F-c_variadic `#![feature(c_variadic)]` S-tracking-ready-to-stabilize Status: This is ready to stabilize; it may need a stabilization report and a PR T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests