Add an #[align(…)] attribute to set the minimum alignment of struct and
enum fields, statics, functions, 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
;
// Rust bindings generated by `bindgen`
The __bindgen_padding_0 field makes the generated bindings more confusing and
less ergonomic. Also, it is unsound: the padding should be using MaybeUninit.
And even then, there is no guarantee of ABI compatibility on all potential
targets.
Packing a value into fewer cache lines
When working with large values (lookup tables, for example), it is often
desirable to ensure they cross over as few cache lines as possible. This
minimizes the amount of data that is brought into cache when the value is
accessed heavily. One way of doing this is to force the alignment of the value
to be at least the size of the cache line—or, for smaller values,
next_power_of_two() of the value size.
The simplest way of accomplishing this in Rust today is to use a wrapper struct
with a #[repr(align(…))] attribute:
type SomeLargeType = ;
;
static LOOKUP_TABLE: = CacheAligned;
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 unnecessary padding to the value, wasting memory.
In some cases, it can also improve performance to align a function's code in the same way.
Required alignment for low-level use cases
Some low-level use-cases (for example, the RISC-V mtvec
register)
require functions or statics to have a certain minimum alignment.
Pointer tagging
Users may want to specify a minimum alignment for various items, in order to leave the low bits of pointers to such items free to store additional data.
Interoperating with systems that have types where size is not a multiple of alignment
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.
Explanation
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 positive integer literal.
The maximum alignment is the same as #[repr(align(…))]: 229. That
being said, some targets may not be able to support very high alignments in all
contexts. In such cases, the compiler must impose a lower limit for those
specific contexts on those specific targets. The compiler may choose to emit a
lint warning for high, non-portable alignment specifiers.
Multiple instances of the align attribute may be present on the same item; the
highest alignment among them will be used. The compiler may signal this case
with a warn-by-default lint.
#[align(…)] on a public item is part of the item’s public API, and it is a
semver-breaking change to lower it. As such, it is shown in rustdoc-generated
documentation. To avoid making this commitment, one can use
#[cfg_attr(not(doc), align(…))].
On ADT fields
The align attribute may be applied to any field of any
non-#[repr(transparent)] struct, enum, or union.
union Baz
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, which must in turn be no less than that of the
most-aligned field. That hasn't changed.)
Instances of the align attribute for fields of a #[repr(packed(n))] ADT may
not specify an alignment higher than n.
Interaction with repr(C)
repr(C) currently has two contradictory meanings: “a simple, linear layout
algorithm that works the same everywhere” and “an ABI matching that of the
target’s standard C compiler”. This RFC does not aim to resolve that conflict;
that is being discussed as part of RFC
3718. Henceforth, we will use
repr(C_for_real) to denote “match the system C compiler”, and repr(linear)
to denote “simple, portable layout algorithm”; but those names are not
normative.
Of course, if a type declaration is using one of these reprs to make a public
API commitment as to the exact layout of a type, then any change to field
#[align(…)]s may be breaking.
repr(C_for_real)
The layout of a repr(C_for_real) 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(linear)
In a repr(linear) ADT, a field with an align attribute has its alignment, as
well as the alignment of the containing ADT, increased to at least what the
attribute specifies.
For example, the following two structs have the same layout in memory (though not necessarily the same ABI):
On static items
Any static item (including statics inside extern blocks) may have an
align attribute applied:
static BAZ: = ;
unsafe extern "C"
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 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
its type. The question of whether the UB can occur even if the item is unused,
has the same answer for both cases.)
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!
derive macro helper ambiguity
Some existing derive macros in the ecosystem currently declare align as a
helper attribute. To avoid breakage, we specify that derive helper attributes
shadow built-in attributes of the same name (so the built-in attribute does not
take effect). A warn-by-default lint is triggered when this occurs.
On function items
On function items, #[align(…)] sets the alignment of the function’s entry
symbol. (It does not affect the alignment of its function item type, which
remains a 1-ZST.) This replaces #[repr(align(…))] on function items, from
#![feature(fn_align)].
On async fn, the attribute controls the alignment of the code of the function
that returns the Future.
On function items in trait declarations, #[align(n)] (for some alignment n)
specifies the minimum alignment that all implementations of the item must have.
The generated implementation for the trait’s dyn type will also provide this
alignment. impl blocks do not have to repeat the #[align(n)] attribute, it
is implicit. (This enables the trait definition to raise n without breaking
implementations.) That being said, impl blocks are free to specify an
align(…) higher than that provided by the trait. (If they specify a lower
alignment, it will simply be ignored.)
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.
#[align(…)] is compatible with #[naked].
#[align(…)] may be used on function items inside extern blocks. This imposes
a requirement on the symbol being linked to. The UB that can result if this
alignment is not satisfied, follows the same rules as the UB that can result
from an incorrect function signature.
If a function item in an extern block does not specify #[align(…)], then a
function pointer derived from that item may have any alignment. In other words,
the following assertion may fail on any platform:
unsafe extern "Rust"
When #[align(…)] is combined with #[track_caller], the attribute applies to
the shim that is generated for calls via function pointer, as well as to the
primary function (though the alignment of the latter is usually not observable).
WASM targets will most likely not support function alignments above 1, at least initially.
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:
The align attribute may not be applied to function parameters. (This
prohibition is semantic, not syntactic; it is allowed under #[cfg(false)]).
//~ ERROR
The attribute may only be applied to binding patterns. It may not be applied to any other type of pattern, including wildcard patterns:
let _ = true; //~ ERROR
Drawbacks
- This feature adds additional complexity to the language.
- The distinction between
alignandrepr(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.
#[align(…)] vs #[repr(align(…))]
One potential alternative would be to use #[repr(align(…))] everywhere,
instead of introducing a new attribute.
Benefits of this alternative:
- No new attribute polluting the namespace.
- Requesting a certain alignment is spelled the same everywhere.
#[repr(…)]on fields might accept additional options in the future, for specifying layout and padding more precisely.#[repr(…)]on function items could also acceptinstruction_set(…)as an argument, replacing the existing attribute of that name.
Drawbacks:
#[repr(align(…))]is a longer and noisier syntax.#[repr(…)]on non-ADTs would never accept the same set of options as on ADTs. On field definitions, it might accept additional options to precisely control layout; on function items, it might acceptinstruction_set(…), if we were to overturn the precedent of that being a standalone attribute. On statics and local variables, I doubt it would ever accept anything else at all.#[align(…)]only aligns, while#[repr(align(…))]also pads to a multiple of the alignment. Having different syntax makes that distinction more clear.
#[align(n)] vs #[align = n]
align = n might be misinterpreted as requesting an alignment of exactly n,
instead of at least n.
#[align(…)] on function parameters
We could choose to allow this. However, this RFC specifies that it should be rejected, because users might incorrectly think the attribute affects ABI when it does not. C and C++ make the same choice.
To give an example of what could go wrong, consider the following function:
On typical platforms, calling this function will involve first passing
very_large_value on the stack or by pointer, and then copying the entire array
to a new place on the stack in order to align it. This implicit extra stack copy
is not present for #[align(…)]ed locals. Forbidding this, and requiring users
to make the move/copy explicit, avoids the performance footgun.
We could always lift this limitation in the future.
Interaction with async fn
This RFC specifies that when applied to async fn, the align attribute should
affect the alignment of the function that returns the future. This breaks
precedent with #[inline], which affects the future poll method.
There is good reason for this difference. In the case of inline, controlling
the inlineability of the function that returns the future is almost never what
you want. That function is mostly trivial, and there is little reason to deviate
from the default of inlining it in most cases. Controlling the inlineability of
the poll method is far more useful.
In contrast, there are several potential reasons to want to control the
alignment of an async fn. For example, this could be used in concert with
function pointer tagging schemes. If users apply #[align(…)] to an async fn
item believing that it will affect the alignment of the function’s pointer (as
it does with any other function item), but instead it affects that of the poll
method, that could even result in UB. Therefore, it makes more sense to choose
the simpler and more consistent rule of having the #[align(…)] attribute
affect the alignment of the function that returns the future.
The current #![feature(fn_align)] works this way already.
#[align(…)] mut local via mut #[align(…)] local
This RFC proposes that the #[align(…)] attribute should come before the mut
keyword when declaring an aligned local variable.
Local variables are declared with identifier patterns. The local is defined by three pieces of information:
- Its name, which is declared explicitly
- Its mutability, which is declared explicitly via the presence or absence of
the
mutkeyword - Its type, which is derived implicitly from the structure and type of the surrounding pattern.
In Rust, attributes come before the element they modify. In this case, the mut
keyword is an integral part of the local’s declaration; therefore, the attribute
should precede it.
#[align(…)] on _ wildcard patterns
This makes no semantic sense. Just as binding modes can only be applied to
bindings, #[align(…)] also can only be applied to bindings. _ patterns are
not bindings; they are a completely separate element in the grammar.
#[align(…)] function item types being 1-ZSTs
Anything else would pessimize memory usage for no reason.
Prior art
This proposal is the Rust equivalent of
C and
C++ alignas.
There are a few significant semantic differences between those features and this RFC:
#[align]additionally allows applying the attribute to function item declarations, whichalignasdoes not permit.- C++, but not C, allows applying
alignasto type declarations, like Rust’srepr(align); this RFC does not permit that usage. alignas(n)accepts any integer constant expression or type name forn; this RFC accepts only integer literals (for now).alignas(n)allowsnto be zero, in which case the specifier is ignored; this RFC does not permit that usage.
Unresolved questions
MSVC
Does MSVC do something weird with alignas? In other words, is the concern
about repr(C) vs repr(linear) purely theoretical at this point, or does it
matter in practice today?
Interaction with ref/ref mut
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.
- Option B: the attribute goes outside the
ref/ref mut.
I believe the simplest option is to forbid this combination entirely for now, especially as there is effectively no use-case for it.
How I believe we should decide this eventually
In my view, the resolution of this question hinges on whether ref/ref mut
are an integral part of the local declaration. My instinct is to say that they
are not. Like the rest of the surrounding pattern, they describe how the initial
value of the local should be extracted from the scrutinee. Like this surrounding
pattern, they implicitly affect the local’s type, but don’t otherwise affect its
properties. At the point of use, you can’t distinguish a local declared with
ref/ref mut from one declared some other way.
However, one could argue that ref/ref mut are part of the same “binding
mode” syntactic element as mut, and that therefore, if the align attribute
precedes mut, it should precede ref/ref mut also.
I believe the the correct time to resolve this question will be when we decide
on a syntax for combining ref/ref mut with mut. If we choose a syntax that
make it clear that these are distinct elements:
let ref = …;
let ref mut = …;
Then, that would imply that #[align(…)] should be applied just before the mut:
let ref x = …;
let ref mut x = …;
let ref mut x = …;
let ref mut mut x = …;
But if we choose a syntax that treats them as components of a single “binding mode” element:
let mut ref x = …;
let mut ref mut x = …;
Then, #[align] should always precede that element:
let ref x = …;
let ref mut x = …;
let mut ref x = …;
let mut ref mut x = …;
Future possibilities
- The
align(…)andrepr(align(…))attributes currently accept only integer literals as parameters. In the future, they could supportconstexpressions 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.- We could also introduce a similar facility for function pointers.
- We could also add similar APIs for over-aligned function pointers.
- We could loosen the restriction that fields of a
packed(n)struct cannot specify an alignment greater thatn. (Apparently, some C compilers allow something similar.) - Once
#![feature(stmt_expr_attributes)]is stable, we could allow applying#[align(…))]to closures and async blocks as well. - We could add tools to make it easier to implement function pointer tagging in a way that’s resilient to the ARM Thumb issue (and similar strangeness on hypothetical future targets).