crystal icon indicating copy to clipboard operation
crystal copied to clipboard

Alignment requirements for specific types

Open malte-v opened this issue 6 years ago • 6 comments

When working with C libraries/specifications, you sometimes have to align data of specific types in memory. I came across the need for this while I was learning how to share variables between GLSL and Crystal. In GLSL, most types have very special alignent requirements. From the Vulkan spec:

  • A scalar has a base alignment equal to its scalar alignment.
  • A two-component vector has a base alignment equal to twice its scalar alignment.
  • A three- or four-component vector has a base alignment equal to four times its scalar alignment.
  • An array has a base alignment equal to the base alignment of its element type.
  • A structure has a base alignment equal to the largest base alignment of any of its members.
  • A row-major matrix of C columns has a base alignment equal to the base alignment of a vector of C matrix components.
  • A column-major matrix has a base alignment equal to the base alignment of the matrix column type.

Of course, these requirements won't be met by default. In C++11, you can easily get around this using alignas, e. g.

struct UniformBufferObject {
    char this_would_normally_break_alignment;
    alignas(16) glm::mat4 model;
    alignas(16) glm::mat4 view;
    alignas(16) glm::mat4 proj;
};

The alignas specifier works with many different constructs like local variables, instance variables (see above) or struct definitions. I would be totally fine with just having something equivalent to alignas for types in Crystal. Maybe something like below:

@[Align(32)]
struct MyVerySpecialStruct
end

What also would be nice is something I'd call "alignment aliases":

@[Align(16)]
alias Vec4 = Float32[4]

Although the term "alias" would be kind of misleading here since using the "alignment alias" would have a semantically different meaning than using the original type.

This could also become useful when we have SIMD support one day and we need to align the SIMD vector types. What are your thoughts?

malte-v avatar May 13 '19 14:05 malte-v

Three years have passed by. Are there any updates for the issue?

chenzhekl avatar Jan 07 '22 18:01 chenzhekl

An annotation for controlling alignment is useful not just for overaligned types, but also for underaligned ones. For example, Windows does this:

// winnt.h

#include "pshpack4.h" // #pragma pack(push, 4)

#include "pshpack2.h" // #pragma pack(push, 2)

typedef struct _IMAGE_DOS_HEADER {
  WORD   e_magic;
  WORD   e_cblp;
  WORD   e_cp;
  WORD   e_crlc;
  WORD   e_cparhdr;
  WORD   e_minalloc;
  WORD   e_maxalloc;
  WORD   e_ss;
  WORD   e_sp;
  WORD   e_csum;
  WORD   e_ip;
  WORD   e_cs;
  WORD   e_lfarlc;
  WORD   e_ovno;
  WORD   e_res[4];
  WORD   e_oemid;
  WORD   e_oeminfo;
  WORD   e_res2[10];
  LONG   e_lfanew;
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

// ...

#include "poppack.h" // #pragma pack(pop)

#include "poppack.h" // #pragma pack(pop)

alignof(IMAGE_DOS_HEADER); // => 2

It would be equivalent to the following GCC C:

typedef struct __attribute__ ((packed)) _IMAGE_DOS_HEADER {
  __attribute__ ((aligned(2))) WORD e_magic;
  __attribute__ ((aligned(2))) WORD e_cblp;
  // ...
  __attribute__ ((aligned(2))) LONG e_lfanew;
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

Additional annotations are necessary as IMAGE_DOS_HEADER's default alignment is 4 bytes and @[Packed] reduces that to 1 byte; there is currently no way to specify any alignment inbetween.

On Windows those alignment requirements are not associated with the struct declaration itself, nor shown in their public documentation. Windows.Win32.winmd exposes these types as having a PackingSize field that is neither 0 nor 1.

Note that setting the alignment for the entire struct is different from setting the alignment for individual elements, whereas setting __attribute__ ((packed)) for the entire struct is equivalent to setting it for all members. LLVM doesn't support these attributes directly and Crystal::LLVMTyper most likely has to emulate the padding fields.

Also note that alignas cannot be used to decrease a type or member's alignment in standard C or C++.

My proposal would be as follows:

  • @[Align(n)] on a type or member, where n is a non-negative power of two, changes its alignment, which may be stricter or less strict than the natural alignment;
  • If multiple @[Align]s are on the same type or member, the strictest one takes effect;
  • @[Packed(n = 1)] on a type, where n is a non-negative power of two, applies @[Align(n)] to all of its members;
  • They should only be allowed on lib or extern structs / unions. It should have been a compilation error to use @[Packed] on non-extern structs or even classes, which do not officially commit to a C-compatible ABI, but apparently this isn't the case.

There should also be ways to query those alignments, see #14033.

HertzDevil avatar Jun 04 '23 21:06 HertzDevil

What's the difference between a @[Align(n)] and @[Packed(n)] in a type?

beta-ziliani avatar Jun 21 '23 18:06 beta-ziliani

@[Align(n)] on a type alone does not change its members' alignments:

lib Lib
  @[Align(16)]
  struct Foo
    x : Int32
    y : Int32
  end
end

offsetof(Lib::Foo, @y) # => 4
sizeof(Lib::Foo)       # => 16

@[Packed(n)] is equivalent to @[Align(n)] on all members:

# these two definitions are ABI-equivalent

lib Lib
  @[Packed(16)]
  struct Foo
    x : Int32
    y : Int32
  end

  # default C ABI rules imply `Foo2`'s alignment is also 16 bytes
  struct Foo2
    @[Align(16)]
    x : Int32
    @[Align(16)]
    y : Int32
  end
end

offsetof(Lib::Foo, @y) # => 16
sizeof(Lib::Foo)       # => 32

These definitions are equivalent:

lib Lib
  @[Packed(4)]
  struct Foo
    @[Align(2)]
    x : UInt8
    @[Align(8)]
    y : UInt8
    z : UInt8
  end

  struct Foo2
    @[Align(4)]
    x : UInt8
    @[Align(8)]
    y : UInt8
    @[Align(4)]
    z : UInt8
  end
end

Technically both annotations can be used together at the type level:

lib Lib
  @[Align(16)]
  @[Packed(4)]
  struct Foo
    x : UInt8
    y : UInt8
  end

  @[Align(1)]
  @[Packed(4)]
  struct Foo2
    x : UInt16
    y : UInt16
  end

  struct Foo3
    x : UInt8
    y : Foo2
  end
end

offsetof(Lib::Foo, @y)  # => 4
sizeof(Lib::Foo)        # => 16
offsetof(Lib::Foo2, @y) # => 4
sizeof(Lib::Foo2)       # => 8
offsetof(Lib::Foo3, @y) # => 1
sizeof(Lib::Foo3)       # => 9

HertzDevil avatar Jun 22 '23 12:06 HertzDevil

ah, that makes sense. thanks!

beta-ziliani avatar Jun 22 '23 20:06 beta-ziliani

I just hit this issue with LibC::CONTEXT being aligned on 16 bytes in the Win32 API. Just @[Align(16)] on the type would be nice to have.

Maybe it could also help with #13609?

ysbaddaden avatar Jun 18 '24 12:06 ysbaddaden