Conflation of alignment and packing?

In https://doc.rust-lang.org/nomicon/other-reprs.html it says about #[repr(packed)]:

repr(packed) forces Rust to strip any padding, and only align the type to a byte

and about #[repr(align(n))]:

repr(align(n)) (where n is a power of two) forces the type to have an alignment of at least n. […] This is a modifier on repr(C) and repr(rust) . It is incompatible with repr(packed) .

However this:

It is incompatible with repr(packed) .

doesn’t align with my intuitive understanding about what these mean.

My understanding is that #[repr(packed)] eliminates the internal padding in a structure, so given:

#[repr(packed)]
struct Foo {
    a: Box<()>,
    b: u8,
    c: Box<()>,
    d: u8,
    e: Box<()>,
    f: u8,
    g: Box<()>,
    h: u8
}

the size is 36 rather than 40 (on a 64-bit machine, assuming rustc reorders the fields).

Whereas #[repr(align(n))] defines the external alignment constraint of the structure - that is, the overall structure has to have a particular alignment, rather than the alignment of its most constrained element.

There’s no conflict here; a struct declared:

#[repr(packed, align(64))]
struct Foo { ... }

simply means there’s no internal padding, but in memory the overall structure must be 64-byte aligned (either in a Box/Arc, on the stack, or as an element of an array or other structure). Or to put it another way, it dictates the padding around it that must be applied by a container in order to retain the alignment constraint for the element.

(There is an inherent conflict here:

#[repr(align(128)]
struct Foo { ... }

#[repr(packed)]
struct Bar {
    foo1: Foo,
    foo2: Foo,
}

but this is just another instance of the general problem that repr(packed) allows unaligned references to escape which could cause traps or UB.)

I feel like this is something that would have been discussed before. Pointers? What am I missing?

Thanks.

I think I remember hearing that the combination is intentionally not supported as clang and msvc interpret the combination differently, and thus there’s no good way to allow it for FFI.

(So the options instead are repr(packed(128)) or wrapping the packed struct in an aligned struct, I guess?)

1 Like

What about

#[repr(packed)]
struct Foo_ {
    a: u8,
    b: u16,
}

#[repr(align(128))]
struct Foo /* = */ (
    Foo_,
);

At least gcc 9 and msvc v19.20 seem to do the same thing according to compiler explorer. If a structure is both packed and has an alignment, the fields are packed together, but there’s padding after the last field up to the alignment.

Yeah, that works, but doesn’t seem necessary.

1 Like

Also it’s not possible to put a repr(align) inside a repr(packed) :confused:

Yeah, that would be so much simpler… I got confused by that as well. :confused:

Basically it seems like packed(N) has the implicit side-effect of also setting align(N). And packed is packed(1). So you can’t have both at the same time because then you’d specify alignment twice. This strikes me as rather bad language design, but we probably would confuse everyone if we deviated form what C did (and it is my understanding that we are following C here).

Curious, I didn’t know that. What is the reason for that? I would have thought that the attribute in e.g. #[repr(align(4))] struct U32(u32); is strictly redundant.

1 Like

Structure packing is not an ISO-standardised feature. And if you mean de facto C as provided by major implementations, then I’ll point out that for the code below:

#include <stdint.h>

struct __attribute__((packed, aligned(4))) foo {
	char x;
	uint64_t y;
};

int main() {
	__builtin_printf("sz=%zu, .x @%zu, .y @%zu, align %zu\n",
		sizeof(struct foo),
		__builtin_offsetof(struct foo, x),
		__builtin_offsetof(struct foo, w),
		_Alignof(struct foo)
	);
	return 0;
}

both GCC and Clang output sz=12, .x @0, .y @1, align 4, which is exactly what one would expect. So ‘following C’ seems like a poor justification here.

But what does C do for this?

struct __attribute__((packed)) foo {
	char x;
	uint64_t y;
};

If packed just affects internal field offsets, shouldn’t this have alignment 8?

Also, doesn’t de-facto C have something like packed(N)? I’ve seen #pragma pack(n) being mentioned. What would that mean, if packed is a boolean flag that says no padding should be added?

I think it’s more accurate to say it affects internal field alignments. A uint64_t in a packed struct is treated as if it had alignment 1. From this POV the struct has alignment 1 for the same reason a struct of only u8s does

1 Like

That makes sense. That also explains why

#[repr(packed(2))]
struct Foo { x: u8, y: u8 }

has alignment 1: packed(n) resets the alignment of all fields to be no more than n.

So I assume if both packed and align are present, align should take precedence for the alignment of the struct itself?


On another note, in Rust align can only increase the alignment requirement of a struct, not decrease it:

#[repr(align(1))]
struct Foo { x: u16, y: u8 }

fn main() {
    println!("{}", std::mem::align_of::<Foo>()); // prints 2
}

Is that expected? There’s not even a warning that the attribute is basically ignored.

Discussed (including possible ways to warn) in https://github.com/rust-lang/rust/issues/57996#issuecomment-493195928 ff – should probably extract that into a new issue so it’s not as easily forgotten.

In C (and Rust), setting alignment has the side-effect adjusting the sizeof the struct, so

#[repr(align(128))]
struct Foo { x: u32 }

will have size_of:: == 128. This needn’t be the case; you could imagine the size remains the same, but setting the alignment requires any container to have trailing padding (and the overall container has >= that amount of alignment). But it needn’t have trailing padding after the final field (or if it were the only field).

But that would require a notion of a type’s “stride” - the required spacing in an array - and would raise awkward questions about how much space the final element takes up (I guess the first element would require sizeof(type) memory, and every subsequent element needs alignof(type) memory).

So making sizeof(type) = alignof(T) is really the only sensible thing to do. However if you wanted to be able to reduce the alignment of a type, then it means its stride is effectively max(alignment, size), assuming you don’t intend that the reduced alignment to allow them to overlap in an array.

2 Likes

Some more links for the stride > size discussion: There’s an RFC issue, and we talked about it in the UCG, and talked about it some more in the PR.

3 Likes