Привет, Хабр!
Сегодня я хочу поговорить про pinning в Rust, замечательной конструкции Pin. Поначалу вызывает вопросы, зачем нужен какой-то пин, если и без него всё работало?
Но вот незадача, без Pin не реализовать безопасно ни одну хитрую программку, самоссылающиеся структуры и связанные с ними асинхронные генераторы. В этой статье я расскажу, почему вообще появился Pin/Unpin, как он спасает от падений программы, и как его вообще применять.
Зачем нужен Pin?
Начнем с проблемы, ради решения которой и создали Pin. Представьте себе такую структуру, которая хранит указатель на собственные данные. Звучит немного странненько, но такие случаи встречаются.
Классика – узел двусвязного списка, хранящий указатели на соседние узлы, или структура, slice на свой же буфер. В тех же плюсах такое сделать просто, кладем указатель, указывающий на нужное место внутри структуры, и радуемся жизни до тех пор, пока эта структура не переместится в памяти. В системных языках объект может свободно менять адрес, например, если вы положили объект в вектор, а потом вектор вырос и сделал realloc разом передвинув все элементы.
В момент перемещения все внутренние указатели структуры, ссылающиеся на неё саму, перестают указывать на правильное место. Программа продолжит работать, но по факту эти указатели теперь вистя, при попытке доступа мы получим неопределённое поведение и, скорее всего, краш. Вот почему самоссылающиеся структуры несут в себе проблемы, без доп. мер их нельзя просто так взять и перемещать.
В замечательно расте есть гарантия памяти, если программа компилируется, то она не упадёт из-за некорректных указателей. Поэтому все фундаментально нет права создавать самоссылающиеся конструкции напрямую. Попытки сделать это через обычные ссылочные типы & ловятся компилятором. Например, такой код не скомпилируется, потому что нам с особой заботой запрещают перемещать a, пока существует ссылка a.s_ref на его часть:
struct SelfRef<'a> {
s: String,
s_ref: Option<&'a str>,
}
fn main() {
let mut a = SelfRef {
s: "hello".to_owned(),
s_ref: None,
};
a.s_ref = Some(&a.s);
// попытка переместить структуру, на которую есть ссылка
// let b = a; // error[E0505]: cannot move out of `a` because it is borrowed
}
Компилятор видит ссылку &a.s внутри a и запрещает двигать a. Обычные ссылки в Rust предотвращают перемещение самоссылающегося объекта.
Потому что иначе ссылка стала бы висячей!
Однако народ изобретательный, и всегда может выстрелить себе в ногу, если очень хочется. В примере выше мы использовали безопасную ссылку &str. А что если взять сырой указатель? Raw-указатели компилятор на предмет жизненного цикла не проверяет, ведь это уже небезопасная штука. Перепишем пример, заменив ссылку на сырой указатель:
use std::ptr::null;
struct SelfRaw {
s: String,
s_ptr: *const String, // указатель на саму строку внутри структуры
}
fn main() {
let mut a = SelfRaw {
s: "hello".to_owned(),
s_ptr: null(),
};
a.s_ptr = &a.s; // установим указатель на строку
let b = a; // перемещаем структуру a в переменную b
unsafe {
// попытка воспользоваться сырым указателем после перемещения:
println!("b.s = {}", *b.s_ptr);
}
}
Может показаться, что всё сработало, мы сохранили указатель на строку s внутри структуры, переместили объект и даже прочитали строку через указатель. Но уже после let b = a старая переменная a больше не существует, структура перемещена. Перемещение в Rust представляет из себя побайтовое копирование значений структуры на новое место. Теперь b содержит прежние данные, а a считать уже нельзя. Но s_ptr по прежнему указывает на старый адрес в памяти, где раньше лежала строка.
Если в этом месте памяти уже что-то поменялось, то разыменование b.s_ptr выдаст мусор или вообще просто дропнется с ошибкой доступа. В нашем случае, возможно, программа даже выведет "hello", просто по счастливой случайности старые байты ещё не перезаписались. Но полагаться на такое естественно нельзя, рано или поздно всё упадёт.
Итак, вот она проблема, самоссылающиеся структуры без нужных мер неминуемо ломаются при перемещении. Нужно как-то зафиксировать объект в памяти, чтобы он не менял адрес после инициализации. Значит, нам нужен способ сказать компилятору:
Дружище, вот этот объект после создания трогать нельзя, иначе все переломается
Таким способом и стал Pinning.
Контракт перемещения
С версии Rust 1.33 ввели два связанных механизма: маркерный трейт Unpin и обертку Pin<T>. Проще всего понимать их в связке.
Unpin – это авто-трейт, автоматически реализуемый для почти всех типов, кроме особых случаев. Когда тип помечен Unpin, это означает, что ему не страшны перемещения. Иными словами, перемещая такой объект по памяти, мы не сломаем его внутреннюю логику. Большинство обычных структур и примитивов в Rust безопасно перемещать, и они, естественно, Unpin. Например, типы вроде i32, String, Vec<T> все Unpin, потому что не содержат самоссылающихся ухищрений. Даже наша структура SelfRef из первого примера, содержащая обычную ссылку &str, является Unpin ведь компилятор и так не даст ее сдвинуть, пока ссылка жива.
А вот для типов, которые неперемещаемы, придумали категорию !Unpin. Если тип объявлен как !Unpin, то нельзя просто так взять и переместить его, не нарушив инвариантов.
Rust не позволяет просто написать impl !Unpin for MyType, это автотрейт. Однако автотрейт не реализуется автоматически, если у типа есть поле, которое само !Unpin.
Этим и пользуются.
В дефолт библиотеке есть специальный пустой маркер std::marker::PhantomPinned. Этот тип не реализует Unpin. Поэтому, если добавить поле PhantomPinned в вашу структуру, компилятор перестанет автоматически помечать её как Unpin. Получится собственный !Unpin тип.
Сделаем структуру, которая заявляет о том, что она не перемещается:
use std::marker::PhantomPinned;
struct Demo {
text: String,
_pin: PhantomPinned, // полемаркер, делающее тип !Unpin
}
fn main() {
let a = Demo {
text: "demo".to_owned(),
_pin: PhantomPinned,
};
let b = a; // компилятор скопирует a в b, хотя тип !Unpin
println!("{}", b.text);
}
Сам по себе факт, что Demo является !Unpin, не препятствует перемещению переменной a в b. Мы успешно сделали let b = a. Почему? Потому что мы пока не задействовали механизм Pin. Маркер !Unpin сам по себе ничего не гарантирует, он лишь сообщает остальной части системы, что с этим типом будьте осторожны, просто так его двигать нельзя. Но чтобы адекватно зафиксировать объект, нужен Pin.
Итак:
если тип реализует
Unpin, значит его можно свободно перемещать, даже когда он закреплен.Unpinснимает все ограничения, иPinдля такого типа ведет себя тривиально.если тип не реализует
Unpin, значит после закрепления его перемещать нельзя, иначе будет беда.
Большинство типов по дефолту Unpin. Неперемещаемыми становятся только специально помеченные, как мы сделали с помощью PhantomPinned, или те, что содержат внутри такие поля.
Нужно заворачивать объекты в контейнер Pin, который и будет следить за соблюдением контрактов.
Как работает Pin
Pin<T> – это такая обертка, которая гарантирует, что значение типа T закреплено в памяти по некому адресу. На самом деле Pin параметризуется не самим T, а указателем на T. Чаще всего юзают Pin<Box<T>> или Pin<&mut T>, т.е закреплённый указатель на объект. Пока наш объект завернут в Pin, через этот контейнер нельзя сделать ничего, что привело бы к перемещению объекта.
Pin фиксирует не сам указатель, а значение, на которое он указывает. Если у нас есть, например, Pin<Box<T>>, то Box<T> хранит объект где-то в куче, и Pin обещает, что этот объект в куче не будет перемещен. Сам же Box может копироваться как угодно, неважно, главное, что содержимое остается по одному адресу.
Конечно, как и многое в Расте, Pin опирается на соглашение между программистом и компилятором. Полностью предотвратить перемещение на уровне машинного кода невозможно, всегда остаются unsafe лазейки. Но Pin делает перемещение необходимо небезопасным, все безопасные способы получить доступ к объекту через Pin не позволят его сдвинуть. А раз так, то если вы не пишете unsafe, то и не ошибетесь.
Посмотрим, как пользоваться Pin. Допустим, есть наш Demo, который !Unpin. Попробуем закрепить экземпляр Demo:
use std::pin::Pin;
let mut pinned_box: Pin<Box<Demo>> = Box::pin(Demo {
text: "demo".to_owned(),
_pin: PhantomPinned,
});
Используем удобную функцию Box::pin(...), которая выделяет место в куче под объект и сразу возвращает Pin<Box<T>>. Для типов !Unpin это единственный безопасный способ закрепить значение. Если мы попробуем сделать Pin::new(&mut value) на неперемещаемом типе, компилятор не даст, вдруг наш &mut value указывает на временный объект на стеке, который потом ещё куда-то поедет? Поэтому Pin::new потребует, чтобы T: Unpin, иначе придется быть хитрее. А Box::pin как раз знает, что в куче объект будет лежать стабильно, поэтому позволяет закрепить даже !Unpin.
Теперь у нас есть pinned_box типа Pin<Box<Demo>>. Что можно с ним сделать? Например, просто прочитать поле через безопасную проекцию:
println!("Pinned text: {}", pinned_box.as_ref().text);
Метод Pin::as_ref() даст нам Pin<&Demo>, а у Pin<&T> есть имплементация Deref на &T, для чтения это ок. Мы смогли вывести поле text. А вот если захотим изменить это поле или весь объект, так просто не выйдет.
Например, такой код не скомпилируется:
// pinned_box.text = "new text".to_owned(); // error: no field `text` on type `Pin<Box<Demo>>`
Прямого доступа нет, потому что DerefMut для Pin<Box<T>> реализован только если T: Unpin. Наш Demo не Unpin, значит, изменять его напрямую запрещено, вдруг мы попытаемся присвоить новый объект, переместив старый? Компилятор не может этого позволить.
Как же тогда менять внутренности закрепленного объекта? Для этого есть специальные методы. Общая схема такая:
получить закрепленную изменяемую ссылку
Pin<&mut T>с помощью методаPin::as_mut().потом при необходимости разберемся, какое поле хотим изменить, возможно, нам понадобится проекция, об этом чуть позже.
если нужно полностью перевести
Pin<&mut T>в обычный&mut T, можно использовать небезопасный методPin::get_unchecked_mut(). Онunsafe, потому что открывает врата, ведь получив обычную мутабельную ссылку, можно при желании опять же сделать что-то нехорошее.
Для нашего примера с Demo изменение поля могло бы выглядеть так:
unsafe {
// получаем Pin<&mut Demo>
let mut_ref: Pin<&mut Demo> = Pin::as_mut(&mut pinned_box);
// получаем обычную &mut Demo (unsafe, доверяем себе)
let demo_mut: &mut Demo = Pin::get_unchecked_mut(mut_ref);
// меняем данные
demo_mut.text = "new demo".to_string();
}
println!("Pinned text after mutation: {}", pinned_box.as_ref().text);
Не перемещали объект Demo, а лишь изменили его поле. Теперь поле text содержит новое значение, и программа работает.
Ну а что мешает недобросовестному человеку внутри блока unsafe все таки вынуть значение и двинуть его? По сути, ничего не мешает, кроме здравого смысла и опасения словить UB. Pin полагается на нас. Если это обещание нарушить, компилятор уже не защитит, и ответственность целиком на вас. Поэтому лучше по возможности избегать ручной работы с unsafe и Pin.
Реализуем самоссылающийся объект правильно
Наконец вернёмся к нашей исходной задаче, структуре с указателем на своё поле. Теперь создадим самоссылающуюся структуру безопасно, используя Pin.
Представим структуру, которая хранит большую строку и одновременно предоставляет срез на эту же строку внутри себя. Можно подумать, зачем такое нужно, но бывают случаи, когда хочется распарсить текст и сохранить ссылку на часть внутри полного текста, не выделяя лишнюю память. Такой объект нельзя перемещать после создания, иначе внутренняя ссылка станет недействительной.
use std::marker::PhantomPinned;
use std::pin::Pin;
struct SliceHolder<'a> {
text: String,
slice: Option<&'a str>,
_pin: PhantomPinned, // помечаем как !Unpin
}
impl<'a> SliceHolder<'a> {
fn new(text: &str) -> Pin<Box<Self>> {
// создаем начальный объект с пустой ссылкой
let mut boxed = Box::pin(SliceHolder {
text: text.to_owned(),
slice: None,
_pin: PhantomPinned,
});
// получаем временный Pin<&mut Self>, чтобы заполнить поле slice
let self_mut: Pin<&mut SliceHolder<'a>> = boxed.as_mut();
let self_ref: &mut SliceHolder<'a> = unsafe { Pin::get_unchecked_mut(self_mut) };
// установим slice, ссылающийся на начало строки
self_ref.slice = Some(&self_ref.text[..1]);
// вернём закреплённый бокс наружу
boxed
}
fn get_slice(&self) -> &str {
// безопасно разыменовываем ссылку (она должна быть актуальна, если объект не двигали)
self.slice.as_ref().expect("Slice is not set")
}
}
Конструктор new возвращает Pin<Box<SliceHolder>>, сразу создаём на куче и пинаем. Сначала помещаем внутрь slice: None, потому что еще не знаем адреса, он же зависит от расположения text. После этого получаем Pin<&mut Self> через as_mut(). Дальше используем get_unchecked_mut, потому что нужно временно обойти правила, мы уверены, что пока что объект в boxed никуда не делся, он только что создан, мы держим его Pin. Заполнив поле slice правильной ссылкой, мы получили самоссылающийся объект! Возвращаем его наружу. Тип возвращаемого значения Pin<Box<Self>> гарантирует пользователю, что теперь с этим объектом можно работать, но нельзя его перемещать из Pin безопасно.
Протестируем на небольшом примере:
# use std::marker::PhantomPinned;
# use std::pin::Pin;
# struct SliceHolder<'a> { text: String, slice: Option<&'a str>, _pin: PhantomPinned }
# impl<'a> SliceHolder<'a> {
# fn new(text: &str) -> Pin<Box<Self>> {
# let mut boxed = Box::pin(SliceHolder { text: text.to_owned(), slice: None, _pin: PhantomPinned });
# let self_mut = boxed.as_mut();
# let self_ref = unsafe { Pin::get_unchecked_mut(self_mut) };
# self_ref.slice = Some(&self_ref.text[..1]);
# boxed
# }
# fn get_slice(&self) -> &str { self.slice.as_ref().unwrap() }
# }
let pinned = SliceHolder::new("Hello, world!");
println!("Slice inside: {}", pinned.as_ref().get_slice());
//
// Попробуем переместить pinned (раскомментируйте следующую строку для эксперимента):
// let pinned2 = *pinned; // ERROR: cannot move out of `pinned` because it is behind a pointer
Метод get_slice возвращает &str, ссылку на внутреннюю строку. В нашем примере это будет "H". Главное, что код компилируется и работает. А если мы вдруг попытаемся вынуть наш SliceHolder из Pin, например, через разыменование *pinned, компилятор нас остановит, ведь ельзя перемещать значение, оно находится за pinned-указателем". Именно этого мы и добивались!
Получилась схема: Pin + PhantomPinned + немного unsafe = безопасная самоссылающаяся структура. Все операции с ней в безопасном коде.
Коречное, писать такой код по удовольствию ниже среднего. Можно допустить ошибку и не сразу понять. К счастью есть крейт pin-project, который с помощью макросов генерирует за вас весь этот бойлерплейт, помечает тип как !Unpin, делает методы проекции полей, и избавляет от ручного unsafe-кода. В проде конечно используем его, но мне кажется, что полезно понимать, как ваще все это устроено.
Pin и асинхронность
Когда вы пишете обычную асинхронную функцию, Rust компилятор преобразует её в скрытый тип, реализующий трейт Future. Если прям коротко, он формирует state machine, которая хранит все локальные переменные и где она остановилась. При каждом await выполнение приостанавливается, а потом Future::poll продолжит с того же места. В этом и кроется проблема, во время приостановки функция как бы хранит ссылку на своё будущее состояние.
Например, напишем такой псевдокод:
async fn demo_async() {
let big_data = String::from("...много данных...");
let part = &big_data[0..5];
some_future.await;
println!("{}", part);
}
Когда происходит .await, функция приостанавливается не завершив жизнь переменной big_data и продолжает держать ссылку part на её часть. Получается сгенерированный Future содержит внутри себя и big_data и ссылку &str на неё же. Самый настоящий самоссылающийся объект!
Если такой Future переместить в памяти во время выполнения, то ссылка part станет указывать в никуда. Поэтому Rust помечает такие футуры как !Unpin. Т.е любой Future, который захватывает локальные ссылки или имеет подобную зависимость, будет неперемещаемым. Даже если внутри Future нет явных ссылок, компилятор на всякий случай делает консервативно, большинство async fn порождают !Unpin типы, если только точно известно, что внутри нет заимствований через паузу.
Таким образом, поллить Future можно только на месте. Трейт Future объявлен так:
trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}
Метод poll принимает self: Pin<&mut Self>, то есть закреплённую изменяемую ссылку на футур. Когда исполнитель берет задачу и начинает ее выполнять, он сначала размещает Future в нужном месте и делает пин. Далее он будет передавать в poll именно Pin<&mut> этой задачи. executor гарантирует, что не будет перемещать задачу в памяти между вызовами poll. Если задача Pending, она остаётся по тому же адресу до следующей попытки, и тд.
Если вы вручную реализуете Future, я надеюсь, что для вас это едкий случай в эпоху async/await синтаксиса, но всё же, вам придётся иметь дело с Pin. Например, в предыдущем разделе мы реализовали самоссылающийся SliceHolder. Представьте, что у него есть метод, возвращающий Future читает что то асинхронно, используя поле slice. Такой Future должен быть неперемещаемым, иначе внутри него тоже могли бы быть висячие ссылки. При реализации метода poll у этого Future вы бы получили self: Pin<&mut Self> и должны были бы правильно спроецировать поля.
Мне кажется, делать в ручную это трудно, поэтому появились утилиты вроде упомянутого pin-project или pin-utils. Они позволяют объявлять, какие поля структуры Future должны быть pinned, а какие можно брать как обычные. Макрос сгенерирует метод project() для безопасного доступа. Например, он разложит Pin<&mut MyFuture> на Pin<&mut field1> для поля 1 и &mut field2 для поля 2. После этого можно вызывать методы на внутренних футурах безопасно.
Возвращаясь к высокому уровню любая async fn в итоге использует Pin за кулисами. Когда вы пишете просто fut.await происходит примерно так:
use std::pin::Pin;
use std::future::Future;
# use std::task::{Context, Poll};
// представим, что есть какой то future fut:
let mut fut = some_async_function(); // получили Future
pin_utils::pin_mut!(fut); // закрепили его на стеке (macro pin_mut!)
let waker = /* ... */; // получили Waker (детали неважны сейчас)
let mut cx = Context::from_waker(&waker);
match Future::poll(Pin::as_mut(&mut fut), &mut cx) {
Poll::Ready(output) => {/*...*/},
Poll::Pending => {/*...*/},
}
Макрос делает на стеке локальную переменную futPinned, по сути он возвращает Pin<&mut Fut> готовый для опроса. В executors всё чуть хитрее, они хранят задачи в куче, чтобы можно было их приостанавливать и возобновлять, но суть та же.
Отдельно упомяну генераторы, более общий механизм, на основе которого сделаны async/await. Генератор, который делает yield на значения и потом возобновляется, по сути тоже хранит свой state с возможными внутреннями ссылками. Поэтому API генераторов также требует вызова resume(self: Pin<&mut Self>). Всё как с Future.
Нюансы
Про Drop:
Если тип помечен !Unpin и используется в Pin, то важно аккуратно обращаться с ним при разрушении. В частности, нельзя перемещать поля внутри метода Drop такого типа.
Представим, что у нас есть struct MyType { field: T, _pin: PhantomPinned } и мы реализовали Drop для MyType. Внутри drop(&mut self) вы имеете обычную &mut Self. Тем не менее, если вы знаете, что ваш тип может быть закреплён, обязаны соблюдать его инварианты. А именно не смещать важные поля. Что вообще значит смещать в контексте Drop? Например, вызвать std::mem::replace или take на поле, тем самым вынув его значение. Это было бы перемещение. Хотя после drop объект все равно уходит в никуда, такое перемещение может нарушить логику, если внутри поля лежит что-то самоссылающееся. Как минимум, до вызова десструкторов поля должны оставаться на тех же адресах.
К счастью, в большинстве случаев не нужно ничего особого предпринимать, просто не перемещайте поля явно. Если у вас сначала объявлено поле, зависящее от второго, а потом второе поле, то при разрушении первым упадёт зависимое поле, а потом то, на что оно ссылалось. Если порядок Drop важен, можно обернуть поля во что-то вроде ManuallyDrop и самому контролировать, или поменять их местами. Но это отдельная тема. Главное, Pin не меняет порядка drop, но требует, чтобы вы не пытались "спасти" данные из pinned объекта в последний момент. Пусть уж погибает со всем содержимым по месту.
Теперь о цепочках .map() / .then() в асинхронном коде. Когда мы комбинируем несколько футур, образуется вложенная структура. Например, fut.map(f).then(g) может породить что-то вроде типа Then<Map<Fut, F>, G>. Это тоже Future, внутри которого находится другой Future (Map), а внутри него исходный Fut. Все они могут быть !Unpin или Unpin, зависит от ситуаций. Библиотеки обычно стараются реализовать такие комбинаторы аккуратно, чтобы результат был !Unpin только если исходный футур !Unpin. В любом случае, Pin легко пролезает внутрь: Pin<Box<Then<...>>> будет содержать pinned внутренний футур и так далее.
Интересно другое, что случается, если мы бросаем незавершенный футур на пол, вот просто больше не ждем его, он дропается? Правильный ответ в том,что ничего страшного не произойдёт с точки зрения памяти, утечек не будет, если только не было циклических Rc. Все ресурсы внутри футура должны адекватно освободиться благодаря обычному механизму Drop. Но есть нюанс, если у вас были побочные эффекты при завершении, при отмене задачи эти эффекты не выполнятся. Это дефолт, отмена асинхронной операции означает, что она может завершиться в середине, так и не дойдя до конца. Поэтому не полагайтесь на то, что .map или .then обязательно исполнится до конца, при отмене нет. Все Drop, впрочем, вызовутся.
Например, если у вас цепочка из нескольких футур, и вы бросаете её, то внутренние футуры будут уничтожены. Их порядок уничтожения соответствует порядку полей в структуре комбинирующего футура. Обычно внутри типа Then<Fut, Fun> хранится либо исходный футур, либо уже результат и новый футур. Разработчики таких комбинаторов продумали это: в момент Poll::Ready они перемещают результат или как-то помечают состояние, так что потом при деструктуре не случится двойного дропа. Ну а вообще достаточно понять того, что Drop цепочки футур рекурсивно деструктят всё содержимое. Никаких подвисших задач не останется.
Все pinned футуры внутри тоже будут безопасно дропнуты, их память освобождается, но нас не волнует, что они не переместились. А вот если бы вы попытались вручную вынуть из середины цепочки футур неперемещаемый объект, вы нарушили бы безопасность. К счастью, делать так практически невозможно без unsafe, так что порядок соблюдается.
Безопасные обёртки над небезопасными буферами
Наконец, последний штрих, о котором хочу рассказать, хоть это и немного в сторону от Pin, но близко по духу, про безопасные адаптеры над сырыми ресурсами, такими как буферы памяти. Часто в системном программировании приходится работать с небезопасными буферами, например, выделять память через FFI, возвращается *mut u8 и нужно не забыть освободить. Ошибки с управлением памятью могут привести к падению программы не хуже, чем наши самоссылающиеся указатели. В Rust принято оборачивать такие вещи в ресурсные структуры с Drop чтобы соблюсти RAII и не допустить утечек или двойного освобождения.
Напишем обертку над сырой памятью, которая освобождает её при удалении объекта, а если вы вдруг забыли вызвать метод освобождения, то сообщит об утечке.
use std::ptr::NonNull;
use std::alloc::{alloc, dealloc, Layout};
struct RawBuf {
ptr: NonNull<u8>,
size: usize,
freed: bool,
}
impl RawBuf {
fn new(size: usize) -> RawBuf {
// выделим память через глобальный аллокатор
let layout = Layout::from_size_align(size, 1).unwrap();
let ptr = unsafe { alloc(layout) };
RawBuf {
ptr: NonNull::new(ptr).expect("Allocation failed"),
size,
freed: false,
}
}
fn free(&mut self) {
if !self.freed {
let layout = Layout::from_size_align(self.size, 1).unwrap();
unsafe { dealloc(self.ptr.as_ptr(), layout); }
self.freed = true;
}
}
}
impl Drop for RawBuf {
fn drop(&mut self) {
if !self.freed {
// если деструктор вызвался, а freed == false, значит free() не вызывали
eprintln!("Memory leak detected: buffer of {} bytes not freed!", self.size);
// попробуем освободить, чтобы не быть совсем уж злодеями (или можно panic! тут)
let layout = Layout::from_size_align(self.size, 1).unwrap();
unsafe { dealloc(self.ptr.as_ptr(), layout); }
}
}
}
RawBuf управляет кусочком памяти. Метод free освобождает память вручную, помечая флаг. А в Drop проверяем, если объект уничтожается, но флаг freed не выставлен, значит, какой-то негодяй забыл вызвать free. Тогда выводим предупреждение и сами освобождаем память.
Тут тоже есть потенциально висящий указатель, поле ptr: NonNull<u8>. Но мы заботимся, чтобы он никогда не гулял без присмотра. Мы не перемещаем RawBuf особо нигде, а даже если бы переместили, сам указатель на буфер остаётся валидным. Главное, вовремя освобождать.
Итак, Rust позволяет оборачивать небезопасные конструкции в безопасные, следуя определённым шаблонам.
Кстати, в случае самоссылающихся структур иногда тоже проще пересмотреть дизайн и избежать вообще таких ситуаций. Например, вместо хранения указателя или ссылки внутрь себя, можно хранить индексы или смещения. Взять случай со строкой, мы же могли хранить начало и длину среза вместо самой &str. Тогда структура стала бы перемещаемой без проблем, а перед использованием просто брала бы &self.text[offset..offset+len]. Минус в том, что компилятор не проверит правильность индексов, это уже на совести. Как видите, универсального решения нет, где-то лучше использовать Pin, где-то изменить структуру данных, а где-то вообще держать всё в одном куске памяти и никуда не дергать.
В итоге
Поначалу концепция кажется странненькой. Однако, подходя системно, можно уловить суть:
Pinгарантирует, что объект не будет перемещён, пока находится под защитой этого типа.Unpinопция выхода из этой гарантии, говорящая о том, что нам вот это не важно, можно и подвигать.В основе всего лежит стремление Rust сохранить безопасность, не допустить ситуации, когда указатель внезапно стал указывать на невалидный адрес из-за перемещения данных.
Pinиспользуется внутриFuture/async, без него невозможно безопасно приостановить функцию и потом продолжить.
А вообще, если Rust заставляет нас делать какую-либо суету, значит на кону безопасность приложения. Всем добрых снов и теплого чая.
Nuflyn
"В замечательно расте ", "указатели теперь вистя ", "Поэтому все фундаментально нет права " статья не вычитана (зато хоть не творчество ИИ)