Продолжаем изучать Rust нетрадиционным способом. В этом раз будем разбираться в нюансах работы с кучей, для их понимания понадобятся: сырые указатели, выделение памяти в куче, размер экземпляра типа, запись значений в кучу и чтение из нее, const и static, unit-like структуры, переопределение глобального аллокатора.
Это, определенно, overkill для одной статьи, а вот половину списка вполне можно освоить.
- Предыдущая часть: Времена и структуры
- Начало и содержание: Владение
Сырые указатели (Raw Pointers)
Указатель на неизменяемое значение:
let i: i32 = 10;
let pi = &i as *const i32;
unsafe {
dbg!(*pi);
}
Указатель на изменяемое значение:
let mut i: i32 = 10;
let p_i = &mut i as *mut i32;
unsafe {
*p_i = 20;
println!("*p_i: {}", *p_i)
}
let i: i32 = 0x_10_20_30_40;
let p_i = &i as *const _ as *mut i16;
unsafe{
*p_i = 0x_70_80;
*p_i.offset(1) = 0x_50_60;
}
println!("i: {:x}", i);
- Брать адреса можно сколько угодно, а вот разыменование указателя — опасная затея, так что добро пожаловать на территорию
unsafe{}
- Для ряда случаев, например, при нестандартном выравнивании или работой с неинициализированной памятью, надо использовать ptr::addr_of!() / ptr::addr_of_mut!()
- Документация по методам сырых указателей: primitive.pointer
Выделение и освобождение памяти
Через std::alloc::alloc(), std::alloc::dealloc():
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main(){
let ppoint = alloc_t::<Point>();
unsafe {
(*ppoint).x = 10;
(*ppoint).y = 20;
println!("*ppoint: {:?}", *ppoint);
}
dealloc_t(ppoint);
}
fn alloc_t<T> () -> * mut T{
let layout = std::alloc::Layout::new::<T>();
unsafe {
let res = std::alloc::alloc(layout) as *mut T;
if res.is_null() {
std::alloc::handle_alloc_error(layout);
}
return res;
}
}
fn dealloc_t<T> (p: *mut T) {
let layout = std::alloc::Layout::new::<T>();
unsafe {
std::alloc::dealloc(p as *mut u8, layout);
}
}
- Пара alloc()/dealloc() указана в The Rustonomicon
при разборе RawVec
- Nomicon то ли отстает, то ли упрощает (в смысле, указанная пара уже не используется)
- Как обстоят дела на самом деле с
RawVec
, рассмотрим позже, для этого нужны неведомые пока (в рамках серии статей) конструкции языка - Вызов
handle_alloc_error
— рекомендованный ("...are encouraged to call this function") способ обработки ошибок выделения памяти -
handle_alloc_error()
имеет сигнатуруpub fn handle_alloc_error(layout: Layout) -> !
— "фатальная" функция, из таких не возвращаются
См. также:
Размер экземпляра типа
Научившись выделять память полезно понимать, на что она пойдет. По отношению к размеру типы бывают:
1. Sized Types. Их размер известен во время компиляции и можно создать экземпляр типа. Несколько примеров, где размер экземпляра больше нуля:
...
dbg!(mem::size_of::<bool>());
dbg!(mem::align_of::<bool>());
dbg!(mem::size_of::<[i32; 50]>());
dbg!(mem::align_of::<[i32; 50]>());
dbg!(mem::size_of::<PointTuple>());
dbg!(mem::align_of::<PointTuple>());
...
Пора сказать пару слов про кортеж (tuple). Это структура с безымянными полями:
#[derive(Debug)]
struct PointTuple(i32, i32);
fn main() {
let mut pt = PointTuple(10, 20);
pt.0 = 100;
pt.1 = 200;
dbg!(pt);
}
2. Zero Sized Types (ZST). Подмножество Sized, размер экземпляра типа равен нулю, но все еще можно его создать.
К ZST относятся:
- Пустые структуры
- Unit-like структуры
- Пустые кортежи
- Пустые массивы
!!! Подавать layout таких типов в функции выделения памяти категорически нельзя
Ну т.е. можно, но результатом будет undefined behavior.
3. Empty Types. Экзотические типы, экземпляров которых не существует.
Пустой enum
:
enum ZeroVariants {}
NeverType
(на текущий момент тип не "стабилизирован"):
let x: ! = panic!();
4. Dynamically Sized Types (DSTs). Размер таких типов неизвестен во время компиляции:
- интерфейсы (traits);
- срезы (slices): [T], str.
Rust не примет такую запись:
let s1: str = "Hello there!";
Интересный вопрос — почему, ведь можно посчитать размер памяти, которая требуется для "Hello there!"? Есть требование, что все экземпляры Sized-типа должны иметь одинаковый размер, вот ему-то значения str и не соответствуют (т.е. единого размера нет), так что — &str
и DST.
Далее, если интересно, см.:
Запись / чтение
Теперь у нас все готово для того, чтобы отправлять переменные в Сумрак и выводить обратно.
Туда:
let ppoint = alloc_t::<Point>();
// Write to heap
{
let p = Point{x: 101, y:201};
unsafe {ppoint.write(p)}
println!("ppoint.write() completed");
}
-
Важно: Деструктор для
p
при этом НЕ вызывается, т.е. Rust в глубинах вызова как бы "забывает" про эту переменную (текущая последовательность: раз, два).
Обратно:
// Read from heap
{
let p;
unsafe { p = ppoint.read()}
println!("ppoint.read() completed: {:?}", p);
}
Для того чтобы посмотреть, когда же вызывается деструктор, реализуем Drop
для Point
:
impl Drop for Point {
fn drop(&mut self) {
println!("Point dropped: {:?}", self);
}
}
Все вместе при запуске дает результат:
ppoint.write() completed
ppoint.read() completed: Point { x: 101, y: 201 }
Point dropped: Point { x: 101, y: 201 }
Т.е. сначала записываем, затем читаем, и только потом вызывается деструктор у прочитанного значения.
Еще немного — и с кучей завершим.
Комментарии (9)
Alina83
19.08.2021 08:24Спасибо за разъяснение. Рекомендую ютуб-канал с уроками по rust: D-web. С достаточно понятной и интересной интерпретацией инфы, и связной логикой уроков!
maxim_ge Автор
19.08.2021 09:55А в какой последовательности надо смотреть? Если начинать с этого, то, замечу, мои первые впечатления от Rust бесконечно далеки от показанного. Матан или сопромат, да и просто физ-мат в рамках средней школы на голову сложнее того, что придумано в Rust. Мне ближе такой видеоряд, только титры надо заменить на "Кто писал документацию?!".
Собственно, из-за нее, родимой, и появляются объемистые сторонние обучающие материалы.
Alina83
19.08.2021 12:03Начинать лучше сначала плей-листа с уроками - с установки vc code и базовых понятий типа переменные, функции, структуры... Эти видео, на которые Вы ссылаетесь понятны, если уже просмотрел все предыдущие)
AnthonyMikh
19.08.2021 14:34+1Для случаев с нестандартным выравниванием надо использовать ptr::addr_of!() / ptr::addr_of_mut!()
Дело не в выравнивании, а в том, что в стабильной версии Rust нету способов, помимо этих макросов, взять указатель на поле структуры, не создавая промежуточную ссылку, а правила языка требуют, чтобы ссылки указывали только на инициализированные данные, нарушение это правила — это неопределённое поведение.
Выделять типы нулевого размера и пустые типы отдельно от размерных (Sized) некорректно, они так же являются Sized.
Важно: Деструктор для p при этом НЕ вызывается, т.е. Rust как бы "забывает" про эту переменную
Это не является особым случаем, а лишь проявлением того факта, что
<*mut T>::write
, как и любая функция, принимает владение переданным аргументом. То, что происходит с переданным значением внутри функции, извне уже не контролируется. Там вполне могут вызватьstd::mem::forget
, внешне эффект будет тот же, то есть отсутствие вызова деструктора.maxim_ge Автор
19.08.2021 15:14Дело не в выравнивании
Документация прямо указывает, что "//
&packed.f2
would create an unaligned reference, and thus be Undefined Behavior!".Да и не скомпилируется вот такой пример без
allow(unaligned_references)
:#[repr(packed)] struct Packed { f1: u8, f2: u16, } #[allow(unaligned_references)] fn main(){ let packed = Packed { f1: 1, f2: 2 }; // `&packed.f2` would create an unaligned reference, and thus be Undefined Behavior! let pf2 = &packed.f2 as *const _ as *const u16; unsafe {dbg!(*pf2)}; dbg!(packed.f1); }
а правила языка требуют, чтобы ссылки указывали только на инициализированные данные, нарушение это правила — это неопределённое поведение.
Неопределённое поведение в каком сценарии, можно ссылку на документацию? Понятно, что данные без инициализации "читать" не стоит, но ведь "читать" никто не собирается.
mayorovp
19.08.2021 15:32+2Неопределённое поведение в каком сценарии, можно ссылку на документацию? Понятно, что данные без инициализации "читать" не стоит, но ведь "читать" никто не собирается.
https://doc.rust-lang.org/stable/std/ptr/macro.addr_of.html
Не важно читают данные или нет, идеология Rust запрещает существование экземпляров безопасных типов данных (а ссылки тоже относятся к таковым) в невалидном состоянии.
maxim_ge Автор
19.08.2021 15:55Понятно. Замечу, что в стабильных рамках языка Rust (если к языку не относить макрос
std::ptr::addr_of!()
) нельзя получить и raw pointer на невыровненные/неинициализированные/ данные.
maxim_ge Автор
19.08.2021 15:22Выделять типы нулевого размера и пустые типы отдельно от размерных (Sized) некорректно
Замечу, так сделано в документации.
… они так же являются Sized.
Но таки да, это лучше пояснить, поправил.
Kazikus
Благодарю за отличную работу, хороший цикл статей