Работа с кучей в Rust

от автора

Продолжаем изучать 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 }

Т.е. сначала записываем, затем читаем, и только потом вызывается деструктор у прочитанного значения.

Еще немного — и с кучей завершим.

Далее: Заменяем глобальный аллокатор


ссылка на оригинал статьи https://habr.com/ru/articles/573422/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *