Skip to content

add c-variadic function definitions#2177

Open
folkertdev wants to merge 6 commits into
rust-lang:masterfrom
folkertdev:c-variadic
Open

add c-variadic function definitions#2177
folkertdev wants to merge 6 commits into
rust-lang:masterfrom
folkertdev:c-variadic

Conversation

@folkertdev
Copy link
Copy Markdown
Contributor

@folkertdev folkertdev commented Feb 17, 2026

I think this has all of the raw material, but needs polishing.

Here is a draft of the stabilization report, for additional context: https://hackmd.io/@Q66MPiW4T7yNTKOCaEb-Lw/S1iI3WIwZg

Tracking issue: rust-lang/rust#44930

@rustbot rustbot added the S-waiting-on-review Status: The marked PR is awaiting review from a maintainer label Feb 17, 2026
Comment thread src/items/functions.md
## C-variadic functions

r[items.fn.c-variadic.intro]
A *c-variadic* function accepts a variable argument list `pat: ...` as its final parameter.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for c-variadic definitions only pat: ... is accepted semantically.

plain ... is currently parsed, the varargs_without_pattern lint is meant to eventually disallow it. This syntax can only be used as an input to macros, when a bare ... makes it past macro expansion, that will emit an error.

I didn't mention this here, but maybe we should: we follow C23 in that pat: ... may be the only argument. Earlier versions of C required at least one standard argument before the ....

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is anything holding back changing varargs_without_pattern to report_in_deps: true?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not as far as I know. I can make that change an nominate for T-lang.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on rust-lang/rust#143619 (comment) there was only one actual impacted crate https://crates.io/crates/binrw. The issue was fixed in jam1garner/binrw#342, however there has not been a release since.

So, if we made this warn in dependencies there would be nothing that users of that crate could do. I'll ping for a release with the fix.

Copy link
Copy Markdown
Member

@joshtriplett joshtriplett Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the releases happened: jam1garner/binrw#342 (comment)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, PR is up at rust-lang/rust#154599

Comment thread src/items/functions.md Outdated
Comment on lines +347 to +348
> [!WARNING]
> Passing an unexpected number of arguments or arguments of unexpected type to a variadic function may lead to [undefined behavior][undefined].
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copied from

https://doc.rust-lang.org/reference/items/external-blocks.html?highlight=variadic#r-items.extern.variadic

basically, the responsibility for passing valid arguments is on the caller.

Comment thread src/items/functions.md
Comment on lines +369 to +377
unsafe extern "C" fn example() -> i32 {
let mut storage = MaybeUninit::<VaList<'_>>::uninit();
va_start(storage.as_mut_ptr()); // Initializes the VaList.
let mut ap: &mut VaList<'_> = ap.assume_init_mut();

unsafe { ap.arg::<i32>() }

va_end(ap)
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

va_start and va_end are C functions/concepts. We could instead handwave and say

let mut ap: VaList<'_> = /* ... */; // Initializes the VaList.

the va_end is called by the VaList Drop implementation.

Copy link
Copy Markdown
Contributor

@ehuss ehuss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the rule items.fn.params.varargs will need to be updated, since it says it can only be used in an external block.

View changes since this review

Comment thread src/items/functions.md
Comment thread src/items/functions.md
> Passing an unexpected number of arguments or arguments of unexpected type to a variadic function may lead to [undefined behavior][undefined].

r[items.fn.async.desugar-brief]
A c-variadic function definition is roughly equivalent to a function operating on a [`VaList`].
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is "roughly" about it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In how arguments are actually passed, these signatures are (very) different

fn foo(ap: ...) { /* ... */ }
fn bar(ap: VaList) { /* ... */ }

Those two functions could have exactly the same body though.

Comment thread src/items/functions.md Outdated
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this pull request Apr 15, 2026
…-in-deps, r=mati865

report the `varargs_without_pattern` lint in deps

tracking issue: rust-lang#44930

After discussion in rust-lang/reference#2177 (comment).

Based on rust-lang#143619 (comment) there was only one actual impacted crate https://crates.io/crates/binrw. The issue was fixed in jam1garner/binrw#342, and has since been released jam1garner/binrw#342 (comment).

Hence we may as well report this loudly.

r? @ghost
jhpratt added a commit to jhpratt/rust that referenced this pull request Apr 16, 2026
…-in-deps, r=mati865

report the `varargs_without_pattern` lint in deps

tracking issue: rust-lang#44930

After discussion in rust-lang/reference#2177 (comment).

Based on rust-lang#143619 (comment) there was only one actual impacted crate https://crates.io/crates/binrw. The issue was fixed in jam1garner/binrw#342, and has since been released jam1garner/binrw#342 (comment).

Hence we may as well report this loudly.

r? @ghost
jhpratt added a commit to jhpratt/rust that referenced this pull request Apr 16, 2026
…-in-deps, r=mati865

report the `varargs_without_pattern` lint in deps

tracking issue: rust-lang#44930

After discussion in rust-lang/reference#2177 (comment).

Based on rust-lang#143619 (comment) there was only one actual impacted crate https://crates.io/crates/binrw. The issue was fixed in jam1garner/binrw#342, and has since been released jam1garner/binrw#342 (comment).

Hence we may as well report this loudly.

r? @ghost
rust-timer added a commit to rust-lang/rust that referenced this pull request Apr 16, 2026
Rollup merge of #154599 - folkertdev:varargs-without-pattern-in-deps, r=mati865

report the `varargs_without_pattern` lint in deps

tracking issue: #44930

After discussion in rust-lang/reference#2177 (comment).

Based on #143619 (comment) there was only one actual impacted crate https://crates.io/crates/binrw. The issue was fixed in jam1garner/binrw#342, and has since been released jam1garner/binrw#342 (comment).

Hence we may as well report this loudly.

r? @ghost
@folkertdev
Copy link
Copy Markdown
Contributor Author

The varargs_without_pattern is now reported in dependencies (on nightly).

I've pushed some tweaks, I'm now planning to submit a stabilization PR in coming days, so if you could look at this again that would be helpful.

@ehuss
Copy link
Copy Markdown
Contributor

ehuss commented Apr 30, 2026

It looks like from rust-lang/rust#155974 that there is an intent to make this unavailable on certain targets. I don't think we've ever done something like that, and I'm not sure how we're going to document that. I suppose there will be a rule. It will need to be careful to distinguish that it is a compile error during validation. Unfortunately we don't define that as a specific phase in the reference, so I'm not sure how we should approach that.

@folkertdev
Copy link
Copy Markdown
Contributor Author

folkertdev commented Apr 30, 2026

I don't think this is very different from https://doc.rust-lang.org/beta/unstable-book/language-features/asm-experimental-arch.html. Consequently the page on inline assembly specifies https://doc.rust-lang.org/nightly/reference/inline-assembly.html?highlight=assemb#r-asm.stable-targets.

So we could have

Support for c-variadic function definitions is stable on the following architectures:

  • x86 and x86-64
  • arm
  • aarch64 and arm64ec
  • ...

The compiler will emit an error if ... is used in a function definition on an unsupported target.

Also spirv and bpf just fundamentally do not support this feature, so there you'd always get an error.

Comment thread src/items/functions.md Outdated
@rustbot

This comment has been minimized.

@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented May 13, 2026

This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

Comment thread src/items/functions.md
```

r[items.fn.c-variadic.dyn-compat]
When a trait method is c-variadic, the trait is no longer [dyn-compatible].
Copy link
Copy Markdown
Contributor

@ehuss ehuss May 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this to the dyn-compatibility section? I assume it would just be added to the list in "Dispatchable functions must…".

I realize there's a bit of a challenge whether to document or link things in multiple places when they cover multiple concepts (dyn-compatibility and c-variadic in this case). In this particular case, I think it's fine to consolidate the rules in the dyn-compatibility list. For example, when describing Self or async or receivers, they don't mention their impact on dyn-compatibility.

View changes since the review

Comment thread src/items/functions.md
Comment on lines +397 to +398
r[items.fn.c-variadic.lifetime]
The lifetime of a `VaList` is that of the function that created it. Hence, the `VaList` value can never outlive the function that created it.
Copy link
Copy Markdown
Member

@tmandry tmandry Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bringing the discussion from rust-lang/rust#155697 (comment) here because I think it's getting too in the weeds for the stabilization PR.

@RalfJung said:

Miri's UB detection around va_end is about catching use-after-free, where you do something like drop(transmute_copy(&my_va_list)) and then keep using my_va_list.
tests/ui/consts/const-eval/c-variadic-fail.rs is a nice test to look at for various bits of UB that Miri/const-eval catch (and that should be all the UB there is).

What makes that case UB, but not the forget case? I don't see anything in the reference PR or stabilization report that defines it. Is it a more general rule about not using a value that isn't Copy after using transmute_copy or something like that? If it is a more general thing, why is there a Drop impl specific to VaList?

The question I want the reference to answer is: What semantic difference does it make to call the destructor of a VaList (and, by extension, what are its implications on the correctness of programs)?

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apparently there are targets (not currently supported by Rust) where VaList::drop needs to free memory allocated on the heap or similar, so that's why VaList: Drop and why checking that you didn't drop it twice or use it after dropping it is useful. Because it's just freeing memory (on the unusual target(s)) or doing nothing (on all existing Rust targets), it's safe to forget it, just like it's safe to forget(my_box).

We mostly decided that we aren't supporting the requirement for the va_end call to be in the same function as va_start, since AFAICT that's just in C because some crazy platform(s) (cfront?) opened a code block in va_start and closed the block in va_end, so if you didn't balance them your program wouldn't compile since the {} aren't balanced.

Copy link
Copy Markdown
Member

@RalfJung RalfJung Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way we model varargs in Miri is that

  • We have a new kind of "magic allocation" that represents a list of variadic arguments. The allocation is "magic" in that the list is represented as native AM state, there aren't actually any bytes here that the program can read or write. This is similar to how we already represent functions (the allocations that function pointers point to) and vtables.
  • When calling a vararg function, a new such allocation gets created, and a pointer to it is stored in the VaList.
  • va_next takes such a pointer, deallocates the allocation, takes the first element off the list, and then creates a new list which has a new pointer and updates the VaList to store that new pointer.
  • va_copy duplicates such an allocation.
  • va_end frees such an allocation.

This model is intended to operationally capture some of the UB that C ascribes to variadics. But there are some things C makes UB that we say are allowed. Specifically C says it is UB to not call va_end, and it is UB to call va_end in a different function than where the VaList got created. We decided to not have all the UB (and we'll require backends to honor that of course) because we'd have to significantly redesign the API to make it sound under such a model. For LLVM this requirement is trivially upheld because va_end at the moment is a NOP on all targets we care about -- but the rules are designed in a way that we can later support targets where it is not a NOP. We just can't support targets where it's UB to actually leak a VaList. We doubt there'll ever be such a target, where would the UB even come from?

So, this is what I'd say the underlying spec for vararg UB is in Rust. The goal was to capture as much as possible of the UB in C. The cases that are UB in C but allowed in Rust satisfy all of the following conditions:

  • No target LLVM currently supports actually makes them UB.
  • We were unable to imagine a target that would make them UB (except for C silliness like needing the curly braces to match up).
  • Making those cases UB would force us to make the safe API a lot less ergonomic and might even force some usecases to go unsafe that we otherwise would make safe. Specifically, we'd have to remove Clone from VaList, and replace it with something like fn with_dup(self: &VaList, f: impl FnOnce(&mut VaList)) -- that's the only way we could enforce the VaList to actually be freed. Having such an unergonomic API just so that we could add entirely theoretical UB that we don't actually need seemed like a bad idea.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • We were unable to imagine a target that would make them UB.

I wouldn't quite say that, but it definitely falls beyond what we expect even the craziest platforms to need:
Having a separate global stack (not the normal function call stack) that varargs are pushed on by va_start and popped off by va_end.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: The marked PR is awaiting review from a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants