Pre-RFC: #[align] attribute

Summary

Add an #[align] attribute to set the minimum alignment of struct and enum fields, statics, and local variables.

Motivation

Bindings to C and C++

C and C++ provide an alignas modifier to set the alignment of specific struct fields. To represent such structures in Rust, bindgen is sometimes forced to add explicit padding fields:

// C code
#include <stdint.h>
struct foo {
    uint8_t x;
    _Alignas(128) uint8_t y;
    uint8_t z;
};
// Rust bindings generated by `bindgen`
pub struct foo {
    pub x: u8,
    pub __bindgen_padding_0: [u8; 127usize],
    pub y: u8,
    pub z: u8,
}

The __bindgen_padding_0 field makes the generated bindings more confusing and less ergonomic.

Packing values into fewer cache lines

When working with large values (lookup tables, for example), it is often desirable, for optimal performance, to pack them into as few cache lines as possible. One way of doing this is to force the alignment of the value to be at least the size of the cache line, or perhaps the greatest common denominator of the value and cache line sizes.

The simplest way of accomplishing this in Rust today is to use a wrapper struct with a #[repr(align)] attribute:

type SomeLargeType = [[u8; 64]; 21];

#[repr(align(128))]
struct CacheAligned<T>(T);

static LOOKUP_TABLE: CacheAligned<SomeLargeType> = CacheAligned(SomeLargeType {
    data: todo!(),
});

However, this approach has several downsides:

  • It requires defining a separate wrapper type.
  • It changes the type of the item, which may not be allowed if it is part of the crate's public API.
  • It may add padding to the value, which might not be necessary or desirable.

Explanation

The align attribute is a new inert, built-in attribute that can be applied to ADT fields, static 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 229. (This is the same as repr(align).)

On ADT fields

The align attribute may be applied to any field of any struct, enum, or union that is not #[repr(transparent)].

#[repr(C)]
struct Foo {
    #[align(8)]
    a: u32,
}

enum Bar {
    Variant(#[align(16)] u128),
}

union Baz {
    #[align(16)]
    a: u32,
}

The effect of the attribute is to force the address of the field to have at least the specified alignment. (If the field already has at least that alignment, due to the required alignment of its type or to a repr attribute on the containing type, the attribute has no effect).

In contrast to a repr(align) wrapper struct, an align annotation does not necessarily add extra padding to force the field to have a size that is a multiple of its alignment. (The size of the containing ADT must still be a multiple of its alignment; that hasn't changed.)

The layout of a repr(C) ADT with align attributes on its fields is identical to that of the corresponding C ADT declared with alignas annotations. For example, the struct below is equivalent to the C struct foo from the motivation section:

#[repr(C)]
pub struct foo {
    pub x: u8,
    #[align(128)]
    pub y: u8,
    pub z: u8,
}

align attributes for fields of a #[repr(packed(n))] ADT may not specify an alignment higher than n.

#[repr(packed(4))]
struct Sardines {
    #[align(2)] // OK
    a: u8,
    #[align(4)] // OK
    b: u16,
    #[align(8)] //~ ERROR
    c: u32,
}

align attributes on ADT fields are shown in rustdoc-generated documentation.

On statics

Any static item (including statics inside extern blocks) may have an align attribute applied:

#[align(32)]
static BAZ: [u32; 12] = [0xDEADBEEF; 12];

// RFC 3484 syntax
// (the attibute works with the legacy syntax as well)
unsafe extern "C" {
    #[align(2)]
    safe static BOZZLE: u8;
}

As before, multiple attributes may be applied to the same item; only the largest one will be considered.

The effect of the attribute is to force the static to be stored with at least the specified alignment. The attribute does not force padding bytes to be added after the static. For statics inside extern blocks, if the static does not meet the specified alignment, the behavior is undefined.

The align attribute may also be applied to thread-local statics created with the thread_local! macro; the attribute affects the alignment of the underlying value, not that of the outer std::thread::LocalKey.

thread_local! {
    #[align(64)]
    static FOO: u8 = 42;
}

fn main() {
    FOO.with(|r| {
        let p: *const u8 = r;
        assert_eq!(p.align_offset(64), 0);
    });
}

align attributes on statics are shown in rustdoc-generated documentation.

On local variables

The align attribute may also be applied to local variable declarations inside let bindings. The attribute forces the local to have at least the alignment specified:

fn main() {
    let (a, #[align(4)] b, #[align(2)] mut c) = (4u8, 2u8, 1u8);
    c *= 2;
    dbg!(a, b, c);

    if let Some(#[align(4)] x @ 1..) = Some(42u8) {
        dbg!(x);
        let p: *const u8 = x;
        assert_eq!(p.align_offset(4), 0);
    }
}

As before, multiple attributes may be applied to the same local; only the largest one will be considered.

align attributes may not be applied to function parameters.

fn foo(#[align(8)] _a: u32) {} //~ ERROR

They also may not be applied to _ bindings.

let #[align(4)] _ = true; //~ ERROR

Drawbacks

  • This feature adds additional complexity to the languge.
  • The distinction between align and repr(align) may be confusing for users.

Rationale and alternatives

Compared to the wrapper type approach, the align attribute adds additional flexibility, because it does not force the insertion of padding. If we don't adopt this feature, bindgen will continue to generate suboptimal bindings, and users will continue to be forced to choose between suboptimal alignment and additional padding.

Prior art

This proposal is the Rust equivalent of C alignas.

Unresolved questions

  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.
fn foo(x: &u8) {
    let ref #[align(4)] _a = *x;
}
  • Option B: the attribute goes outside the ref/ref mut.
fn foo(x: &u8) {
    let #[align(4)] ref _a = *x;
}
  1. Does MSVC do something weird with alignas?

Future possibilities

  • The align and repr(align) attributes currently accept only integer literals as parameters. In the future, they could support const expressions as well.
  • We could provide additional facilities for controlling the layout of ADTs; for example, a way to specify exact field offsets or arbitrary padding.
  • We could add type-safe APIs for over-aligned pointers; for example, over-aligned reference types that are subtypes of &/&mut.
12 Likes

The "this needs to be aligned but you can still pack the other fields in the tail, even reordered in repr rust" does sounds interesting. And at least codegen can ignore it for things like field projections, if it wants (since loading and storing with less alignment is fine), so it's not as blatantly annoying to deal with as repr(packed).

That said, it's still a bit awkward that &myfoo.y is going to give you a &u8 that immediately loses the alignment information. Is this essentially only relevant for cache line alignment, rather than things affecting codegen?

Can you elaborate on why this is needed in locals, compared to the wrapper? Having this in arbitrary patterns is much more complicated than having attributes on fields, and anything you're owning in a local you could put in a wrapper struct just fine. (And I can't see us implementing packing of locals to use the space inside a #[align(2)] a: u8 to store another byte.)

I wish we had a Aligned<T, const A: ptr::Alignment> magic struct to compare to...

6 Likes

I don't need it in locals; I included it mostly because C has it, and because I see no reason not to have it. I would be totally fine with leaving that part as a future extension

3 Likes

Cache line alignment is my motivation. However, I can certainly see this being used for unsafe code that needs alignment for codegen. The type safety will be subpar, as you mention, but it would be possible to build a safe wrapper API on top of of it. And in the future, if there's a need, we could add over-aligned reference types that are subtypes of &/&mut.

If this feature exists at all, it seems to me that there might be other per-field layout control in the future.

For example, instead of specifying field positions in terms of alignment, one could specify them as absolute offsets from the beginning of the struct. This would:

  • be easier for a binding generator to get right, if it already has offset information, because the alignment of the Rust types doesn't affect it
  • allow defining layouts that cannot be expressed solely in terms of "pad to this alignment", such as
    • "right-aligning" fields (e.g. { [3 bytes of padding] u8 u32 }), perhaps in order to enable stepping backwards from the *const u32
    • unions with interesting overlaps

Therefore, to support this and other future possibilities while keeping the prelude namespace small, how about defining a more general attribute for “placement/representation of this field”? Ambitiously, this could be just the existing repr attribute, with a rule that (currently) only the alignment modifiers can be used:

#[repr(C)]
struct Foo {
    #[repr(align(8))]
    a: u32,
}

This could also be used to make individual fields repr(packed), though I think that's never more powerful than repr(packed(N)) on the entire struct.

If repr is too much conflation, then I'd still suggest defining some more general attribute containing the align: #[placement(align(8))].

3 Likes

The way I think of align is that it specifies a property of the place that's independent of its type. This isn't true of repr (affects the type), or of your suggested placement (controls the relationship to other fields, not necessarily relevant when considering the properties of a field on its own; and not relevant at all for statics and locals). So, while such an attribute may be a good idea, I don't think it should subsume align.

While true, has this ever been implemented in other contexts/languages in the past? I don't like the idea of spending time designing attribute interactions which have no users but have to be considered for other "more important" things like ABI stability (crabi) or the like. While it may indeed have uses, I'd like them to be more concrete than "there might be…in the future".

1 Like

One potential use-case would be as a replacement for the controversial Tracking issue for RFC 2102, "Unnamed fields of struct and union type" · Issue #49804 · rust-lang/rust · GitHub.

1 Like

I believe this have some niche use cases in kernel/embedded programming, where some fields has to be at specific positions (because the CPU/MMU/MMIO peripheral says so), but you are okay with letting the compiler place other things in any of the left over parts. Parts of the page tables are structured like that I believe, where the hardware will walk it, but there are a bunch of left over parts reserved for the OS to put whatever metadata it wants in. Though I don't know if any embedded or kernel developers would actually trust the compiler to do this right and optimally.

1 Like

Why not? Feels like if it's allowed on struct fields, we might as well allow it on union fields too. Sure, it's not necessary on a repr(C) union field, since you could use repr(align(N)) instead, but it should be cheap to support, and is arguably usable on a repr(Rust) union where there's no rule that a N-aligned union actually has N-aligned fields.

An interesting consequence of that would be that we could just never use repr(align(N)) ever again. Not that it'd be worth actively deprecating it, but do people ever really want alignment of the type instead of a field? (Honest question. Nothing's jumping to mind that would need it on a type, since I don't really need aligned unit structs and align(N) on a field defacto makes the struct at least align(N), but maybe there's something I'm not thinking about.)

repr(packed(16)) could still allow field align up to 16, no?

2 Likes

Good point, though that's still up in the air AFAIK. I suppose there's not much harm in allowing it even if it does end up being useless later.

Also a good point. I'll update the RFC

Suppose one has some kind of concurrently-updated table where one might want to eliminate false sharing (occupying the same cache line) between elements. Then

struct Table {
    // Suppose we use `rayon` to mutate these elements in parallel
    contents: Vec<Entry>,
}
#[repr(align(128))] // really this should be a platform-dependent constant…
struct Entry {
    // ... multiple fields ...
}

is a tidier way to do that than adding an extra wrapper struct with an aligned field. The key factor here is that you don't get to declare the layout of a [T]; your only control is of the layout of the T it contains.

1 Like

But you could also put #[align(128)] on the highest-aligned field of Entry, rather than the repr(align(128)), and get the same outcome, right?

Yes, but you have to check which field that is, and maybe it changes depending on generic parameters, platform, or library versions… repr is easier

2 Likes

I have also run into this need for explicit padding fields in the context of wgpu code, because WGSL has its own quirky alignment rules for host-shared data.

This includes some types that have a required alignment larger than their size, in contradiction with Rust’s normal alignment rules. It would be great if this mechanism could be compatible with such rules, with the proviso that the containing struct size remains a multiple of its final alignment.

2 Likes

Yes, Ada lets you specify record layout on a per-field basis, down to individual bits, completely decoupled from the declaration order, see http://www.ada-auth.org/standards/22rm/html/RM-13-5-1.html, and more generally, all of clause 13 of the Ada standard.

I don't think Ada has the concept of niches, though, so that might complicate importing the feature to Rust.

4 Likes

This is basically https://lang-team.rust-lang.org/frequently-requested-changes.html#size--stride, so unlikely to happen.

If it is needed to support you talking to GPU hardware I guess rust either needs to support it, or have a workaround for it. It is not like supporting vulkan is optional.

Given all the mess CHERI is creating with the size of pointers and usize this seems relatively benign. And this would be for talking to hardware that is actually available, not some experiment only available to researchers.

Not quite I think:

@2e71828 would have to elaborate though. Do you have a link to some example source code you could share, perhaps?

1 Like

It’s slightly different because here we’re talking about the alignment of fields in-situ, rather than a general requirement for all instances of the type.

Given these WGSL structures:

struct A {
    v1: vec3<u32>,
    a: f32,
    v2: vec3<u32>,
}

struct B {
    v1: vec3<u32>,
    v2: vec3<u32>,
}

The best Rust translations right now are:

#[repr(C, align(16))]
struct A {
    v1: [u32;3],
    a: f32,
    v2: [u32;3],
}

#[repr(C, align(16))]
struct B {
    v1: [u32;3],
    _padding: MaybeUninit<u32>,
    v2: [u32;3],
}

In both cases, the padding at the end of the Rust struct to bring the size to a multiple of 16 is also required by WGSL, and for the same reason.

An alternative approach would be to define a newtype wrapper around [u32;3] with an alignment of 16. This would eliminate the manual padding in B but make A unrepresentable— To match this alternate Rust definition of A, manual padding entries would be required in the WGSL code, and the whole structure will be significantly larger.


With a field alignment attribute, however, we could then represent both structures on the Rust side in a way that directly corresponds to the WGSL definition:

#[repr(C)]
struct A {
    #[align(16)] // optional, because it’s the first field
    v1: [u32;3],

    a: f32,

    #[align(16)]
    v2: [u32;3],
}

#[repr(C)]
struct B {
    #[align(16)] // optional, because it’s the first field
    v1: [u32;3],

    #[align(16)]
    v2: [u32;3],
}
4 Likes