Перейти к содержанию

Закрепление данных по постоянному адресу в rust

Posted on:8 июля 2023 г. at 09:11

Example Dynamic OG Image link Pin - очень запутанная тема, с которой я столкнулся при программировании в Rust. Я старался научиться этому, но руководства, статьи и видео, которые я смотрел, трудно понять. Они обычно связаны с необходимостью знать другую сложную концепцию в Rust, и это заставило меня ходить между статьями, видео и руководствами по другим концепциям.

В этой статье я отфильтрую для Вас все остальные понятия и сосредоточусь исключительно на pin. Прочитав эту статью, вы научитесь применять pin в коде и понимать его использование в других исходниках.

Что такое pin

Pin является важной особенностью в Rust. Он позволяет разработчикам прикреплять объект к позиции в памяти, чтобы ваши данные не могли переместиться ещё куда-либо.

Эта функция необходима при работе с объектами, ссылающимися на другие объекты, которые имеют тенденцию изменять свое положение в памяти, независимо от того, необходимо это или нет. При построении структур данных, таких как связанные списки или работа с асинхронным кодом, это может повлиять на код и вызвать неопределенное поведение.

Как закрепить объект в памяти?

Закреплять объект можно довольно легко. Rust предоставляет структуру Pin, позволяющую закреплять объекты. Структура является частью стандартной библиотеки Rust, доступ к нему можно получить через std::pin::Pin.

Рассмотрим следующий пример:

struct MyStruct {
 value: u32
}

fn main() {
 let my_struct = MyStruct{ value: 10 };
 println!("{}", my_struct.value);
}

Этот пример содержит простую структуру, которая используется в main функции. В main, мы создали экземпляр структуры с value 10, а затем вывели на печать это значение в консоль.

Мы можем закрепить my_struct, Box::pin(). И это сделаем в коде ниже.

use std::pin::Pin;

struct MyStruct {
  value: u32,
  _pin: PhantomPinned,
}

fn main() {
  let mut my_struct: Pin<Box<MyStruct>> = Box::pin(MyStruct {
    value: 10,
    _pin: PhantomPinned,
  });
  println!("{}", my_struct.value);
}

Я не говорил о Box, так что давайте разберемся. Box - это структура, позволяющая выделять память в куче.

Rust имеет два типа памяти, которые он использует для хранения значений в коде: стек и куча. Rust хранит данные заданного размера в стеке и данные размеры которых определяются во время выполнения в куче. Память стека имеет ограниченные возможности хранения, но быстрее, чем память в куче. Но память в куче более обширна и гибка, чем память стека.

Box::pin() выделяет память в куче и устанавливает данные на постоянное место.

Следующий список содержит сведения, необходимые для закрепления значений:

Давайте рассмотрим все это в деталях!

Изменение данных закрепленного объекта

Одна из вещей, в которой вы застрянете сразу после закрепления объекта - это попытка изменить данные в нем. Это может расстроить, но не бойтесь. Есть способ обойти это, но он включает в себя некоторые процессы нарушения правил. Мы будем выполнять эти процессы в unsafe блоке.

Давайте вернемся к последнему примеру, который был у нас в предыдущем разделе.

Допустим, мы хотим изменить значение my_struct.value на 32. Если мы просто попытаемся сделать my_struct.value = 32, компилятор выдаст сообщение о ошибке, сообщающее, что это не сработает.

Чтобы изменить значение my_struct.value, необходимо выполнить несколько шагов:

  1. Сначала сделайте изменяемой ссылку на закрепленный объект с помощью Pin::as_mut(&mut my_struct).
  2. Затем используйте эту изменяемую ссылку для ссылки на объект, с помощью Pin::get_unchecked_mut(mut_ref).
  3. Наконец, используйте ссылку на объект для изменения объекта по своему усмотрению.

Все внесенные изменения будут отражены в закрепленном объекте.

Посмотрим, как выглядит код после выполнения предыдущих шагов.

use std::pin::Pin;

struct MyStruct {
  value: u32,
  _pin: PhantomPinned,
}

fn main() {
  let mut my_struct: Pin<Box<MyStruct>> = Box::pin(MyStruct {
    value: 10,
    _pin: PhantomPinned,
  });
  println!("{}", my_struct.value);
  unsafe {
    let mut_ref: Pin<&mut MyStruct> = Pin::as_mut(&mut my_struct);
    let mut_pinned: &mut MyStruct = Pin::get_unchecked_mut(mut_ref);
    mut_pinned.value = 32;
  }
  println!("{}", my_struct.value);
}

При выполнении кода отображается значение my_struct.value в терминале до и после его изменения.

Свойство _pin

Если вы заметили, мы добавили свойство _pin в структуру при внесении изменений в наш код. Теперь вы можете спросить себя, что это такое и что оно делает. Это то, что мы рассмотрим в этом разделе.

_pin - это свойство, помещаемое в структуру, которую требуется закрепить. Оно сообщает компилятору, что структура должна быть закреплена в памяти по постоянному адресу. Метод Box::pin() можно применить к структуре без свойства _pin, но он не будет фиксировать ее в памяти.

Вы можете проверить предыдущий это самостоятельно с помощью этого кода:

use std::pin::Pin;

struct MyStruct {
  value: u32,
}
fn main() {
  let mut my_struct: Pin<Box<MyStruct>> = Box::pin(MyStruct {
    value: 10,
  });
  println!("{}", my_struct.value);

  my_struct.value = 32; // без '_pin' это работает без каких-либо проблем

  println!("{}", my_struct.value);
}

При использовании свойства _pin в структуре и инициализации его с помощью PhantomPinned строка 14 (my_struct.value = 32;) становится недействительной.

use std::pin::Pin;

struct MyStruct {
  value: u32,
  _pin: PhantomPinned,
}
fn main() {
  let mut my_struct: Pin<Box<MyStruct>> = Box::pin(MyStruct {
    value: 10,
    _pin: PhantomPinned,
  });
  println!("{}", my_struct.value);

  my_struct.value = 32; // Это приведет к ошибке компиляции

  println!("{}", my_struct.value);
}

Если вы посмотрите на свойство _pin, вы можете спросить, почему необходимо инициализировать его с помощью PhantomPinned. Ответ прост: PhantomPinned - это тип, который Rust использует для применения правил закрепления структуры в памяти. При необходимости запрещения релокации структуры в памяти, к свойству _pin у структуры, всегда следует применять PhantomPinned. PhantomPinned не содержит значений в памяти и не выполняет никаких действий, кроме применения правил закрепления структуры в памяти, к которой он применяется.

Чем вы рискуете, используя закрепленные в памяти объекты?

Одна из самых больших проблем с закрепленными объектами - безопасность. Не поймите меня неправильно, закрепление объектов перед их использованием в определенных приложениях способствует безопасности. Теперь вы можете задаться вопросом: я только что сказал, что их проблема в безопасности, что происходит? Проблема безопасности с закрепленными предметами заключается в их использовании.

Возможно, вы заметили, что при изменении закрепленного объекта, my_struct, пришлось обернуть процессы в unsafe блок. На это была причина. Как выясняется, вы должны быть предельно осторожны при вмешательстве в закрепленный в памяти объект. Иначе, вы можете вызвать неопределенное поведение и другие проблемы в основных частях вашего кода.

Наш пример был прост, поэтому рисков было не так много. Но все равно нам понадобилось завернуть его в unsafe блок.

Вывод: зачем тогда закреплять объект в памяти?

Теперь, когда вы прошли через все это, остается последний вопрос. Зачем вообще утруждать себя pin? Этот вопрос имеет важное значение, поскольку закрепление объектов в памяти по постоянному адресу не будет иметь значения без ответа на них.

Чтобы ответить на эти вопросы, я составил краткий список, в котором говорится о каждой причине: