Skip to content

RFC: Add an attribute for raising the alignment of various items #3806

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

Open
wants to merge 42 commits into
base: master
Choose a base branch
from

Conversation

Jules-Bertholet
Copy link
Contributor

@Jules-Bertholet Jules-Bertholet commented May 1, 2025

Port C alignas to Rust.

Rendered

@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label May 2, 2025
@traviscross traviscross added I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. labels May 2, 2025
Comment on lines 96 to 100
The `align` attribute is a new inert, built-in attribute that can be applied to
ADT fields, `static` items, function items, and local variable declarations. The
attribute accepts a single required parameter, which must be a power-of-2
integer literal from 1 up to 2<sup>29</sup>. (This is the same as
`#[repr(align(…))]`.)

Choose a reason for hiding this comment

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

The 2^29 limit is way too high. The consistency with #[repr(align(..))] is a good default but alignments larger than a page or two have never worked properly in local variables (rust-lang/rust#70143) and in statics (rust-lang/rust#70022, rust-lang/rust#70144). While there are some use cases for larger alignments on types (if they're heap allocated) and an error on putting such a type in a local or static is ugly, for this new attribute we could just avoid the problem from the start.

Copy link
Member

Choose a reason for hiding this comment

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

For a struct field, both GCC and clang supported _Alignas(N) for N ≤ 228 (268435456).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The bug with local variables (rust-lang/rust#70143) seems to have been fixed everywhere except Windows, and just waiting on someone to fix it there as well in LLVM. (And even on Windows where the issue is not fixed, the only effect is to break the stack overflow protection, bringing it down to the same level as many Tier 2 targets.)

So the only remaining issue is with statics, where it looks like a target-specific max alignment might be necessary. Once implemented, that solution can be used to address align as well.

Overall, I don't think any of this is sufficient motivation to impose a stricter maximum on #[align].

Choose a reason for hiding this comment

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

Note that fixing the soundness issue for locals just means that putting a local with huge alignment in a stack frame is very likely to trigger the stack overflow check and abort the program. There is no use case for such massively over-aligned locals or statics, which is why those soundness issues been mostly theoretical problems and why the only progress toward fixing them over many years has been side effects of unrelated improvements (inline stack checks).

The only reason why the repr(align(..)) limit is so enormous is because it’s plausibly useful for heap allocations. Adding a second , lower limit for types put in statics and locals nowadays is somewhat tricky to design and drive to consensus (e.g., there’s theoretical backwards compatibility concerns) and not a priority for anyone, so who knows when it’ll happen. For #[align] we have the benefit of hindsight and could just mostly side-step the whole mess. I don’t see this as “needlessly restricting the new feature” but rather as “not pointlessly expanding upon an existing soundness issue for no reason”.

Copy link
Member

@programmerjake programmerjake May 8, 2025

Choose a reason for hiding this comment

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

There is no use case for such massively over-aligned locals or statics

one use case I can think of is having a massive array that is faster because it's properly aligned so the OS can use huge pages (on x86_64, those require alignment $2^{19}$ or $2^{30}$), reducing TLB pressure. admittedly, that would only realistically be useful for statics or heap-allocated/mmap-ed memory.

Copy link

@hanna-kruppe hanna-kruppe May 8, 2025

Choose a reason for hiding this comment

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

To use huge pages for static data, you'd want to align the ELF segment containing the relevant sections (or equivalent in other formats), so the right tool there is a linker script or similar platform-specific mechanism. Over-aligning individual statics is a bad alternative:

  1. It risks wasting a lot more (physical!) memory than expected if you end up with multiple statics in the program doing it and there's not enough other data to fill the padding required between them or they go in different sections.
  2. If the linker/loader ignores the requested section alignment then that leads to UB if you used Rust-level #[align(N)]/#[repr(align(N))] and the code was optimized under that assumption.
  3. While aligning statics appears easier and more portable than linker scripts, the reality is that platform/toolchain support for this is spotty anyway, so you really ought to carefully consider when and where to apply this trick.

In any case, I'm sure I'm technically wrong to claim that nobody could ever come up with a use case for massively over-aligned statics. But there's a reason why Linux and glibc have only started supporting it at all in the last few years, and other environments like musl-based Linux and Windows apparently doesn't support it at all (see discussion in aforementioned issues).

Comment on lines 88 to 91
In Rust, a type’s size is always a multiple of its alignment. However, there are
other languages that can interoperate with Rust, where this is not the case
(WGSL, for example). It’s important for Rust to be able to represent such
structures.

Choose a reason for hiding this comment

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

It's not clear to me how this would work while keeping Rust's "size is multiple of align" rule intact. I guess if it's about individual fields in a larger aggregate that maintains the rule in total? I don't know anything about WGSL so an example would be appreciated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That’s exactly it. The WSGL example was taken from this comment on Internals: https://internals.rust-lang.org/t/pre-rfc-align-attribute/21004/20

Copy link
Contributor

Choose a reason for hiding this comment

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

Adding a worked example would indeed help readers of the RFC on this point.

Copy link
Contributor

@kpreid kpreid May 18, 2025

Choose a reason for hiding this comment

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

Here is a concrete example of implementing Rust-WGSL compatibility using the #[align] attribute defined in this RFC. These structs have the same layout, and together demonstrate both inserting required padding (between foo and bar), and allowing a following field to be placed where a wrapper type would demand padding (baz immediately after bar):

// WGSL
struct Example {     // size = 32, alignment = 16
    foo: vec3<f32>,  // offset = 0, size = 12
    bar: vec3<f32>,  // offset = 16, size = 12
    baz: f32,        // offset = 28, size = 4
}
// Rust
#[repr(linear)] // as defined in this RFC; repr(C) in current Rust
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub(crate) struct Example {
    #[align(16)]
    foo: [f32; 3],

    // #[align] below causes 4 bytes of padding to be inserted here to satisfy it.

    #[align(16)]
    bar: [f32; 3],

    baz: f32,      // If we used a wrapper for bar, this field would be at offset 32, wrongly
}

It is often possible to order structure fields to fill gaps so that no inter-field padding is needed — such as if the fields in this example were declared in the order {foo, baz, bar} — and this is preferable when possible to avoid wasted memory, but the advantage of using #[align] in this scenario is that when used systematically, it can imitate WGSL's layout and thus will be correct even if the field ordering is not optimal.

(Please feel free to use any of the above text in the RFC.)

@traviscross
Copy link
Contributor

We discussed this in the lang call today. We were feeling generally favorable about this, but all need to read it more closely.

Comment on lines 378 to 395
1. What should the syntax be for applying the `align` attribute to `ref`/`ref
mut` bindings?

- Option A: the attribute goes inside the `ref`/`ref mut`.

```rust
fn foo(x: &u8) {
let ref #[align(4)] _a = *x;
}
```

- Option B: the attribute goes outside the `ref`/`ref mut`.

```rust
fn foo(x: &u8) {
let #[align(4)] ref _a = *x;
}
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Whatever we do, I'd expect it to be the same as for mut. So it's probably not worth deferring this question, as we need to handle it there.

As for where to put it, it seems like a bit of a coin toss. Anyone have a good argument for which way it should go?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’m comfortable deferring it because I see no use-case for it, and I don’t want to hold up the RFC on something with no use case.

Copy link
Contributor

@traviscross traviscross May 17, 2025

Choose a reason for hiding this comment

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

Sure, but still, I repeat my question, as we need to answer it for mut in any case, about whether there are good arguments for on which side of mut the attribute should appear.

Copy link
Contributor Author

@Jules-Bertholet Jules-Bertholet May 17, 2025

Choose a reason for hiding this comment

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

The mut case does have actual use-cases, so I think we should handle the issue in the context of that, not this RFC.

Copy link
Contributor Author

@Jules-Bertholet Jules-Bertholet May 17, 2025

Choose a reason for hiding this comment

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

Oh, wait, I think there may be a misunderstanding here. By “the same as for mut”, are you referring to combining mut with ref/ref mut?

Copy link
Contributor

Choose a reason for hiding this comment

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

No. This RFC specifies this is allowed (quoting from an example in the RFC):

let (a, #[align(4)] b, #[align(2)] mut c) = (4u8, 2u8, 1u8);

My question is whether there are good arguments about whether we should prefer that, or should instead prefer:

let (a, #[align(4)] b, mut #[align(2)] c) = (4u8, 2u8, 1u8);

The RFC should discuss any reasons we might want to prefer one over the other.


Separately, and secondarily, my feeling is that if we chose

let #[align(..)] mut a = ..;

then we would also choose:

let #[align(..)] ref a = ..;

And if we instead chose

let mut #[align(..)] a = ..;

then we would choose:

let ref #[align(..)] a = ..;

So my feeling is that in settling the question of how to syntactically combine #[align(..)] and mut, we are de facto settling the question of how to combine #[align(..)] with any other binding mode token.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don’t agree that we would necessarily want to make the same choice in both cases. I actually think it depends on how mut and ref/ref mut should be combined.

If the combination looks like

let ref (mut x) = …;
let ref mut (mut x) = …;

Then we should also do

let ref (#[align()] x) = …;
let ref mut (#[align()] x) = …;

But if it looks like

let mut ref x = …;
let mut ref mut x = …;

Then we should do

let #[align()] ref x = …;
let #[align()] ref mut x = …;

Copy link
Contributor

@traviscross traviscross May 17, 2025

Choose a reason for hiding this comment

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

In that event, and in your model, that would still leave us deciding between:

let ref (mut #[align(..)] x) = ..; // 1
// vs
let ref (#[align(..)] mut x) = ..; // 2

And between:

let #[align(..)] mut ref x = ..; // 3
// vs
let mut #[align(..)] ref x = ..; // 4

I would estimate that we'd comfortably favor 1, 3 over 2, 4.

There are also, of course, these possibilities:

let #[align(..)] ref (mut x) = ..; // 5
let mut ref #[align(..)] x = ..; // 6

If in this RFC we pick #[align(..) mut x, that would rule out for me option 1 if we later did ref (mut x) (and I wouldn't pick option 2 anyway). If we pick mut #[align(..)] x, that would rule out for me option 3 if we later did mut ref x (and I wouldn't pick option 4 anyway).

That is, even in this future possibility, I'm going to want to keep all of the binding mode tokens either to the left or to the right of the attribute.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’ll elaborate in the RFC, but my preference is for 2 or 3.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’ve added a section on this to the RFC.

@traviscross traviscross removed the P-lang-drag-1 Lang team prioritization drag level 1. label Jun 20, 2025
the specified alignment. The attribute does not force padding bytes to be added
after the `static`. For `static`s inside `unsafe extern` blocks, if the `static`
does not meet the specified alignment, the behavior is undefined. (This UB is
analogous to the UB that can result if the static item is not a valid value of
Copy link
Member

Choose a reason for hiding this comment

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

I don't think "valid value" is the analog here, because "valid value" is only relevant if the value is consumed with a typed read. I think this is more like the UB from getting a type signature wrong, which is UB even if you don't call the function in question.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

 is only relevant if the value is consumed with a typed read

Can the compiler insert spurious reads? That’s an unsettled question AFAIK, and I don’t aim to settle it in this RFC.

([WGSL](https://www.w3.org/TR/WGSL/#alignment-and-size), for example). It’s
important for Rust to be able to represent such structures.

# Explanation
Copy link
Member

Choose a reason for hiding this comment

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

Please follow the template in https://github.com/rust-lang/rfcs/blob/master/0000-template.md and have both guide-level (how I'd explain it to someone) and reference-level (all the gory details of exactly what the operational behaviour is) parts.

Copy link
Contributor Author

@Jules-Bertholet Jules-Bertholet Jun 25, 2025

Choose a reason for hiding this comment

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

The “Explanation” section is intended to be reference-level. This is a super niche/advanced feature; it is all gory details. Therefore, I don’t feel a separate guide-level explanation would be worthwhile.

Comment on lines +294 to +298
The numerical value of a function pointer to a function with an `#[align(n)]`
attribute is *not* always guaranteed to be a multiple of `n` on all targets. For
example, on 32-bit ARM, the low bit of the function pointer is set for functions
using the Thumb instruction set, even though the actual code of the function is
always aligned to at least 2 bytes.
Copy link
Member

Choose a reason for hiding this comment

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

This is the kind of thing that emphasizes that the reference-level explanation is so important.

What does alignment mean in rust if not (my_fn_ptr as usize).trailing_zeroes() >= ALIGN.log2()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is only the case for function pointers. Data pointers work as you would expect.

Choose a reason for hiding this comment

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

These targets perform pointer tagging on function pointers, see https://developer.arm.com/documentation/ka002971/latest/:

ARM mode instructions are located on 4-byte boundaries. Thumb mode instructions are located on 2-byte boundaries. In the ARM architecture, bit 0 of a function pointer indicates the mode (ARM or Thumb) of the called function, rather than a memory location. When bit 0 = 1, Thumb mode is selected.

So the messier guarantee that I think we can give is:

((my_fn_pointer as usize) & !(core::mem::size_of_val(&my_fn_pointer) - 1)).is_multiple_of(ALIGN)

Although actually maybe https://doc.rust-lang.org/std/marker/trait.FnPtr.html#tymethod.addr should return just the address, and not the tag bits. (the current implementation still includes the tag bits however).

Choose a reason for hiding this comment

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

If you drop the ”tag” when taking the address as usize, can you recover it later to turn the address back to a function pointer? Because that’s probably one of the use cases for casting a function pointer to usize.

Choose a reason for hiding this comment

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

You could recover the function pointer only if you somehow knew whether the function uses Thumb or Arm32 instructions. In any case this feels sort of related to how on CHERI a pointer value is not just its address: there are cases where you do want the extra info, and cases where you don't.

In practice the actual address of a function's code is not that useful though, the pointer tagging mostly just makes exactly specifying the behavior of #[align] more difficult, because we have to talk about the memory location of the machine code and can't talk about the numerical value of the function pointer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a future possibility for adding APIs to make it easier to do function pointer tagging in a way that’s portable to platforms like Thumb. 5da3dcb

I feel comfortable relegating this to a future possibility because, at present, I am not aware of a single actual user who would want this. The original motivation for #[align(…)] on functions was the RISC-V mtvec register (rust-lang/rust#75072). What this RFC provides is more than sufficient for use-cases like that one.

Copy link
Member

@scottmcm scottmcm left a comment

Choose a reason for hiding this comment

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

I do like this RFC in the abstract, as allowing attributes to raise field alignment in structs does make sense to me as something not sufficiently handled with a foo: MinAlign<2, Foo> wrapper or similar.

I've left a bunch of comments on my way by. Most aren't things that would keep me from checking a box, but I think the "follow the template and be specific about semantics in the reference-level section" pushed me to "request changes" here. If alignment doesn't make the address a multiple of the alignment, then I have no idea what the semantics of this RFC are when it comes to Rust code that thought it was being portable.

@Jules-Bertholet Jules-Bertholet requested a review from scottmcm June 26, 2025 17:30
@scottmcm scottmcm dismissed their stale review June 26, 2025 19:37

(It's my review I shouldn't need to leave a reason)

@scottmcm
Copy link
Member

Thinking more and seeing updates, I think I'm fine to check a box here, subject to a note that I think this would be a really good thing to split into different features in the compiler for the different parts. I foresee stabilizing it on fields relatively promptly, but with much less urgency to figure it out for local variables, for example.

@rfcbot reviewed

Copy link
Member

@workingjubilee workingjubilee left a comment

Choose a reason for hiding this comment

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

Having seen how the -Cmin-function-alignment stabilization PR is playing out, I do not believe #[align] should be permitted to differ between a trait declaration and its implementations, and thus the implementations of a trait should simply reject this. No stated use-case for this difference has been given, and I expect it will be surprisingly difficult to implement even this simpler rule in the compiler and have it survive optimizations.

@Jules-Bertholet
Copy link
Contributor Author

@workingjubilee This feature is going to be implemented piecemeal anyway. Over-aligning trait impl methods can be considered a form of refinement (rust-lang/rust#100706), and so could justifiably be held up until the rest of that feature is ready. But forbidding it forever would be inconsistent with allowing other forms of refinement.

@folkertdev
Copy link

That means that for now we should disallow it on function prototypes, right? E.g. inline currently behaves like that:

warning: `#[inline]` is ignored on function prototypes
 --> src/main.rs:2:5
  |
2 |     #[inline(always)]
  |     ^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_attributes)]` on by default

It doesn't look like refinement is moving that quickly, and currently the only attribute that I can find that is inherited from a trait is #[track_caller].

@Jules-Bertholet
Copy link
Contributor Author

#[align(…)] is part of public API, #[inline] is an optimization hint.

@workingjubilee
Copy link
Member

workingjubilee commented Jun 28, 2025

@Jules-Bertholet I am fine with us someday exploring it. I just know that #[align] is going to be stabilized almost immediately on functions after this, and I think that in principle, for refactoring purposes, you would definitely want to be able to go on a path from this:

#[align(16)]
fn something<T: Trait>(arg: &mut T) -> &mut T {
    // code
}

To this:

trait SomethingTrait: Trait {
    #[align(16)]
    fn call_me(&mut self) -> &mut Self;
}

impl<T> SomethingTrait for T where T: Trait {
    fn call_me(&mut self) -> &mut Self {
        // code
    }
}

That seems like a perfectly sensible evolution path, right? We would want parity there.

But I don't see any immediate value in these cases:

impl SomethingTrait for Type {
    #[align(32)]
    fn call_me(&mut self) -> &mut Self {
        // code
    }
}

impl SomethingTrait for OtherType {
    #[align(64)]
    fn call_me(&mut self) -> &mut Self {
        // code
    }
}

That would grant you a specific quality that is not present yet on the trait's generic API, which would be harder to reason about. So I don't think we should address the case of the impl refining the alignment immediately until we've got the rest of this nailed down hard.

And in general, people get very confused when an RFC is only in a partially-implemented state. It would be nice if we could at least be able to make clean and easy distinctions as to which parts are implemented, like "oh yes, it's completely implemented for functions and statics but not structs and locals".

@Jules-Bertholet
Copy link
Contributor Author

Jules-Bertholet commented Jun 28, 2025

I realized that this RFC currently fails to specify how #[align(…)] interacts with #[track_caller]. Unfortunately, it seems that #[track_caller] has problems (rust-lang/rust#143162) that probably need to be resolved before I can make a proposal.

@Jules-Bertholet
Copy link
Contributor Author

Jules-Bertholet commented Jun 28, 2025

I’ve added a note that #[align(…)] is compatible with #[naked]. (The compiler currently incorrectly forbids this.) (Edit: it does work, I was using an old nightly)

@Jules-Bertholet
Copy link
Contributor Author

I’ve added a note that #[align(…)] is allowed on function items in extern blocks.

@traviscross traviscross removed I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. P-lang-drag-2 Lang team prioritization drag level 2. labels Jul 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. T-lang Relevant to the language team, which will review and decide on the RFC. to-announce
Projects
None yet
Development

Successfully merging this pull request may close these issues.