Расположение типов
Расположение типа — это его размер, выравнивание и относительные смещения его полей. Для перечислений частью расположения типа также является то, как размещается и интерпретируется дискриминант.
Расположение типа может меняться с каждой компиляцией. Вместо того чтобы пытаться документировать точные действия, мы документируем только то, что гарантировано на сегодняшний день.
Обратите внимание, что даже типы с одинаковым расположением могут различаться по способу передачи через границы функций. Для совместимости типов с ABI вызовов функций смотрите здесь.
Размер и выравнивание
Все значения имеют выравнивание и размер.
Выравнивание значения указывает, по каким адресам допустимо хранить значение.
Значение с выравниванием n должно храниться только по адресу, кратному n.
Например, значение с выравниванием 2 должно храниться по чётному адресу, а
значение с выравниванием 1 может храниться по любому адресу. Выравнивание
измеряется в байтах, должно быть не менее 1 и всегда является степенью двойки.
Выравнивание значения можно проверить с помощью функции align_of_val.
Размер значения — это смещение в байтах между последовательными элементами в
массиве с данным типом элемента, включая заполнение для выравнивания. Размер
значения всегда кратен его выравниванию. Обратите внимание, что некоторые типы
имеют нулевой размер; 0 считается кратным любому выравниванию (например, на
некоторых платформах тип [u16; 0] имеет размер 0 и выравнивание 2). Размер
значения можно проверить с помощью функции size_of_val.
Типы, у которых все значения имеют одинаковый размер и выравнивание, и оба
известны во время компиляции, реализуют трейт Sized и могут быть проверены
с помощью функций size_of и align_of. Типы, не являющиеся Sized,
известны как типы динамического размера. Поскольку
все значения типа Sized имеют одинаковый размер и выравнивание, мы ссылаемся
на эти общие значения как на размер типа и выравнивание типа соответственно.
Расположение примитивных данных
Размер большинства примитивов приведён в этой таблице.
| Тип | 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 |
usize и isize имеют размер, достаточный для хранения любого адреса на
целевой платформе. Например, на 32-битной цели это 4 байта, а на 64-битной
цели — 8 байт.
Выравнивание примитивов зависит от платформы.
В большинстве случаев их выравнивание равно их размеру, но может быть и меньше.
В частности, i128 и u128 часто выравниваются по 4 или 8 байт, даже если
их размер равен 16, а на многих 32-битных платформах i64, u64 и f64
выравниваются только по 4 байтам, а не по 8.
Расположение указателей и ссылок
Указатели и ссылки имеют одинаковое расположение. Изменяемость указателя или ссылки не меняет расположение.
Указатели на типы с известным размером имеют тот же размер и выравнивание, что и usize.
Указатели на типы без размера сами имеют размер. Гарантируется, что их размер и выравнивание как минимум равны размеру и выравниванию указателя.
Note
Хотя на это не стоит полагаться, в настоящее время все указатели на DST имеют размер в два раза больше, чем
usize, и то же выравнивание.
Расположение массивов
Массив [T; N] имеет размер size_of::<T>() * N и то же выравнивание, что и T.
Массивы располагаются так, что элемент массива с индексом n (начиная с нуля)
смещён от начала массива на n * size_of::<T>() байт.
Расположение срезов
Срезы имеют то же расположение, что и часть массива, которую они срезают.
Note
Речь идёт о самом типе
[T], а не об указателях (&[T],Box<[T]>и т.д.) на срезы.
Расположение str
Строковые срезы — это UTF-8 представление символов, которое имеет то же расположение, что и срезы типа [u8]. Ссылка &str имеет то же расположение, что и ссылка &[u8].
Расположение кортежей
Кортежи располагаются в соответствии с представлением Rust.
Исключением является кортеж-единица (()), который гарантированно является
типом с нулевым размером, имеет размер 0 и выравнивание 1.
Расположение объектов трейтов
Объекты трейтов имеют то же расположение, что и значение, которым является объект трейта.
Note
Речь идёт о самих типах объектов трейтов, а не об указателях (
&dyn Trait,Box<dyn Trait>и т.д.) на них.
Расположение замыканий
Для замыканий нет гарантий расположения.
Представления
Все пользовательские составные типы (struct, enum и union) имеют
представление, которое определяет расположение для этого типа.
Возможные представления для типа:
Rust(по умолчанию)C- Примитивные представления
transparent
Представление типа можно изменить, применив к нему атрибут repr.
Следующий пример показывает структуру с представлением C.
#![allow(unused)] fn main() { #[repr(C)] struct ThreeInts { first: i16, second: i8, third: i32 } }
Выравнивание может быть увеличено или уменьшено с помощью модификаторов align и packed
соответственно. Они изменяют представление, указанное в атрибуте.
Если представление не указано, изменяется представление по умолчанию.
#![allow(unused)] fn main() { // Представление по умолчанию, выравнивание снижено до 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 } }
Note
Поскольку представление является атрибутом элемента, оно не зависит от обобщённых параметров. Любые два типа с одинаковым именем имеют одинаковое представление. Например,
Foo<Bar>иFoo<Baz>имеют одинаковое представление.
Представление типа может изменить заполнение между полями, но не меняет
расположение самих полей. Например, структура с представлением C,
содержащая структуру Inner с представлением Rust, не изменит расположение Inner.
Представление Rust
Представление Rust является представлением по умолчанию для именованных типов
без атрибута repr. Явное использование этого представления через атрибут
repr гарантированно эквивалентно полному отсутствию атрибута.
Единственные гарантии размещения данных, предоставляемые этим представлением, — это те, которые требуются для безопасности. А именно:
- Поля правильно выровнены.
- Поля не перекрываются.
- Выравнивание типа не меньше максимального выравнивания его полей.
Формально первая гарантия означает, что смещение любого поля делится на выравнивание этого поля.
Вторая гарантия означает, что поля можно упорядочить таким образом, что смещение плюс размер любого поля меньше или равно смещению следующего поля в этом порядке. Порядок не обязан совпадать с порядком, в котором поля указаны в объявлении типа.
Имейте в виду, что вторая гарантия не подразумевает, что поля имеют разные адреса: типы с нулевым размером могут иметь тот же адрес, что и другие поля в той же структуре.
Это представление не даёт никаких других гарантий относительно размещения данных.
Представление C
Представление C предназначено для двух целей. Первая цель — создание типов,
совместимых с языком C. Вторая цель — создание типов, над которыми можно безопасно
выполнять операции, зависящие от размещения данных, например, переинтерпретацию
значений как другой тип.
Из-за этой двойной цели возможно создание типов, которые не полезны для взаимодействия с языком программирования C.
Это представление может применяться к структурам, объединениям и перечислениям. Исключением
являются перечисления без вариантов, для которых представление C является ошибкой.
#[repr(C)] Структуры
Выравнивание структуры равно выравниванию самого выровненного поля в ней.
Размер и смещение полей определяются следующим алгоритмом.
Начните с текущего смещения 0 байт.
Для каждого поля в порядке объявления в структуре сначала определите размер и выравнивание поля. Если текущее смещение не кратно выравниванию поля, то добавьте байты заполнения к текущему смещению, пока оно не станет кратным выравниванию поля. Смещение для поля — это полученное текущее смещение. Затем увеличьте текущее смещение на размер поля.
Наконец, размер структуры — это текущее смещение, округлённое до ближайшего кратного выравниванию структуры.
Вот описание этого алгоритма в псевдокоде.
/// Возвращает количество байтов заполнения, необходимых после `offset`, чтобы гарантировать,
/// что следующий адрес будет выровнен по `alignment`.
fn padding_needed_for(offset: usize, alignment: usize) -> usize {
let misalignment = offset % alignment;
if misalignment > 0 {
// Округляем до следующего кратного `alignment`
alignment - misalignment
} else {
// Уже кратно `alignment`
0
}
}
struct.alignment = struct.fields().map(|field| field.alignment).max();
let current_offset = 0;
for field in struct.fields_in_declaration_order() {
// Увеличиваем текущее смещение так, чтобы оно стало кратно выравниванию
// этого поля. Для первого поля это всегда будет ноль.
// Пропущенные байты называются байтами заполнения.
current_offset += padding_needed_for(current_offset, field.alignment);
struct[field].offset = current_offset;
current_offset += field.size;
}
struct.size = current_offset + padding_needed_for(current_offset, struct.alignment);
Warning
Этот псевдокод использует наивный алгоритм, который игнорирует проблемы переполнения ради ясности. Для выполнения вычислений размещения памяти в реальном коде используйте
Layout.
Note
Этот алгоритм может создавать структуры с нулевым размером. В C объявление пустой структуры, такое как
struct Foo { }, недопустимо. Однако и gcc, и clang поддерживают опции для включения таких структур и присваивают им размер ноль. В C++, напротив, пустые структуры имеют размер 1, если они не унаследованы от другой структуры или не являются полями с атрибутом[[no_unique_address]], в этом случае они не увеличивают общий размер структуры.
#[repr(C)] Объединения
Объявление объединения с #[repr(C)] будет иметь тот же размер и выравнивание, что и
эквивалентное объявление объединения на языке C для целевой платформы.
Объединение будет иметь размер, равный максимальному размеру всех его полей, округлённому до его выравнивания, и выравнивание, равное максимальному выравниванию всех его полей. Эти максимумы могут происходить от разных полей.
#![allow(unused)] fn main() { #[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 #[repr(C)] union SizeRoundedUp { a: u32, b: [u16; 3], } assert_eq!(std::mem::size_of::<SizeRoundedUp>(), 8); // Размер 6 из b, // округлённый до 8 из-за // выравнивания a. assert_eq!(std::mem::align_of::<SizeRoundedUp>(), 4); // Из a }
#[repr(C)] Перечисления без полей
Для перечислений без полей представление C имеет размер и выравнивание
по умолчанию для enum в соответствии с C ABI целевой платформы.
Note
Представление перечисления в C определяется реализацией, так что это действительно «наилучшее предположение». В частности, это может быть неверно, когда интересующий код C компилируется с определёнными флагами.
Warning
Существуют crucial различия между
enumв языке C и перечислениями без полей в Rust с этим представлением.enumв C — это в основномtypedefплюс некоторые именованные константы; другими словами, объект типаenumможет содержать любое целочисленное значение. Например, это часто используется для битовых флагов вC. В отличие от этого, перечисления без полей в Rust могут законно содержать только значения дискриминанта, всё остальное — неопределённое поведение. Поэтому использование перечисления без полей в FFI для моделирования Cenumчасто является ошибкой.
#[repr(C)] Перечисления с полями
Представление repr(C) перечисления с полями — это repr(C) структура с
двумя полями, также называемая «помеченное объединение» в C:
repr(C)версия перечисления со всеми удалёнными полями («метка»)
repr(C)объединениеrepr(C)структур для полей каждого варианта, которые имели поля («полезная нагрузка»)
Note
Из-за представления
repr(C)структур и объединений, если вариант имеет одно поле, нет разницы между помещением этого поля непосредственно в объединение или его обёртыванием в структуру; поэтому любая система, которая хочет манипулировать представлением такогоenum, может использовать ту форму, которая для неё более удобна или последовательна.
#![allow(unused)] fn main() { // Это перечисление имеет то же представление, что и ... #[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 } // Эта структура может быть опущена (она является типом с нулевым размером), но должна быть // в заголовках C/C++. #[repr(C)] #[derive(Copy, Clone)] struct MyDFields; }
Примитивные представления
Примитивные представления — это представления с теми же именами, что и
примитивные целочисленные типы. А именно: u8, u16, u32, u64, u128,
usize, i8, i16, i32, i64, i128 и isize.
Примитивные представления могут применяться только к перечислениям и имеют разное поведение в зависимости от того, есть ли у перечисления поля. Для перечислений без вариантов примитивное представление является ошибкой. Совмещение двух примитивных представлений вместе является ошибкой.
Примитивное представление перечислений без полей
Для перечислений без полей примитивные представления устанавливают размер и выравнивание
такими же, как у примитивного типа с тем же именем. Например, перечисление без полей
с представлением u8 может иметь дискриминанты только от 0 до 255 включительно.
Примитивное представление перечислений с полями
Представление перечисления с примитивным представлением — это repr(C) объединение
repr(C) структур для каждого варианта с полем. Первое поле каждой структуры
в объединении — это версия перечисления с примитивным представлением со всеми удалёнными полями
(«метка»), а остальные поля — это поля этого варианта.
Note
Это представление не изменится, если метке выделить собственный член в объединении, если это сделает манипуляции более понятными для вас (хотя, чтобы соответствовать стандарту C++, член-метка должен быть обёрнут в
struct).
#![allow(unused)] fn main() { // Это перечисление имеет то же представление, что и ... #[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 байт.
Перечисление дискриминантов из предыдущего примера тогда становится:
#![allow(unused)] fn main() { #[repr(C, u8)] // `u8` был добавлен enum MyEnum { A(u32), B(f32, u64), C { x: u32, y: u8 }, D, } // ... #[repr(u8)] // Поэтому здесь используется `u8` вместо `C` enum MyEnumDiscriminant { A, B, C, D } // ... }
Например, для перечисления repr(C, u8) невозможно иметь 257 уникальных
дискриминантов («меток»), тогда как то же перечисление только с атрибутом repr(C)
скомпилируется без проблем.
Использование примитивного представления в дополнение к repr(C) может изменить размер
перечисления по сравнению с формой repr(C):
#![allow(unused)] fn main() { #[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 assert_eq!(std::mem::size_of::<Enum8>(), 2); // Два байта для дискриминанта и один байт для значения в Enum16::Variant0 // плюс один байт заполнения. assert_eq!(std::mem::size_of::<Enum16>(), 4); }
Модификаторы выравнивания
Модификаторы align и packed могут использоваться для повышения или понижения
выравнивания struct и union соответственно. packed также может изменять заполнение
между полями (хотя он не будет изменять заполнение внутри любого поля).
Само по себе align и packed не дают гарантий относительно порядка
полей в расположении структуры или расположении варианта перечисления, хотя
они могут комбинироваться с представлениями (такими как C), которые предоставляют такие
гарантии.
Выравнивание задаётся целочисленным параметром в форме
#[repr(align(x))] или #[repr(packed(x))]. Значение выравнивания должно быть
степенью двойки от 1 до 229. Для packed, если значение не задано,
как в #[repr(packed)], то значение равно 1.
Для align, если указанное выравнивание меньше выравнивания типа
без модификатора align, то выравнивание не изменяется.
Для packed, если указанное выравнивание больше выравнивания типа
без модификатора packed, то выравнивание и расположение не изменяются.
Выравнивания каждого поля, для целей позиционирования полей, берутся как меньшее из указанного выравнивания и выравнивания типа поля.
Заполнение между полями гарантированно является минимально необходимым для
удовлетворения (возможно, изменённого) выравнивания каждого поля (хотя обратите внимание,
что само по себе packed не даёт никаких гарантий относительно порядка полей). Важным
следствием этих правил является то, что тип с #[repr(packed(1))]
(или #[repr(packed)]) не будет иметь заполнения между полями.
Модификаторы align и packed не могут применяться к одному и тому же типу, и
packed тип не может транзитивно содержать другой align тип. align и
packed могут применяться только к представлениям Rust и C.
Модификатор align также может применяться к enum.
Когда это сделано, эффект на выравнивание enum такой же, как если бы enum
был обёрнут в новую структуру (struct) с тем же модификатором align.
Note
Ссылки на невыровненные поля запрещены, так как это неопределённое поведение. Когда поля не выровнены из-за модификатора выравнивания, рассмотрите следующие варианты использования ссылок и разыменований:
#![allow(unused)] fn main() { #[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
Представление transparent может использоваться только на struct
или enum с одним вариантом, который имеет:
- любое количество полей с размером 0 и выравниванием 1 (например,
PhantomData<T>), и - не более одного другого поля.
Структуры и перечисления с этим представлением имеют то же расположение и ABI, что и единственное поле с ненулевым размером или выравниванием не 1, если оно присутствует, или как unit-тип в противном случае.
Это отличается от представления C, потому что
структура с представлением C всегда будет иметь ABI C struct,
в то время как, например, структура с представлением transparent с
примитивным полем будет иметь ABI этого примитивного поля.
Поскольку это представление делегирует расположение типа другому типу, оно не может быть использовано с любым другим представлением.