Раздел 23 · Rust

Pin и самоссылающиеся структуры

lead~45 мин

открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Pin и самоссылающиеся структуры

Прошлый урок показал, что async fn это enum-машина состояний, а в сигнатуре poll стоит загадочный Pin<&mut Self>. Мы отложили вопрос «зачем». Сегодня отвечаем. Окажется, что машина состояний умеет ссылаться сама на себя, а такую структуру нельзя двигать в памяти, иначе ссылка повиснет. Pin это типаж, который запрещает движение. Урок уровня L: тут много unsafe, но за ним простая идея.

Откуда берутся самоссылки

Вспомни, как компилятор раскладывает по полям состояния переменные, которые живут через await. А теперь представь такой async fn:

async fn process() {
    let data = vec![1, 2, 3, 4];
    let slice = &data[1..3];   // ссылка внутрь data
    some_async_op().await;     // точка приостановки
    println!("{slice:?}");     // slice ещё нужен после await
}

И data, и slice живут через await, значит оба попадают в поле состояния. Но slice это ссылка внутрь data, то есть внутрь соседнего поля той же самой структуры. Получается самоссылающаяся структура: одно поле смотрит на другое поле того же объекта.

В чём беда? В Rust move это рутина: вернул значение из функции, положил в Vec, передал по значению, и объект переехал на новый адрес. Все поля переехали вместе с ним, а вот указатель slice хранит старый адрес и теперь смотрит в никуда. Висячий указатель, чтение мусора, неопределённое поведение.

Вот почему футуру нельзя двигать после первого poll: на первом poll она могла создать внутреннюю ссылку, и с этого момента её адрес обязан быть стабильным.

Воспроизводим опасность руками

Соберём самоссылку без всякого async, чтобы увидеть проблему в чистом виде:

pub struct SelfRefUnsafe {
    data: String,
    slice: *const u8, // указатель внутрь data
}

impl SelfRefUnsafe {
    pub fn new(data: impl Into<String>) -> Self {
        SelfRefUnsafe { data: data.into(), slice: std::ptr::null() }
    }

    /// После этого вызова структуру нельзя двигать: переедет буфер data,
    /// и slice повиснет.
    pub fn init(&mut self) {
        self.slice = self.data.as_ptr();
    }

    pub fn first_byte(&self) -> Option<u8> {
        if self.slice.is_null() {
            return None;
        }
        // SAFETY: корректно, только если структуру не двигали после init.
        Some(unsafe { *self.slice })
    }
}

Здесь есть коварная тонкость. String хранит свой буфер на куче, поэтому move самой структуры SelfRefUnsafe двигает только три слова (указатель, длина, ёмкость), а буфер остаётся на месте, и slice случайно продолжает работать. Из-за этого наивный код «не падает» и усыпляет бдительность. Но опираться на это нельзя: замени String на инлайновый массив [u8; 16], и первый же move мгновенно сломает указатель. Мы намеренно не провоцируем неопределённое поведение в коде, а чиним проблему типами. Запомни вывод: самоссылку нельзя двигать, а компилятор по умолчанию двигает что угодно. Нужен способ сказать «это двигать запрещено».

Pin: обещание не двигаться

Pin<P> это обёртка над указателем (&mut T, Box<T> и подобными), которая даёт одну гарантию: значение за этим указателем больше не сдвинется в памяти, пока не будет уничтожено. Это не рантайм-проверка и не замок. Это инвариант системы типов: из Pin<&mut T> безопасным кодом нельзя достать голый &mut T, а без &mut T нельзя ни вызвать mem::swap, ни переместить значение.

Именно поэтому executor получает футуру как Pin<&mut F>: получив её, он физически не может её передвинуть, и самоссылки внутри остаются валидными.

Unpin: кому пиннинг не нужен

Большинству типов самоссылки не грозят. Число, String, Vec, твоя обычная структура из полей-значений: их можно двигать сколько угодно, даже «закреплёнными». Для таких типов есть Unpin: авто-трейт, который означает «меня можно двигать даже из-под Pin».

Тонкость, которая всех путает: для T: Unpin тип Pin<Box<T>> по гарантиям ничем не отличается от обычного Box<T>. Pin для Unpin-типа ничего не закрепляет, потому что закреплять нечего: двигать T и так безопасно. Pin начинает работать только для не-Unpin типов. А не-Unpin компилятор делает сам, когда видит самоссылку, через поле-маркер PhantomPinned. Unpin и его соседи (Send, Sync, Sized) разбирались в уроке про маркерные трейты.

Безопасная самоссылка

Теперь сделаем то же самое, но правильно: закрепим структуру на куче и пометим её !Unpin.

use std::marker::PhantomPinned;
use std::pin::Pin;

pub struct SelfRef {
    data: String,
    slice: *const u8,
    _pin: PhantomPinned, // делает тип !Unpin: безопасный move запрещён
}

impl SelfRef {
    pub fn new(data: impl Into<String>) -> Pin<Box<SelfRef>> {
        let value = SelfRef {
            data: data.into(),
            slice: std::ptr::null(),
            _pin: PhantomPinned,
        };
        // Box::pin закрепляет значение на куче: его адрес теперь стабилен.
        let mut boxed = Box::pin(value);
        Self::init(boxed.as_mut());
        boxed
    }

    fn init(self: Pin<&mut Self>) {
        let data_ptr = self.data.as_ptr();
        // SAFETY: только записываем сырой указатель в поле, не двигаем структуру
        // и не выдаём наружу &mut, нарушающий инвариант Pin.
        let this = unsafe { self.get_unchecked_mut() };
        this.slice = data_ptr;
    }

    pub fn first_byte(self: Pin<&Self>) -> Option<u8> {
        if self.slice.is_null() {
            return None;
        }
        // SAFETY: объект закреплён через Pin<Box<..>> и !Unpin, значит data не
        // переезжала с момента init, slice указывает на живой буфер.
        Some(unsafe { *self.slice })
    }
}

Разберём, что изменилось. Поле _pin: PhantomPinned делает SelfRef не-Unpin. Конструктор сразу кладёт значение в Box::pin, получая Pin<Box<SelfRef>>: адрес на куче зафиксирован. init берёт Pin<&mut Self> и через get_unchecked_mut (вот он, unsafe) записывает указатель. Этот unsafe мы берём на себя осознанно: мы клянёмся не двигать структуру, и Pin нам в этом помогает, не давая безопасного &mut наружу.

Теперь самоссылка валидна навсегда. Структуру можно как угодно передавать: сам Box (указатель на кучу) копируется, но объект на куче остаётся на месте.

let s = SelfRef::new("hello");
assert_eq!(s.as_ref().first_byte(), Some(b'h'));

fn take(s: Pin<Box<SelfRef>>) -> Option<u8> {
    s.as_ref().first_byte() // объект не двигался, ссылка цела
}
assert_eq!(take(s), Some(b'h'));

Два конструктора Pin

У Pin ровно два способа получить закреплённый указатель, и разница между ними это вся граница безопасности.

Pin::new(pointer) безопасен, но требует T: Unpin. Логика прямая: если значение и так можно двигать, закреплять его не опасно, обещать нечего. Поэтому в прошлом уроке, когда мы опросили под-футуры StepA и StepB, мы писали Pin::new(fut): их поля это числа и bool, они Unpin.

Pin::new_unchecked(pointer) это unsafe дверь для не-Unpin типов. Зовущий клянётся: «значение за указателем не сдвинется до своего drop». Именно её придётся использовать в block_on следующего урока, когда мы закрепляем произвольную (возможно !Unpin) футуру на стеке:

let mut future = future;
// SAFETY: future живёт до конца функции и больше не двигается (работаем только
// через &mut по этой Pin), поэтому закрепление на стеке корректно.
let mut future = unsafe { Pin::new_unchecked(&mut future) };

Симметрично работает и обратная сторона. Pin::get_mut отдаёт безопасный &mut T, но только для T: Unpin. Для не-Unpin типа есть лишь get_unchecked_mut (unsafe), который мы и звали в SelfRef::init. Правило запоминается одной фразой: всё безопасное в Pin требует Unpin, всё про не-Unpin требует unsafe и твоего обещания не двигать.

Проекции: structurally pinned против обычного поля

В реальном коде ты редко пишешь сырые указатели внутрь буфера. Проблема всплывает иначе: у тебя комбинатор с несколькими полями, он закреплён как Pin<&mut Self>, и тебе надо опросить вложенную футуру, то есть получить Pin<&mut field>. Это называется проекция.

Соль в том, что поля делятся на два сорта. Поле structurally pinned наследует пиннинг структуры: раз закреплена структура, закреплено и поле, и проекция даёт Pin<&mut Field>. Обычное поле пиннинг не наследует: к нему берут простой &mut Field. Напишем проекцию руками, чтобы увидеть оба случая:

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

/// Комбинатор: полить вложенную футуру f, считая число вызовов poll.
pub struct Counted<F> {
    f: F,        // structurally pinned: это футура, полим её через Pin<&mut f>
    count: u32,  // обычное поле: число, двигать безопасно, берём &mut
}

impl<F: Future> Future for Counted<F> {
    type Output = (u32, F::Output);

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // SAFETY: разбираем Pin вручную. Обещаем: self целиком не двигаем;
        // поле f трактуем как закреплённое (не двигаем, не выдаём &mut наружу);
        // поле count двигать можно, оно по смыслу Unpin.
        let this = unsafe { self.get_unchecked_mut() };

        // count: обычное поле, обычный &mut, без всякого пиннинга.
        this.count += 1;

        // f: наследуем пиннинг. f лежит внутри закреплённого self, значит и сам
        // f не двигается, поэтому new_unchecked здесь корректен.
        let f = unsafe { Pin::new_unchecked(&mut this.f) };
        match f.poll(cx) {
            Poll::Ready(out) => Poll::Ready((this.count, out)),
            Poll::Pending => Poll::Pending,
        }
    }
}

Ошибиться легко и дорого. Выдай ты на f обычный &mut (а через него mem::replace или mem::swap), и закреплённую футуру с самоссылкой можно передвинуть, то есть сломать. Наследуй ты пиннинг для count без нужды, и тип станет жёстче, чем надо. Решение, что поле structurally pinned, тянет за собой обязательства: никогда не двигать это поле, никогда не выдавать на него безопасный &mut, считать структуру Unpin только если это поле Unpin, и аккуратно соблюсти гарантию drop (о ней ниже). Перечень длинный, легко забыть пункт.

Поэтому руками так почти не пишут, а берут крейт pin-project. Тот же Counted с ним выглядит так, и весь unsafe берёт на себя макрос:

use pin_project::pin_project;

#[pin_project]
pub struct Counted<F> {
    #[pin]      // помечаем f как structurally pinned
    f: F,
    count: u32, // без #[pin]: обычное поле
}

impl<F: Future> Future for Counted<F> {
    type Output = (u32, F::Output);

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.project();      // безопасные проекции
        *this.count += 1;               // count: &mut u32
        match this.f.poll(cx) {         // f: Pin<&mut F>
            Poll::Ready(out) => Poll::Ready((*this.count, out)),
            Poll::Pending => Poll::Pending,
        }
    }
}

#[pin] это и есть пометка «structurally pinned»; макрос разбирает её и генерирует тот самый unsafe, что мы писали выше, но без права на ошибку. Когда дойдёшь до своих комбинаторов поверх футур, бери pin-project, а не сырые указатели.

Гарантия drop

У контракта Pin две половины, и вторую часто забывают. Первая: пока значение закреплено, оно не двигается. Вторая, drop guarantee: для не-Unpin значения его память нельзя переиспользовать или освободить, пока не отработал его Drop. Иначе говоря, закреплённое значение обязано «дожить до своего деструктора» на том же адресе.

Зачем это нужно, видно на самоссылке. Если бы можно было освободить буфер под SelfRef, не вызвав drop, то любой внешний указатель на этот объект (а самоссылающиеся типы как раз для того и существуют, чтобы кто-то на них ссылался) повис бы. mem::forget при этом разрешён: он навсегда теряет значение, но не переиспользует его байты, так что ничего не повисает. А вот положить закреплённое !Unpin значение в хранилище, которое потом перезапишет эти байты без вызова drop, контракт запрещает. На практике эту половину контракта соблюдают за тебя Box::pin и pin-project, но знать про неё надо: именно она делает самоссылки безопасными до самого конца жизни объекта.

В нашем executor’е (следующий урок) самоссылок внутри задач нет: мы держим футуру в Box и двигаем только сам Box, поэтому обходимся Pin::new_unchecked в одном месте и не возимся с проекциями. Но знать, откуда растёт Pin, надо: без него весь async был бы небезопасен.

Что унести из урока

Машина состояний async fn может ссылаться сама на себя: поле-ссылка смотрит на поле-данные той же структуры. Двигать такую структуру нельзя, иначе ссылка повиснет, а компилятор по умолчанию двигает значения свободно. Pin<P> это инвариант системы типов «значение за указателем не сдвинется до drop», и executor получает футуру именно как Pin<&mut F>. Почти все типы Unpin (двигать безопасно), и для них Pin ничего не меняет; не-Unpin делают через PhantomPinned, и только для них Pin работает по-настоящему. Проекции на поля не пиши руками, бери pin-project.

Дальше, в уроке 23-rust/51 · Свой исполнитель, мы наконец соберём ту сторону, что двигает футуры: Waker через сырую vtable, очередь задач и reactor поверх epoll. Весь Pin оттуда и пришёл.

Домашка