타입 레이아웃

타입 레이아웃 (Type layout)

어떤 타입이 메모리에서 얼마만큼의 공간을 차지하고, 어떻게 정렬되는지 궁금할 때가 있어요. 특히 C와 상호 운용하거나 성능에 민감한 코드에서 이 레이아웃 문제가 중요해지죠. 이 페이지에서 타입 레이아웃이 보장하는 것들을 정리해볼게요.

출처: Rust Reference

본문

타입의 레이아웃(layout)은 그 크기(size), 정렬(alignment), 그리고 **필드들의 상대적 오프셋(relative offset)**이에요. 열거형의 경우, 판별자(discriminant)가 어떻게 배치되고 해석되는지도 타입 레이아웃의 일부예요.

타입 레이아웃은 컴파일할 때마다 바뀔 수 있어요. 정확히 무엇이 수행되는지 문서화하려고 하기보다, 오늘 보장되는 것만 문서화해요. 같은 레이아웃을 가진 타입이라도 함수 경계를 넘어 어떻게 전달되는지는 다를 수 있다는 점에 유의하세요. 함수 호출 ABI 호환성에 대해서는 별도 문서를 참조해요.

크기와 정렬 (Size and alignment)

모든 값에는 정렬과 크기가 있어요.

**정렬(alignment)**은 값이 저장될 수 있는 유효한 주소가 무엇인지 지정해요. 정렬이 n인 값은 n의 배수인 주소에만 저장되어야 해요. 예를 들어 정렬이 2인 값은 짝수 주소에 저장되어야 하고, 정렬이 1인 값은 어떤 주소에도 저장될 수 있어요. 정렬은 바이트 단위로 측정되며, 최소 1이어야 하고, 항상 2의 거듭제곱이에요. 값의 정렬은 align_of_val 함수로 확인할 수 있어요.

**크기(size)**는 그 아이템 타입을 가진 배열에서 연속된 요소들 사이의 바이트 단위 오프셋으로, 정렬 패딩을 포함해요. 값의 크기는 항상 그 정렬의 배수예요. 일부 타입은 크기가 0(zero-sized)인데, 0은 어떤 정렬의 배수로도 간주돼요 (예를 들어 일부 플랫폼에서 [u16; 0] 타입은 크기 0과 정렬 2를 가져요). 값의 크기는 size_of_val 함수로 확인할 수 있어요.

모든 값이 같은 크기와 정렬을 가지면서 둘 다 컴파일 타임에 알려지는 타입은 Sized 트레이트를 구현하며, size_ofalign_of 함수로 확인할 수 있어요. Sized가 아닌 타입은 **동적 크기 타입(dynamically sized type)**이라고 알려져 있어요. Sized 타입의 모든 값이 같은 크기와 정렬을 공유하므로, 우리는 그 공유된 값들을 각각 타입의 크기와 타입의 정렬이라고 불러요.

기본 데이터 레이아웃 (Primitive data layout)

대부분의 기본 타입의 크기는 다음 표에 나와 있어요.

타입 size_of::<Type>()
bool 1
u8 / i8 1
u16 / i16 2
u32 / i32 4
u64 / i64 8
u128 / i128 16
usize / isize 아래 참조
f32 4
f64 8
char 4

usizeisize는 대상 플랫폼의 모든 주소를 담을 수 있을 만큼 큰 크기를 가져요. 예를 들어 32비트 대상에서는 4바이트, 64비트 대상에서는 8바이트예요. usizeisize는 같은 크기와 정렬을 가져요.

기본 타입의 정렬은 플랫폼 특정적이에요. 대부분의 경우 정렬은 크기와 같지만, 더 작을 수도 있어요. 특히 i128u128은 크기가 16인데도 종종 4바이트나 8바이트로 정렬돼요. 그리고 많은 32비트 플랫폼에서 i64, u64, f64는 8바이트가 아니라 4바이트로만 정렬돼요.

정렬은 같은 표시 크기의 고정 너비 부호/무부호 정수 변형에 대해 같다고 보장돼요. 즉, 주어진 크기 N에 대해 align_of::<uN>() == align_of::<iN>()이에요.

포인터와 참조 레이아웃 (Pointers and references layout)

포인터와 참조는 같은 레이아웃을 가져요. 포인터나 참조의 가변성은 레이아웃을 바꾸지 않아요. 크기 있는 타입에 대한 포인터는 usize와 같은 크기와 정렬을 가져요.

크기 없는 타입에 대한 포인터는 크기가 있어요. 크기 없는 타입에 대한 포인터의 크기와 정렬은 각각 크기 있는 타입에 대한 포인터의 그것보다 크거나 같다고 보장돼요.

참고: 이것에 의존해서는 안 되지만, 현재 DST에 대한 모든 포인터는 usize 크기의 두 배 크기이고 같은 정렬을 가져요.

배열 레이아웃 (Array layout)

[T; N] 배열은 크기가 size_of::<T>() * N이고, T와 같은 정렬을 가져요. 배열은 0 기반의 n번째 요소가 배열의 시작에서 n * size_of::<T>() 바이트만큼 떨어져 있도록 배치돼요.

슬라이스 레이아웃 (Slice layout)

슬라이스는 그들이 슬라이스하는 배열의 구간과 같은 레이아웃을 가져요.

참고: 이는 슬라이스에 대한 포인터(&[T], Box<[T]> 등)가 아니라 원시 [T] 타입에 관한 것이에요.

str 레이아웃

문자열 슬라이스는 [u8] 타입의 슬라이스와 같은 레이아웃을 가진 문자들의 UTF-8 표현이에요. 참조 &str은 참조 &[u8]과 같은 레이아웃을 가져요.

튜플 레이아웃 (Tuple layout)

튜플은 Rust 표현(Rust representation)에 따라 배치돼요. 이 규칙의 예외는 단위 튜플 (())인데, 이는 크기 0과 정렬 1을 가진 zero-sized 타입임이 보장돼요.

트레이트 객체 레이아웃 (Trait object layout)

트레이트 객체는 그 트레이트 객체가 나타내는 값과 같은 레이아웃을 가져요.

참고: 이는 트레이트 객체에 대한 포인터(&dyn Trait, Box<dyn Trait> 등)가 아니라 원시 트레이트 객체 타입에 관한 것이에요.

클로저 레이아웃 (Closure layout)

클로저는 레이아웃 보장이 없어요.

표현 (Representations)

모든 사용자 정의 복합 타입(구조체, 열거형, 공용체)은 그 타입의 레이아웃이 무엇인지를 지정하는 표현을 가져요. 타입에 가능한 표현들은 다음과 같아요.

  • Rust (기본값)
  • C
  • 기본 표현들 (primitive representations)
  • transparent

타입의 표현은 그 타입에 repr 속성을 적용해서 바꿀 수 있어요. 다음 예시는 C 표현을 가진 구조체를 보여줘요.

#[repr(C)]
struct ThreeInts {
    first: i16,
    second: i8,
    third: i32
}

정렬은 alignpacked 수정자로 각각 높이거나 낮출 수 있어요. 이들은 속성에 지정된 표현을 변경해요. 표현이 지정되지 않았다면 기본값이 변경돼요.

// 기본 표현, 정렬이 2로 낮아짐.
#[repr(packed(2))]
struct PackedStruct {
    first: i16,
    second: i8,
    third: i32
}

// C 표현, 정렬이 8로 높아짐.
#[repr(C, align(8))]
struct AlignedStruct {
    first: i16,
    second: i8,
    third: i32
}

참고: 표현이 아이템에 대한 속성이기 때문에, 표현은 제네릭 매개변수에 의존하지 않아요. 같은 이름을 가진 어떤 두 타입도 같은 표현을 가져요. 예를 들어 Foo<Bar>Foo<Baz>는 둘 다 같은 표현을 가져요.

타입의 표현은 필드 사이의 패딩을 바꿀 수 있지만, 필드 자체의 레이아웃은 바꾸지 않아요. 예를 들어 Rust 표현의 Inner 구조체를 담은 C 표현 구조체는 Inner의 레이아웃을 바꾸지 않아요.

Rust 표현 (The Rust representation)

Rust 표현은 repr 속성이 없는 명목 타입(nominal type)의 기본 표현이에요. repr 속성을 통해 이 표현을 명시적으로 사용하는 것은 속성을 아예 생략하는 것과 같다고 보장돼요.

이 표현이 만드는 유일한 데이터 레이아웃 보장은 건전성에 필요한 것들이에요.

  • 필드의 오프셋은 그 필드의 정렬로 나누어 떨어져요.
  • 타입의 정렬은 최소한 그 필드들의 최대 정렬이에요.

구조체의 경우, 필드들이 겹치지 않는다는 것이 추가로 보장돼요. 즉, 필드들은 어떤 필드의 오프셋에 그 크기를 더한 값이 그 순서의 다음 필드의 오프셋보다 작거나 같도록 정렬될 수 있어요. 그 순서는 타입 선언에서 필드가 지정된 순서와 같을 필요는 없어요. 이 보장이 필드들이 서로 다른 주소를 가진다는 것을 뜻하지는 않는다는 점에 유의하세요. zero-sized 타입은 같은 구조체의 다른 필드와 같은 주소를 가질 수 있어요.

이 표현에 의한 데이터 레이아웃의 다른 보장은 없어요.

C 표현 (The C representation)

C 표현은 이중 목적을 위해 설계됐어요. 하나는 C 언어와 상호 운용 가능한 타입을 만드는 것이고, 다른 하나는 값 재해석처럼 데이터 레이아웃에 의존하는 연산을 안전하게 수행할 수 있는 타입을 만드는 것이에요. 이 이중 목적 때문에, C 프로그래밍 언어와의 인터페이싱에 유용하지 않은 타입을 만들 수도 있어요.

이 표현은 구조체, 공용체, 열거형에 적용할 수 있어요. 예외는 변형이 0개인 열거형인데, 이 경우 C 표현은 오류예요.

#[repr(C)] 구조체

구조체의 정렬은 그 안에서 가장 정렬이 높은 필드의 정렬이거나, 필드가 없으면 1이에요.

필드의 크기와 오프셋은 다음 알고리즘으로 결정돼요.

  1. 현재 오프셋 0바이트에서 시작해요.
  2. 구조체의 각 필드를 선언 순서대로 처리하면서, 먼저 필드의 크기와 정렬을 결정해요. 현재 오프셋이 필드의 정렬의 배수가 아니면, 배수가 될 때까지 현재 오프셋에 패딩 바이트를 추가해요. 필드의 오프셋은 지금의 현재 오프셋이에요. 그런 다음 현재 오프셋을 필드의 크기만큼 증가시켜요.
  3. 마지막으로, 구조체의 크기는 현재 오프셋을 구조체의 정렬의 가장 가까운 배수로 올림한 값이에요.

이 알고리즘은 다음과 같아요.

/// 구조체의 필드.
#[derive(Debug)]
struct Field {
    alignment: usize,
    size: usize,
}
/// 사용자 정의 구조체의 레이아웃.
#[derive(Debug)]
struct MockLayout {
    /// 선언 순서로 저장된 필드들.
    fields: Vec<Field>,
    /// 구조체 시작에서 각 필드까지의 오프셋.
    field_offsets: Vec<usize>,
    /// 전체 정렬.
    alignment: usize,
    /// 전체 크기.
    size: usize,
}

impl MockLayout {
    /// `offset` 뒤에 필요한 패딩 양을 반환하여 다음 주소가
    /// `alignment`에 정렬되도록 해요.
    fn padding_needed_for(offset: usize, alignment: usize) -> usize {
        let misalignment = offset % alignment;
        if misalignment > 0 {
            // `alignment`의 다음 배수로 올림.
            alignment - misalignment
        } else {
            // 이미 `alignment`의 배수.
            0
        }
    }

    /// 필드들은 선언 순서여야 해요. 이 시점에는 이미 정렬과 크기가
    /// 계산되어 있어요.
    pub fn from_fields(fields: Vec<Field>) -> Self {
        // "구조체의 정렬은 그 안에서 가장 정렬이 높은 필드의 정렬이거나,
        //  필드가 없으면 1이에요."
        let alignment = fields
            .iter()
            .map(|field| field.alignment)
            .max()
            .unwrap_or(1);

        // "현재 오프셋 0바이트에서 시작해요."
        let mut current_offset = 0;

        let mut field_offsets = vec![];
        for field in &fields {
            // "현재 오프셋이 필드의 정렬의 배수가 아니면, 배수가 될 때까지
            //  현재 오프셋에 패딩 바이트를 추가해요."
            current_offset += Self::padding_needed_for(
                current_offset,
                field.alignment
            );

            // "필드의 오프셋은 지금의 현재 오프셋이에요."
            field_offsets.push(current_offset);

            // "그런 다음 현재 오프셋을 필드의 크기만큼 증가시켜요."
            current_offset += field.size;
        }

        // "마지막으로, 구조체의 크기는 현재 오프셋을 구조체의 정렬의
        //  가장 가까운 배수로 올림한 값이에요."
        let size = current_offset + Self::padding_needed_for(
            current_offset,
            alignment
        );

        MockLayout { fields, field_offsets, alignment, size }
    }
}

#[repr(C)]
struct Demo {
    first: u8,
    second: u32,
    third: u64,
}
macro_rules! fields {
    ( $( $t:ty ),+ ) => {
        vec![
            $( Field {
                alignment: std::mem::align_of::<$t>(),
                size: std::mem::size_of::<$t>(),
            }),+
        ]
    }
}
let fields = fields![u8, u32, u64];
let demo_layout = MockLayout::from_fields(fields);
assert_eq!(std::mem::align_of::<Demo>(), demo_layout.alignment);
assert_eq!(std::mem::size_of::<Demo>(), demo_layout.size);

경고: 이 mock 구현은 명확성을 위해 오버플로 문제를 무시한 단순한 알고리즘을 사용해요. 실제 코드에서 메모리 레이아웃 계산을 수행하려면 Layout을 사용하세요.

참고: 이 알고리즘은 zero-sized 구조체를 만들 수 있어요. C에서 struct Foo { } 같은 빈 구조체 선언은 불법이에요. 그러나 gcc와 clang 모두 그러한 구조체를 활성화하는 옵션을 지원하고, 크기 0을 부여해요. 반면 C++는 빈 구조체에 크기 1을 부여하는데, 상속받거나 [[no_unique_address]] 속성을 가진 필드인 경우는 예외이며 그 경우 구조체의 전체 크기를 늘리지 않아요.

#[repr(C)] 공용체

#[repr(C)]로 선언된 공용체는 대상 플랫폼에서 C 언어의 동등한 C 공용체 선언과 같은 크기와 정렬을 가져요.

공용체는 모든 필드의 최대 크기를 그 정렬로 올림한 크기를 갖고, 모든 필드의 최대 정렬을 정렬로 가져요. 이 최대값들은 서로 다른 필드에서 올 수 있어요. 각 필드는 공용체의 시작에서 바이트 오프셋 0에 살아요.

#[repr(C)]
union Union {
    f1: u16,
    f2: [u8; 4],
}

assert_eq!(std::mem::size_of::<Union>(), 4);  // f2에서
assert_eq!(std::mem::align_of::<Union>(), 2); // f1에서

assert_eq!(std::mem::offset_of!(Union, f1), 0);
assert_eq!(std::mem::offset_of!(Union, f2), 0);

#[repr(C)]
union SizeRoundedUp {
   a: u32,
   b: [u16; 3],
}

assert_eq!(std::mem::size_of::<SizeRoundedUp>(), 8);  // b에서 크기 6,
                                                      // a의 정렬에서 8로 올림.
assert_eq!(std::mem::align_of::<SizeRoundedUp>(), 4); // a에서

assert_eq!(std::mem::offset_of!(SizeRoundedUp, a), 0);
assert_eq!(std::mem::offset_of!(SizeRoundedUp, b), 0);
#[repr(C)] 필드 없는 열거형

필드 없는 열거형에 대해 C 표현은 대상 플랫폼 C ABI의 기본 열거형 크기와 정렬을 가져요.

참고: C의 열거형 표현은 구현 정의여서, 이는 사실상 "최선의 추측"이에요. 특히 관심 있는 C 코드가 특정 플래그로 컴파일될 때는 틀릴 수 있어요.

경고: C 언어의 열거형과 이 표현을 가진 Rust의 필드 없는 열거형 사이에는 결정적인 차이가 있어요. C의 열거형은 대부분 typedef에 몇 개의 명명된 상수를 더한 것이에요. 다시 말해, 열거형 타입의 객체는 어떤 정수 값이든 담을 수 있어요. 예를 들어 이는 C에서 비트플래그에 자주 사용돼요. 반면 Rust의 필드 없는 열거형은 판별자 값만 합법적으로 담을 수 있고, 다른 모든 것은 정의되지 않은 동작이에요. 그러므로 FFI에서 필드 없는 열거형으로 C 열거형을 모델링하는 것은 종종 틀려요.

#[repr(C)] 필드 있는 열거형

필드 있는 repr(C) 열거형의 표현은 두 필드를 가진 repr(C) 구조체이며, C에서는 "태그 공용체(tagged union)"라고도 불려요.

  • 필드를 모두 제거한 열거형의 repr(C) 버전 ("태그")
  • 필드를 가졌던 각 변형의 필드들에 대한 repr(C) 구조체들의 repr(C) 공용체 ("페이로드")

참고: repr(C) 구조체와 공용체의 표현 때문에, 변형이 단일 필드를 가진다면 그 필드를 공용체에 직접 넣는 것과 구조체로 감싸는 것 사이에 차이가 없어요. 그러한 열거형의 표현을 조작하려는 어떤 시스템이라도 더 편리하거나 일관된 형태를 사용할 수 있어요.

// 이 열거형은 다음 구조체와 같은 표현을 가져요 ...
#[repr(C)]
enum MyEnum {
    A(u32),
    B(f32, u64),
    C { x: u32, y: u8 },
    D,
 }

// ... 이 구조체와.
#[repr(C)]
struct MyEnumRepr {
    tag: MyEnumDiscriminant,
    payload: MyEnumFields,
}

// 이건 판별자 열거형이에요.
#[repr(C)]
enum MyEnumDiscriminant { A, B, C, D }

// 이건 변형 공용체예요.
#[repr(C)]
union MyEnumFields {
    A: MyAFields,
    B: MyBFields,
    C: MyCFields,
    D: MyDFields,
}

#[repr(C)]
#[derive(Copy, Clone)]
struct MyAFields(u32);

#[repr(C)]
#[derive(Copy, Clone)]
struct MyBFields(f32, u64);

#[repr(C)]
#[derive(Copy, Clone)]
struct MyCFields { x: u32, y: u8 }

// 이 구조체는 생략될 수 있고 (zero-sized 타입이에요), C/C++ 헤더에는
// 있어야 해요.
#[repr(C)]
#[derive(Copy, Clone)]
struct MyDFields;

기본 표현들 (Primitive representations)

기본 표현들은 기본 정수 타입과 같은 이름을 가진 표현들이에요. 즉 u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize예요.

기본 표현은 열거형에만 적용할 수 있고, 열거형에 필드가 있는지에 따라 다르게 동작해요. 변형이 0개인 열거형에 기본 표현을 붙이는 것은 오류예요. 두 기본 표현을 함께 결합하는 것도 오류예요.

필드 없는 열거형의 기본 표현: 필드 없는 열거형에 대해 기본 표현은 같은 이름의 기본 타입과 크기와 정렬을 같게 설정해요. 예를 들어 u8 표현을 가진 필드 없는 열거형은 0부터 255까지(포함)의 판별자만 가질 수 있어요.

필드 있는 열거형의 기본 표현: 기본 표현 열거형의 표현은 필드가 있는 각 변형에 대한 repr(C) 구조체들의 repr(C) 공용체예요. 공용체의 각 구조체의 첫 번째 필드는 필드를 모두 제거한 열거형의 기본 표현 버전("태그")이고, 나머지 필드들은 그 변형의 필드들이에요.

참고: 태그가 공용체에 자신의 멤버로 주어진다면 (C++ 표준을 따르려면 태그 멤버를 구조체로 감싸야 하지만) 이 표현은 바뀌지 않아요. 조작을 더 명확하게 만들어 준다면요.

// 이 열거형은 다음 공용체와 같은 표현을 가져요 ...
#[repr(u8)]
enum MyEnum {
    A(u32),
    B(f32, u64),
    C { x: u32, y: u8 },
    D,
 }

// ... 이 공용체와.
#[repr(C)]
union MyEnumRepr {
    A: MyVariantA,
    B: MyVariantB,
    C: MyVariantC,
    D: MyVariantD,
}

// 이건 판별자 열거형이에요.
#[repr(u8)]
#[derive(Copy, Clone)]
enum MyEnumDiscriminant { A, B, C, D }

#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantA(MyEnumDiscriminant, u32);

#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantB(MyEnumDiscriminant, f32, u64);

#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantC { tag: MyEnumDiscriminant, x: u32, y: u8 }

#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantD(MyEnumDiscriminant);

필드 있는 열거형의 기본 표현과 #[repr(C)] 결합: 필드 있는 열거형에 대해 repr(C)과 기본 표현을 결합하는 것도 가능해요 (예: repr(C, u8)). 이는 판별자 열거형의 표현을 선택된 기본 표현으로 바꿈으로써 repr(C)을 수정해요. 그래서 u8 표현을 선택했다면, 판별자 열거형은 크기와 정렬이 1바이트가 돼요.

앞의 예시의 판별자 열거형은 그렇게 되어요.

#[repr(C, u8)] // `u8`이 추가됨
enum MyEnum {
    A(u32),
    B(f32, u64),
    C { x: u32, y: u8 },
    D,
 }

// ...

#[repr(u8)] // 그래서 여기서는 `C` 대신 `u8`이 사용됨
enum MyEnumDiscriminant { A, B, C, D }

// ...

예를 들어 repr(C, u8) 열거형에서는 257개의 고유한 판별자("태그")를 가질 수 없는 반면, repr(C) 속성만 가진 같은 열거형은 아무 문제 없이 컴파일돼요. repr(C)에 더해 기본 표현을 사용하면 열거형의 크기를 repr(C) 형태에서 바꿀 수 있어요.

#[repr(C)]
enum EnumC {
    Variant0(u8),
    Variant1,
}

#[repr(C, u8)]
enum Enum8 {
    Variant0(u8),
    Variant1,
}

#[repr(C, u16)]
enum Enum16 {
    Variant0(u8),
    Variant1,
}

// C 표현의 크기는 플랫폼에 따라 달라요
assert_eq!(std::mem::size_of::<EnumC>(), 8);
// Enum8::Variant0의 판별자 1바이트와 값 1바이트
assert_eq!(std::mem::size_of::<Enum8>(), 2);
// Enum16::Variant0의 판별자 2바이트와 값 1바이트
// 그리고 패딩 1바이트.
assert_eq!(std::mem::size_of::<Enum16>(), 4);

정렬 수정자 (The alignment modifiers)

alignpacked 수정자는 각각 구조체와 공용체의 정렬을 높이거나 낮추는 데 사용할 수 있어요. packed는 필드 사이의 패딩도 바꿀 수 있어요 (단, 어떤 필드 내부의 패딩은 바꾸지 않아요). 그 자체로 alignpacked는 구조체 레이아웃에서 필드의 순서나 열거형 변형의 레이아웃에 대한 보장을 제공하지 않아요. 다만 그러한 보장을 제공하는 표현들(예: C)과 결합될 수는 있어요.

정렬은 #[repr(align(x))] 또는 #[repr(packed(x))] 형태의 정수 매개변수로 지정돼요. 정렬 값은 1부터 2^29까지의 2의 거듭제곱이어야 해요. packed의 경우, #[repr(packed)]처럼 값이 주어지지 않으면 값은 1이에요.

align의 경우, 지정된 정렬이 align 수정자 없이 타입이 가지는 정렬보다 작으면 정렬은 영향을 받지 않아요. packed의 경우, 지정된 정렬이 packed 수정자 없이 타입이 가지는 정렬보다 크면 정렬과 레이아웃은 영향을 받지 않아요.

필드를 배치하는 목적에서 각 필드의 정렬은 지정된 정렬과 필드 타입의 정렬 중 더 작은 값이에요. 필드 간 패딩은 각 필드의 (잠재적으로 변경된) 정렬을 만족시키기 위해 필요한 최소값임이 보장돼요 (단, packed 그 자체는 필드 순서에 대한 보장을 제공하지 않는다는 점에 유의하세요). 이 규칙들의 중요한 결과는 #[repr(packed(1))] (또는 #[repr(packed)]) 타입은 필드 간 패딩이 없다는 것이에요.

alignpacked 수정자는 같은 타입에 적용할 수 없고, packed 타입은 전이적으로 다른 aligned 타입을 포함할 수 없어요. alignpacked는 Rust와 C 표현에만 적용할 수 있어요.

align 수정자는 열거형에도 적용할 수 있어요. 적용되면 열거형의 정렬에 대한 효과는 같은 align 수정자를 가진 newtype 구조체로 열거형을 감싼 것과 같아요.

참고: 정렬되지 않은 필드에 대한 참조는 정의되지 않은 동작이기 때문에 허용되지 않아요. 정렬 수정자 때문에 필드가 정렬되지 않았을 때, 참조와 역참조를 사용하는 데 대해 다음 옵션들을 고려해 보세요.

#[repr(packed)]
struct Packed {
    f1: u8,
    f2: u16,
}
let mut e = Packed { f1: 1, f2: 2 };
// 필드에 대한 참조를 만드는 대신, 값을 지역 변수로 복사해요.
let x = e.f2;
// `println!`처럼 참조를 만드는 상황에서는, 중괄호를 사용해 값의
// 복사로 바꿔요.
println!("{}", {e.f2});
// 포인터가 필요하다면, 포인터를 직접 역참조하는 대신 정렬되지 않은
// 읽기/쓰기 메서드를 사용해요.
let ptr: *const u16 = &raw const e.f2;
let value = unsafe { ptr.read_unaligned() };
let mut_ptr: *mut u16 = &raw mut e.f2;
unsafe { mut_ptr.write_unaligned(3) }

transparent 표현 (The transparent representation)

transparent 표현은 다음을 가지는 단일 변형을 가진 구조체 또는 열거형에만 사용할 수 있어요.

  • 크기 0과 정렬 1을 가진 임의의 수의 필드 (예: PhantomData<T>), 그리고
  • 최대 하나의 다른 필드.

이 표현을 가진 구조체와 열거형은 유일한 크기 0이 아닌, 정렬 1이 아닌 필드(있을 경우)와 같은 레이아웃과 ABI를 가지거나, 그런 필드가 없으면 단위(unit)와 같아요.

이것은 C 표현과 다른데, C 표현을 가진 구조체는 항상 C 구조체의 ABI를 갖는 반면, 예를 들어 기본 필드를 가진 transparent 표현 구조체는 그 기본 필드의 ABI를 갖기 때문이에요.

이 표현은 타입 레이아웃을 다른 타입에 위임하기 때문에, 어떤 다른 표현과도 함께 사용할 수 없어요.

더 알아보기 (Learn more)

  • Rust Reference의 타입 레이아웃 문서에서 전체 내용을 확인할 수 있어요.
  • Rust Reference의 함수 호출 규약 문서에서 타입이 함수 경계를 넘어 어떻게 전달되는지(ABI)를 다룬답니다.
  • Rust Reference의 속성 문서에서 repr을 포함한 속성 전체를 살펴볼 수 있어요.