Продолжаем изучать 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 }
Т.е. сначала записываем, затем читаем, и только потом вызывается деструктор у прочитанного значения.
Еще немного — и с кучей завершим.
