Что такое Pin в Rust и когда стоит его использовать?
В Rust Pin — это специальная обёртка, которая гарантирует, что объект не будет перемещён в памяти после того, как он был закреплён (pinned). Это особенно важно при работе с асинхронным программированием, низкоуровневыми абстракциями и взаимодействии с небезопасным (unsafe) кодом, где стабильный адрес объекта в памяти критичен.
Почему перемещение — проблема?
В Rust объекты по умолчанию могут быть перемещены, если они не обёрнуты в Box, Rc, Arc или другой указатель. Например, когда вы возвращаете значение из функции или передаёте его в другую переменную, компилятор перемещает (move) объект, что потенциально изменяет его адрес в памяти.
Для большинства типов это не проблема. Но есть случаи, когда **перемещение объекта после его инициализации может привести к небезопасному поведению. Например:
-
тип содержит указатели на самого себя (self-referential structs);
-
объект участвует в асинхронном стеке и содержит Future, требующий стабильного расположения в памяти;
-
вы передали сырые указатели на объект в unsafe-код и перемещение разрушит корректность работы указателей.
Что делает Pin?
Pin<T> — это обёртка, которая предотвращает перемещение объекта типа T в памяти после того, как он был закреплён.
use std::pin::Pin;
use std::boxed::Box;
let data = Box::pin(123);
Здесь:
-
Box::pin(123) создаёт Pin<Box
— закреплённый в памяти объект; -
Pin гарантирует, что объект внутри Box больше не будет перемещён.
Важно: Pin не делает объект неизменяемым, а именно не позволяет переместить его (то есть вызвать mem::swap, mem::replace, std::ptr::write, и т.п. с ним).
Типичная сигнатура
Тип Pin — это обёртка над типом, реализующим указатель:
Pin<P> where P: DerefMut<Target = T>
Чаще всего это Pin<Box
Где используется Pin?
1. Асинхронность и Future
Когда вы создаёте async fn, компилятор генерирует Future. Эти Future — это состояния, которые могут сохраняться между вызовами .await.
Rust требует, чтобы Future-объекты были pinned, потому что перемещение их во время выполнения может нарушить корректность их внутреннего состояния.
use std::pin::Pin;
use std::future::Future;
fn poll_future(fut: Pin<&mut dyn Future<Output = ()>>) {
// Здесь fut можно опрашивать безопасно, потому что он закреплён
}
2. Типы со ссылками на самих себя
В Rust нельзя безопасно реализовать структуру, содержащую ссылку на саму себя без использования Pin, потому что перемещение приведёт к нарушению ссылочной целостности.
Пример проблемы:
struct SelfRef<'a> {
data: String,
data_ref: Option<&'a str>, // ссылается на \`data\`
}
Такой тип опасен — после перемещения data окажется по другому адресу, а data_ref станет висячей ссылкой.
С Pin мы можем гарантировать, что data никогда не переместится после закрепления.
Методы доступа к данным внутри Pin
Работать с объектами внутри Pin сложнее, чем с обычными — для того, чтобы получить доступ к данным, нужно соблюдать ограничения. Вот основные способы:
-
Pin::as_ref() — создаёт Pin<&T>, чтобы безопасно читать данные;
-
Pin::as_mut() — создаёт Pin<&mut T>, если нужно модифицировать объект без перемещения;
-
Pin::get_mut() — возвращает &mut T, только если T: Unpin.
Что такое Unpin?
Это автоматически реализуемый маркерный трейт, означающий, что тип можно перемещать даже после того, как он был "закреплён". Большинство типов Unpin, если они не содержат самоссылок или других опасностей.
Если тип T: Unpin, тогда Pin<T> теряет своё значение — вы можете безопасно вызывать get_mut() и перемещать объект.
Типы Future, генерируемые из async fn, по умолчанию не Unpin, именно поэтому нужно использовать Pin при их опросе.
Когда стоит использовать Pin
-
При работе с async-кодом и ручной реализации Future, Stream, AsyncRead, и т.п.;
-
Если создаёте структуру со ссылками на себя — требуется Pin и unsafe-код для правильного управления памятью;
-
При использовании низкоуровневых API или FFI, где требуется стабильный адрес объекта в памяти;
-
Если пишете собственные контейнеры/абстракции, завязанные на внутренние указатели.
Пример: кастомный Future
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct MyFuture {
done: bool,
}
impl Future for MyFuture {
type Output = &'static str;
fn poll(mut self: Pin<&mut Self>, \_cx: &mut Context<'\_>) -> Poll<Self::Output> {
if self.done {
Poll::Ready("готово!")
} else {
self.done = true;
Poll::Pending
}
}
}
Здесь self: Pin<&mut Self> — обязательное условие реализации poll, чтобы гарантировать, что Future не будет перемещён между poll() вызовами.
Таким образом, Pin — мощный инструмент контроля владения и адресации в Rust. Он позволяет безопасно работать с объектами, которые по тем или иным причинам не могут быть перемещены. Хотя в большинстве случаев программисты не взаимодействуют с Pin напрямую, он — основа для построения безопасной асинхронной модели Rust и взаимодействия с unsafe-кодом.