Когда безопасно вывести значение члена из закрепленного будущего?

Я пишу будущий комбинатор, который должен использовать значение, которое ему было предоставлено. С фьючерсами 0.1 Future::poll взял self: &mut Self, что фактически означало, что мой комбинатор содержал Option и я вызвал Option::take on, когда базовое будущее разрешается.

Вместо этого метод Future::poll в стандартной библиотеке принимает self: Pin<&mut Self>, поэтому я читал о гарантиях, необходимых для безопасного использования Pin.

Из документации по pin модулю на гарантию Drop (выделено мое):

Конкретно, для закрепленных данных вы должны поддерживать инвариант, чтобы его память не становилась недействительной с момента, когда она закреплена, до момента вызова drop. Память может быть аннулирована путем освобождения, но также путем замены Some(v) на None или вызова Vec::set_len чтобы "убить" некоторые элементы из вектора.

И проекции и структурное закрепление (выделено мной):

Вы не должны предлагать какие-либо другие операции, которые могут привести к удалению данных из полей при закреплении вашего типа. Например, если обертка содержит Option<T> и есть операция типа like с типом fn(Pin<&mut Wrapper<T>>) → Option<T>, эту операцию можно использовать для перемещения T из закрепленного Wrapper<T> - это означает, что закрепление не может быть структурным.

Однако существующий комбинатор Map вызывает Option::take значение элемента, когда базовое будущее разрешено:

fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> {
    match self.as_mut().future().poll(cx) {
        Poll::Pending => Poll::Pending,
        Poll::Ready(output) => {
            let f = self.f().take()
                .expect("Map must not be polled after it returned 'Poll::Ready'");
            Poll::Ready(f(output))
        }
    }
}

Метод f генерируется unsafe_unpinned и выглядит примерно так:

fn f<'a>(self: Pin<&'a mut Self>) -> &'a mut Option<F> {
    unsafe { &mut Pin::get_unchecked_mut(self).f }
}

Похоже, что Map нарушает требования, описанные в документации по pin, но я считаю, что авторы комбинатора Map знают, что они делают, и что этот код безопасен.

Какая логика позволяет им выполнять эту операцию безопасным способом?

Ответы

Ответ 1

Это все о структурном закреплении.

Во-первых, я буду использовать синтаксис P<T> для обозначения чего-то вроде impl Deref<Target = T> - некоторого (умного) указателя типа P который Deref::deref для T Pin только "относится" к/имеет смысл для таких (умных) указателей.

Допустим, у нас есть:

struct Wrapper<Field> {
    field: Field,
}

Первоначальный вопрос

Можем ли мы получить Pin<P<Field>> из Pin<P<Wrapper<Field>>>, "проецируя" наш Pin<P<_>> из Wrapper на его field?

Для этого требуется базовая проекция P<Wrapper<Field>> → P<Field>, которая возможна только для:

  • общие ссылки (P<T> = &T). Это не очень интересный случай, учитывая, что Pin<P<T>> всегда deref к T

  • уникальные ссылки (P<T> = &mut T).

Я буду использовать синтаксис &[mut] T для этого типа проекции.

Вопрос теперь становится:

Можем ли мы перейти от Pin<&[mut] Wrapper<Field>> к Pin<&[mut] Field>?

Вопрос, который может быть неясным из документации, заключается в том, что решение за создателем Wrapper должен решать сам!

Существует два возможных варианта для автора библиотеки для каждого поля структуры.

Существует структурная проекция Pin на это поле

Например, pin_utils::unsafe_pinned! макрос используется для определения такой проекции (Pin<&mut Wrapper<Field>> → Pin<&mut Field>).

Чтобы Pin проецировался правильно:

  • вся структура должна реализовывать Unpin только тогда, когда все поля, для которых есть структурная проекция Pin реализуют Unpin.

    • никакая реализация не может использовать unsafe для перемещения таких полей из Pin<&mut Wrapper<Field>> (или Pin<&mut Self> когда Self = Wrapper<Field>). Например, Option::take() запрещен.
  • вся структура может реализовывать Drop только если Drop::drop не перемещает ни одно из полей, для которых существует структурная проекция.

  • структура не может быть #[repr(packed)] (следствие предыдущего элемента).

В вашем данном примере future::Map это случай future поля структуры Map.

Там нет структурной проекции Pin на это поле

Например, pin_utils::unsafe_unpinned! макрос используется для определения такой проекции (Pin<&mut Wrapper<Field>> → &mut Field).

В этом случае это поле не считается закрепленным с помощью Pin<&mut Wrapper<Field>>.

  • не имеет значения Field Unpin или нет.

    • реализациям разрешено использовать unsafe для перемещения таких полей из Pin<&mut Wrapper<Field>>. Например, Option::take() разрешен.
  • Drop::drop также разрешено перемещать такие поля,

В приведенном вами примере future::Map это случай поля f структуры Map.

Пример обоих типов проекции

impl<Fut, F> Map<Fut, F> {
    unsafe_pinned!(future: Fut); // pin projection -----+
    unsafe_unpinned!(f: Option<F>); // not pinned --+   |
//                                                  |   |
//                 ...                              |   |
//                                                  |   |
    fn poll (mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> {
        //                                          |   |
        match self.as_mut().future().poll(cx) { // <----+ required here
            Poll::Pending => Poll::Pending, //      |
            Poll::Ready(output) => { //             |
                let f = self.f().take() // <--------+ allows this

Ответ 2

редактировать: этот ответ неверен. Это остается здесь для потомков.

Давайте начнем с напоминания, почему Pin был введен в первую очередь: мы хотим статически гарантировать, что фьючерсы с собственной ссылкой не могут быть перемещены, тем самым аннулируя их внутренние ссылки.

Имея это в виду, давайте взглянем на определение Map.

pub struct Map<Fut, F> {
    future: Fut,
    f: Option<F>,
}

Map имеет два поля: первое хранит будущее, второе - закрытие, которое отображает результат этого будущего на другое значение. Мы хотим поддерживать хранение самоссылающихся типов непосредственно в future не размещая их за указателем. Это означает, что если Fut является ссылочным типом, Map не может быть перемещен после того, как он создан. Вот почему мы должны использовать Pin<&mut Map> в качестве приемника для Future::poll. Если обычная изменяемая ссылка на Map содержащую ссылочное будущее, когда-либо использовалась разработчиком Future, пользователи могли заставить UB использовать только безопасный код, вызывая перемещение Map с помощью mem::replace.

Однако нам не нужно поддерживать хранение самоссылочных типов в f. Если мы предположим, что самоссылочная часть Map полностью содержится в future, мы можем свободно изменять f до тех пор, пока не допустим перемещения future.

Хотя самоссылочное закрытие было бы очень необычным, предположение о том, что f безопасно перемещать (что эквивалентно F: Unpin), нигде явно не указано. Тем не менее, мы по-прежнему перемещаем значение в f в Future::poll, вызывая take ! Я думаю, что это действительно ошибка, но я не уверен на 100%. Я думаю, что для f() должен требоваться F: Unpin что означало бы, что Map может реализовать Future только когда аргумент замыкания безопасен для перемещения из-за Pin.

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