Закрепление (Pinning)
Закрепление — это печально известная сложная концепция, обладающая некоторыми тонкими и запутанными свойствами. Этот раздел подробно рассмотрит тему (возможно, слишком подробно). Закрепление является ключевым для реализации асинхронного программирования в Rust1, но можно далеко продвинуться, никогда не сталкиваясь с закреплением и, конечно, не имея глубокого понимания.
Первый раздел даст краткое описание закрепления, которого, надеюсь, достаточно для большинства асинхронных программистов. Остальная часть этой главы предназначена для реализаторов, других, занимающихся продвинутым или низкоуровневым асинхронным программированием, и любознательных.
После краткого описания эта глава даст некоторую предысторию о семантике перемещения, прежде чем перейти к закреплению. Мы рассмотрим общую идею, затем типы Pin и Unpin, как закрепление достигает своих целей, и несколько тем о работе с закреплением на практике. Затем следуют разделы о закреплении и асинхронном программировании, а также некоторые альтернативы и расширения закрепления (для самых любознательных). В конце главы приведены ссылки на альтернативные объяснения и справочные материалы.
TL;DR
Pin помечает указатель как указывающий на объект, который не будет перемещаться до его удаления. Закрепление не встроено в язык или компилятор; оно работает путем простого ограничения доступа к изменяемым ссылкам на указываемый объект. Достаточно легко нарушить закрепление в небезопасном коде, но, как и все гарантии безопасности в небезопасном коде, ответственность за то, чтобы не делать этого, лежит на программисте.
Гарантируя, что объект не будет перемещен, закрепление делает безопасным наличие ссылок из одного поля структуры на другое (иногда называемых самоссылками). Это требуется для реализации асинхронных функций (которые реализуются как структуры данных, где переменные хранятся как поля, поскольку переменные могут ссылаться друг на друга, поля фьючерса, реализующего асинхронную функцию, должны иметь возможность ссылаться друг на друга). В основном программистам не нужно знать об этой детали, но при прямой работе с фьючерсами вам может понадобиться это, потому что сигнатура Future::poll требует, чтобы self был закреплен.
Если вы используете фьючерсы по ссылке, вам может понадобиться закрепить ссылку с помощью pin!(...), чтобы гарантировать, что ссылка все еще реализует трейт Future (это часто возникает с макросом select). Аналогично, если вы хотите вручную вызвать poll на фьючерсе (обычно потому, что вы реализуете другой фьючерс), вам понадобится закрепленная ссылка на него (используйте pin! или убедитесь, что аргументы имеют закрепленные типы). Если вы реализуете фьючерс или у вас есть закрепленная ссылка по какой-то другой причине, и вы хотите изменяемый доступ к внутренностям объекта, вам нужно будет понять раздел ниже о закрепленных полях, чтобы знать, как это сделать и когда это безопасно.
Семантика перемещения (Move semantics)
Полезной концепцией для обсуждения закрепления и связанных тем является идея мест (places). Место — это фрагмент памяти (с адресом), где может жить значение. Ссылка указывает не на фактическое значение, а на место. Вот почему *ref = ... имеет смысл: разыменование дает вам место, а не копию значения. Места хорошо известны реализаторам языков, но обычно неявны в языках программирования (в Rust они неявны). У программистов обычно есть хорошая интуиция для мест, но они могут не думать о них явно.
Помимо ссылок, переменные и обращения к полям вычисляются в места. Фактически, все, что может появиться в левой части присваивания, должно быть местом во время выполнения (поэтому места называются «lvalue» в жаргоне компиляторов).
В Rust изменяемость является свойством мест, как и «замороженность» в результате заимствования (мы можем сказать, что место заимствовано).
Присваивание в Rust перемещает данные (в основном, некоторые простые данные имеют семантику копирования, но это не так важно). Когда мы пишем let b = a;, данные, которые были в памяти в месте, идентифицируемом a, перемещаются в место, идентифицируемое b. Это означает, что после присваивания данные существуют в b, но больше не существуют в a. Или, другими словами, адрес объекта изменяется присваиванием2.
Если бы существовали указатели на место, из которого было перемещено, указатели были бы недействительны, поскольку они больше не указывают на объект. Вот почему заимствованные ссылки предотвращают перемещение: let r = &a; let b = a; незаконно, существование r предотвращает перемещение a.
Компилятор знает только о ссылках извне объекта в объект (как в приведенном выше примере, или ссылке на поле объекта). Ссылка полностью внутри объекта была бы невидима для компилятора. Представьте, если бы нам разрешили написать что-то вроде:
#![allow(unused)] fn main() { struct Bad { field: u64, r: &'self u64, } }
Мы могли бы иметь экземпляр b структуры Bad, где b.r указывает на b.field. В let a = b; внутренняя ссылка b.r на b.field невидима для компилятора, поэтому выглядит, что нет ссылок на b, и, следовательно, перемещение в a было бы нормально. Однако, если бы это произошло, то после перемещения a.r указывала бы не на a.field, как мы хотели, а на недействительную память по старому местоположению b.field, нарушая гарантии безопасности Rust.
Перемещение данных не ограничивается значениями. Данные также могут быть перемещены из уникальной ссылки. Разыменование Box перемещает данные из кучи в стек. take, replace и swap (все в std::mem) перемещают данные из изменяемой ссылки (&mut T). Перемещение из Box делает указываемое место недействительным. Перемещение из изменяемой ссылки оставляет место действительным, но содержащим другие данные.
Абстрактно, перемещение реализуется копированием битов из источника в назначение и последующим стиранием битов источника. Однако компилятор может оптимизировать это многими способами.
Закрепление (Pinning)
Важное примечание: я начну с обсуждения абстрактной концепции закрепления, которая не точно то, что выражается каким-либо конкретным типом. Мы будем делать концепцию более конкретной по мере продвижения и в итоге придем к точным определениям того, что означают разные типы, но ни один из этих типов не означает exactly то же, что и концепция закрепления, с которой мы начнем.
Объект закреплен, если он не будет перемещен или иным образом инвалидирован. Как я объяснил выше, это не новая концепция — заимствование объекта предотвращает перемещение объекта на время заимствования. Может ли объект быть перемещен или нет, не явно в типах Rust, хотя это известно компилятору (вот почему вы можете получать сообщения об ошибках «cannot move out of»). В отличие от заимствования (и временного ограничения на перемещения, вызванного заимствованием), закрепление является постоянным. Объект может измениться с незакрепленного на закрепленный, но once он закреплен, он должен оставаться закрепленным до его удаления3.
Так же, как типы указателей отражают владение и изменяемость указываемого объекта (например, Box vs &, &mut vs &), мы хотим отражать закрепленность в типах указателей тоже. Это не свойство указателя — указатель не закреплен или перемещаем — это свойство указываемого места: может ли указываемый объект быть перемещен из своего места.
Грубо, Pin<Box<T>> — это указатель на владеемый, закрепленный объект, а Pin<&mut T> — это указатель на уникально заимствованный, изменяемый, закрепленный объект (ср., &mut T, который является указателем на уникально заимствованный, изменяемый объект, который может быть закреплен или нет).
Концепция закрепления не была добавлена в Rust до версии 1.0, и по причинам обратной совместимости нет способа явно выразить, закреплен объект или нет. Мы можем only выразить, что ссылка указывает на закрепленный или не закрепленный объект.
Закрепление ортогонально изменяемости. Объект может быть изменяемым и либо закрепленным (Pin<&mut T>), либо нет (&mut T) (т.е. объект может быть изменен, и либо он закреплен на месте, либо может быть перемещен), или неизменяемым и либо закрепленным (Pin<&T>), либо нет (T) (т.е. объект не может быть изменен, и либо он не может быть перемещен, либо может быть перемещен, но не изменен). Обратите внимание, что &T не может быть изменен или перемещен, но не закреплен, потому что его неперемещаемость только временна.
Unpin
Хотя перемещение и неперемещение — это то, как мы ввели закрепление, и это somewhat подразумевается названием, Pin на самом деле мало что говорит вам о том, будет ли указываемый объект actually перемещен или нет.
Что? Вздох.
Закрепление на самом деле является контрактом о validity, а не о перемещении. Оно гарантирует, что если объект чувствителен к адресу, то его адрес не изменится (и, следовательно, адреса, производные от него, такие как адреса его полей, также не изменятся). Большинство данных в Rust не чувствительны к адресу. Их можно перемещать, и все будет в порядке. Pin гарантирует, что указываемый объект будет valid относительно своего адреса. Если указываемый объект чувствителен к адресу, то он не может быть перемещен; если он не чувствителен к адресу, то не имеет значения, перемещен он или нет.
Unpin — это трейт, который выражает, являются ли объекты чувствительными к адресу. Если объект реализует Unpin, то он не чувствителен к адресу. Если объект !Unpin, то он чувствителен к адресу. Альтернативно, если мы думаем о закреплении как о действии удержания объекта на его месте, то Unpin означает, что безопасно отменить это действие и позволить объекту быть перемещенным.
Unpin — это автотрейт, и большинство типов являются Unpin. Only типы, которые имеют поле !Unpin или которые явно отказываются, не являются Unpin. Вы можете отказаться, имея поле PhantomPinned или (если вы используете ночную версию) с impl !Unpin for ... {}.
Для типов, которые реализуют Unpin, Pin essentially ничего не делает. Pin<Box<T>> и Pin<&mut T> могут использоваться так же, как Box<T> и &mut T. Фактически, для типов Unpin закрепленные и обычные указатели могут быть свободно преобразованы друг в друга с помощью Pin::new и Pin::into_inner. Стоит повторить: Pin<...> не гарантирует, что указываемый объект не переместится, only что указываемый объект не переместится, если он !Unpin.
Практическое следствие вышесказанного заключается в том, что работа с типами Unpin и закреплением much проще, чем с типами, которые не Unpin, фактически маркер Pin basically не влияет на типы Unpin и указатели на типы Unpin, и вы можете basically игнорировать все гарантии и требования закрепления.
Unpin не следует понимать как свойство объекта alone; единственное, что Unpin меняет, — это то, как объект взаимодействует с Pin. Использование ограничения Unpin вне контекста закрепления не влияет на поведение компилятора или то, что можно сделать с объектом. Единственная причина использовать Unpin — в сочетании с закреплением или для распространения ограничения туда, где оно используется с закреплением.
Pin
Pin — это тип-маркер, он важен для проверки типов, но компилируется и не существует во время выполнения (Pin<Ptr> гарантированно имеет то же расположение в памяти и ABI, что и Ptr). Это обертка указателей (таких как Box), поэтому он ведет себя как тип указателя, но он не добавляет косвенности, Box<Foo> и Pin<Box<Foo>> одинаковы при запуске программы. Лучше думать о Pin как о модификаторе указателя, а не как об указателе самом по себе.
Pin<Ptr> означает, что указываемый объект Ptr (не сам Ptr) закреплен. То есть, Pin гарантирует, что указываемый объект (не указатель) останется valid относительно своего адреса до тех пор, пока указываемый объект не будет удален. Если указываемый объект чувствителен к адресу (т.е. !Unpin), то указываемый объект не будет перемещен.
Закрепление значений (Pinning values)
Объекты не создаются закрепленными. Объект начинается незакрепленным (и может свободно перемещаться), он становится закрепленным, когда создается закрепляющий указатель, указывающий на объект. Если объект Unpin, то это тривиально с использованием Pin::new, однако, если объект не Unpin, то его закрепление должно гарантировать, что он не может быть перемещен или инвалидирован через псевдоним.
Чтобы закрепить объект в куче, вы можете создать новый закрепляющий Box с помощью Box::pin или преобразовать существующий Box в закрепляющий Box с помощью Box::into_pin. В любом случае вы получите Pin<Box<T>>. Некоторые другие указатели (такие как Arc и Rc) имеют похожие механизмы. Для указателей, которые не имеют, или для ваших собственных типов указателей, вам нужно будет использовать Pin::new_unchecked для создания закрепленного указателя4. Это небезопасная функция, поэтому программист должен обеспечить соблюдение инвариантов Pin. То есть, что указываемый объект будет, при любых обстоятельствах, оставаться valid до вызова его деструктора. Есть некоторые тонкие детали для обеспечения этого, обратитесь к документации функции или разделу ниже как работает закрепление для получения дополнительной информации.
Box::pin закрепляет объект в месте в куче. Чтобы закрепить объект в стеке, вы можете использовать макрос pin для создания и закрепления изменяемой ссылки (Pin<&mut T>)5.
Tokio также имеет макрос pin, который делает то же самое, что и std макрос, и также поддерживает присваивание переменной внутри макроса. Крейты futures-rs и pin-utils имеют макрос pin_mut, который раньше часто использовался, но теперь устарел в пользу вышеупомянутых крейтов и функциональности, теперь находящейся в std.
Вы также можете использовать Pin::static_ref и Pin::static_mut для закрепления статической ссылки.
Использование закрепленных типов (Using pinned types)
Теоретически, использование закрепленных указателей похоже на использование любого другого типа указателя. Однако, поскольку это не самая интуитивная абстракция и поскольку у нее нет языковой поддержки, использование закрепленных указателей tends to быть довольно неэргономичным. Самый распространенный случай использования закрепления — при работе с фьючерсами и потоками, мы рассмотрим эти specifics более подробно ниже.
Использование закрепленного указателя как неизменяемо заимствованной ссылки тривиально из-за реализации Deref для Pin. Вы можете mostly просто обращаться с Pin<Ptr<T>> как с &T, используя явный deref() при необходимости. Аналогично, получение Pin<&T> довольно легко с использованием as_ref().
Самый распространенный способ работы с закрепленными типами — использование Pin<&mut T> (например, в Future::poll), однако, самый простой способ создать закрепленный объект — Box::pin, который дает Pin<Box<T>>. Вы можете преобразовать последнее в первое с помощью Pin::as_mut. Однако, без языковой поддержки для повторного использования ссылок (неявное перезаимствование), вам приходится продолжать вызывать as_mut вместо повторного использования результата. Например (из документации as_mut),
#![allow(unused)] fn main() { impl Type { fn method(self: Pin<&mut Self>) { // делаем что-то } fn call_method_twice(mut self: Pin<&mut Self>) { // `method` потребляет `self`, поэтому перезаимствуем `Pin<&mut Self>` через `as_mut`. self.as_mut().method(); self.as_mut().method(); } } }
Если вам нужно получить доступ к закрепленному указываемому объекту каким-либо другим способом, вы можете сделать это через Pin::into_inner_unchecked. Однако, это небезопасно, и вы должны быть очень осторожны, чтобы обеспечить соблюдение требований безопасности Pin.
Как работает закрепление (How pinning works)
Pin — это простая структура-обертка (aka, newtype) для указателей. Она обеспечивает работу only с указателями, требуя ограничения Deref на свой generic параметр для чего-либо полезного, однако, это только для выражения намерения, а не для сохранения безопасности. Как и большинство оберток newtype, Pin существует для выражения инварианта во время компиляции, а не для какого-либо эффекта во время выполнения. Действительно, в большинстве обстоятельств Pin и механизм закрепления completely исчезнут во время компиляции.
Если быть точным, инвариант, выраженный Pin, касается validity, а не just перемещаемости. Это также инвариант validity, который применяется only once указатель закреплен — до этого Pin не имеет эффекта и не предъявляет требований к тому, что происходит до того, как что-то закреплено. Once указатель закреплен, Pin требует (и гарантирует в безопасном коде), что указываемый объект останется valid по тому же адресу в памяти до вызова деструктора объекта.
Для неизменяемых указателей (например, заимствованных ссылок) Pin не имеет эффекта — поскольку указываемый объект не может быть изменен или заменен, нет опасности его инвалидации.
Для указателя, который позволяет изменение (например, Box или &mut), наличие прямого доступа к этому указателю или доступа к изменяемой ссылке (&mut) на указываемый объект может позволить изменить или переместить указываемый объект. Pin simply не предоставляет никакого (не-unsafe) способа получить прямой доступ к указателю или изменяемой ссылке. Обычный способ для указателя предоставить изменяемую ссылку на свой указываемый объект — реализовать DerefMut, Pin реализует DerefMut only если указываемый объект Unpin.
Эта реализация невероятно проста! Подведем итог: Pin — это структура-обертка вокруг указателя, которая предоставляет only неизменяемый доступ к указываемому объекту (и изменяемый доступ, если указываемый объект Unpin). Все остальное — детали (и тонкие инварианты для небезопасного кода). Для удобства Pin предоставляет возможность преобразования между типами Pin (всегда безопасно, поскольку указатель не может выйти из Pin) и т.д.
Pin также предоставляет небезопасные функции для создания закрепленных указателей и доступа к базовым данным. Как и все unsafe функции, поддержание инвариантов безопасности является ответственностью программиста, а не компилятора. К сожалению, инварианты безопасности для закрепления somewhat разбросаны, в том смысле, что они применяются в разных местах и их трудно описать глобально, унифицированно. Я не буду описывать их здесь подробно и отсылаю вас к документации, но попытаюсь обобщить (см. документацию модуля для подробного обзора):
- Создание нового закрепленного указателя
new_unchecked. Программист должен обеспечить, чтобы указываемый объект был закреплен (то есть соблюдал инварианты закрепления). Это требование может быть удовлетворено только типом указателя (например, в случаеBox) или может требовать участия типа указываемого объекта (например, в случае&mut). Это включает (но не ограничивается):- Не перемещение из
selfвDerefиDerefMut. - Правильную реализацию
Drop, см. гарантию drop. - Отказ от
Unpin(с использованиемPhantomPinned), если вам требуются гарантии закрепления. - Указываемый объект не может быть
#[repr(packed)].
- Не перемещение из
- Доступ к закрепленному значению
into_inner_unchecked,get_unchecked_mut,map_uncheckedиmap_unchecked_mut. Ответственность за обеспечение гарантий закрепления (включая неперемещение данных) ложится на программиста с момента доступа к данным до завершения работы их деструктора (обратите внимание, что эта область ответственности выходит за пределы небезопасного вызова и применяется к тому, что происходит с базовыми данными). - Не предоставление любого другого способа переместить данные из закрепленного типа (что потребовало бы небезопасной реализации).
Типы закрепляющих указателей (Pinning pointer types)
Мы сказали ранее, что Pin оборачивает тип указателя. Часто можно видеть Pin<Box<T>>, Pin<&T> и Pin<&mut T>. Технически, единственное требование к типу закрепляющего указателя — это то, что он реализует Deref. Однако, нет способов создать Pin<Ptr> для любых других типов указателей, кроме использования небезопасного кода (через new_unchecked). Это накладывает требования на тип указателя для обеспечения контракта закрепления:
- Реализации
DerefиDerefMutуказателя не должны перемещать данные из их указываемого объекта. - Должно быть невозможно получить ссылку
&mutна указываемый объект в любое время после созданияPin, даже после того, какPinбыл удален (вот почему нельзя безопасно построитьPin<&mut T>из&mut T). Это должно оставаться истинным через несколько шагов или через ссылки (что предотвращает использованиеRcилиArc). - Реализация
Dropдля указателя не должна перемещать (или иным образом инвалидировать) его указываемый объект.
См. документацию new_unchecked docs для большей детализации.
Pinning и Drop
Контракт закрепления применяется до тех пор, пока закрепленный объект не будет удален (технически, это означает, когда его метод drop возвращается, а не когда он вызывается). Обычно это довольно straightforward, поскольку drop вызывается автоматически при уничтожении объектов. Если вы делаете что-то вручную с жизненным циклом объекта, вам, возможно, придется подумать об этом дополнительно. Если у вас есть объект, который (или может быть) закреплен, и этот объект не Unpin, то вы должны вызвать его метод drop (используя drop_in_place) перед освобождением или повторным использованием памяти или адреса объекта. См. std docs для деталей.
Если вы реализуете тип, чувствительный к адресу (т.е. !Unpin), то вы должны быть особенно осторожны с реализацией Drop. Даже though тип self в drop — &mut Self, вы должны рассматривать тип self как Pin<&mut Self>. Другими словами, вы должны обеспечить, чтобы объект оставался valid до возврата из функции drop. Один из способов сделать это явным в исходном коде — следовать следующей идиоме:
#![allow(unused)] fn main() { impl Drop for Type { fn drop(&mut self) { // `new_unchecked` нормально, потому что мы знаем, что это значение никогда не используется // снова после удаления. inner_drop(unsafe { Pin::new_unchecked(self)}); fn inner_drop(this: Pin<&mut Self>) { // Фактический код удаления идет здесь. } } } }
Обратите внимание, что требования к validity будут зависеть от реализуемого типа. Рекомендуется точно определять эти требования, особенно касающиеся уничтожения объектов, особенно если может быть задействовано несколько объектов (например, интрузивный связный список). Обеспечение корректности здесь, вероятно, будет интересным!
Закрепленный self в методах (Pinned self in methods)
Вызов методов для закрепленных типов приводит к размышлениям о типе self в этих методах. Если метод не нуждается в изменении self, то вы все еще можете использовать &self, поскольку Pin<...> может разыменовываться в заимствованную ссылку. Однако, если вам нужно изменить self (и ваш тип не Unpin), то вам нужно выбирать между &mut self и self: Pin<&mut Self> (хотя закрепленные указатели не могут быть неявно приведены к последнему типу, их можно легко преобразовать с помощью Pin::as_mut).
Использование &mut self делает реализацию легкой, но означает, что метод не может быть вызван для закрепленного объекта. Использование self: Pin<&mut Self> означает необходимость учитывать проекцию закрепления (см. следующий раздел) и может быть вызван only для закрепленного объекта. Хотя все это немного сбивает с толку, это интуитивно имеет смысл, когда вы помните, что закрепление — это фазовое понятие — объекты начинаются незакрепленными, и в какой-то момент претерпевают фазовое изменение, чтобы стать закрепленными. Методы &mut self — это те, которые могут быть вызваны в первой (незакрепленной) фазе, а методы self: Pin<&mut Self> — это те, которые могут быть вызваны во второй (закрепленной) фазе.
Обратите внимание, что drop принимает &mut self (даже though он может быть вызван в любой фазе). Это связано с ограничением языка и желанием обратной совместимости. Это требует специального обращения в компиляторе и сопровождается требованиями безопасности.
Закрепленные поля, структурное закрепление и проекция закрепления
Учитывая, что объект закреплен, что это говорит нам о «закрепленности» его полей? Ответ зависит от выбора, сделанного реализатором типа данных, нет универсального ответа (действительно, он может быть разным для разных полей одного и того же объекта).
Если закрепленность объекта распространяется на поле, мы говорим, что поле проявляет «структурное закрепление» или что закрепление проецируется на поле. В этом случае должен быть метод проекции fn get_field(self: Pin<&mut Self>) -> Pin<&mut Field>. Если поле не структурно закреплено, то метод проекции должен иметь сигнатуру fn get_field(self: Pin<&mut Self>) -> &mut Field. Реализация любого метода (или реализация аналогичного кода) требует unsafe кода, и любой выбор имеет последствия для безопасности. Распространение закрепления должно быть consistent, поле должно always быть структурно закрепленным или нет, почти always небезопасно, чтобы поле было структурно закрепленным в одни времена и нет в другие.
Закрепление должно проецироваться на поле, если поле является чувствительной к адресу частью агрегатного типа данных. То есть, если закрепление агрегата зависит от закрепления поля, то закрепление должно проецироваться на это поле. Например, если есть ссылка из другой части агрегата в поле, или если есть самоссылка внутри поля, то закрепление должно проецироваться на поле. С другой стороны, для универсальной коллекции закрепление не needs проецироваться на ее содержимое, поскольку коллекция не relies на их поведение (это потому, что коллекция не может relies на реализацию универсальных элементов, которые она содержит, поэтому сама коллекция не может relies на адреса своих элементов).
При написании небезопасного кода вы можете only предполагать, что гарантии закрепления применяются к полям объекта, которые структурно закреплены. С другой стороны, вы можете безопасно относиться к не структурно закрепленным полям как к перемещаемым и не беспокоиться о требованиях закрепления для них. В частности, структура может быть Unpin, even if поле не является, as long as это поле always treated как не структурно закрепленное.
Если поле структурно закреплено, то требования закрепления на агрегатную структуру распространяются на поле. Ни при каких обстоятельствах код не может перемещать содержимое поля, пока агрегат закреплен (это always потребует небезопасного кода). Структурно закрепленные поля должны быть удалены before они перемещены (включая освобождение) even в случае паники, что означает, что необходимо проявлять осторожность внутри реализации Drop агрегата. Более того, агрегатная структура не может быть Unpin, unless все ее структурно закрепленные поля являются.
Макросы для проекции закрепления (Macros for pin projection)
Существуют макросы, доступные для помощи с проекцией закрепления.
Крейт pin-project предоставляет атрибутный макрос #[pin_project] (и вспомогательный атрибут #[pin]), который реализует безопасную проекцию закрепления для вас, создавая закрепленную версию аннотированного типа, к которой можно получить доступ с помощью метода project на аннотированном типе.
Pin-project-lite — это альтернатива, использующая декларативный макрос (pin_project!), который работает очень похоже на pin-project. Pin-project-lite является легковесным в том смысле, что это не процедурный макрос и, следовательно, не добавляет зависимостей для реализации процедурных макросов в ваш проект. Однако он менее выразителен, чем pin-project, и не дает пользовательских сообщений об ошибках. Pin-project-lite рекомендуется, если вы хотите избежать добавления зависимостей процедурных макросов, а pin-project рекомендуется в противном случае.
Pin-utils предоставляет макрос unsafe_pinned для помощи в реализации проекции закрепления, но весь крейт устарел в пользу вышеупомянутых крейтов и функциональности, теперь находящейся в std.
Присваивание закрепленному указателю
Generally безопасно присваивать в закрепленный указатель. Хотя это нельзя сделать обычным способом (*p = ...), это можно сделать с помощью Pin::set. Более generally, вы можете использовать небезопасный код для присваивания в поля указываемого объекта.
Использование Pin::set always безопасно, since ранее закрепленный указываемый объект будет удален, выполняя требования закрепления, и новый указываемый объект не закреплен, until перемещение в закрепленное место завершено. Присваивание в отдельные поля не automatically нарушает требования закрепления, но необходимо проявлять осторожность, чтобы обеспечить, чтобы объект в целом оставался valid. Например, если в поле присваивается значение, то любые другие поля, которые ссылаются на это поле, должны still быть valid с новым объектом (это не часть требований закрепления, но может быть частью других инвариантов объекта).
Копирование одного закрепленного объекта в другое закрепленное место может быть сделано only в небезопасном коде, как безопасность поддерживается, зависит от отдельного объекта. Нет general нарушения требований закрепления — объект, который заменяется, не перемещается, и объект, который копируется, тоже. Однако validity объекта, который заменяется, может иметь требования безопасности, которые usually защищаются закреплением, но в этом случае должны быть установлены программистом. Например, если у нас есть структура с двумя полями a и b, где b ссылается на a, эта ссылка требует закрепления, чтобы оставаться valid. Если такая структура копируется в другое место, то значение b должно быть обновлено, чтобы указывать на новый a, а не на старый.
Закрепление и асинхронное программирование
Надеюсь, вы можете делать все, что вы когда-либо хотите, с асинхронным Rust и никогда не беспокоиться о закреплении. Иногда вы столкнетесь с corner case, который требует использования закрепления, и если вы хотите реализовывать фьючерсы, среду выполнения или подобные вещи, вам нужно будет знать о закреплении. В этом разделе я объясню почему.
Асинхронные функции реализуются как фьючерсы (см. раздел TODO — это общий обзор, убедитесь, что мы объясняем более глубоко и с примерами в другом месте). В каждой точке await выполнение функции может быть приостановлено, и в течение этого времени значения живых переменных должны быть сохранены. Они essentially становятся полями структуры (которая является частью перечисления). Такие переменные могут ссылаться на другие переменные, которые сохранены в фьючерсе, например, рассмотрим:
#![allow(unused)] fn main() { async fn foo() { let a = ...; let b = &a; bar().await; // используем b } }
Сгенерированный объект фьючерса здесь будет чем-то вроде:
#![allow(unused)] fn main() { struct Foo { a: A, b: &'self A, // Инвариант `self.b == &self.a` } }
(Я немного упрощаю, игнорируя состояние выполнения и т.д., но важная часть — это переменные/поля).
Это интуитивно понятно, к сожалению, 'self не существует в Rust. И по хорошей причине! Помните, что объекты Rust могут быть перемещены, поэтому код типа следующего был бы небезопасен:
#![allow(unused)] fn main() { let f1 = Foo { ... }; // f1.b == &f1.a let f2 = f1; // f2.b == &f1.a, но f1 больше не существует, поскольку он переместился в f2 }
Обратите внимание, что это не просто проблема невозможности назвать время жизни, even if мы используем сырые указатели, такой код все равно был бы некорректен.
Однако, если мы знаем, что once он создан, экземпляр Foo никогда не переместится, то все Just Works. (Компилятор имеет концепцию, похожую на 'self, внутренне для таких случаев, как программист, нам пришлось бы использовать сырые указатели и небезопасный код). Эта концепция неперемещения — это точно то, что описывает закрепление.
Мы видим это требование в сигнатуре Future::poll, где тип self (фьючерса) — Pin<&mut Self>. В основном, при использовании async/await компилятор заботится о закреплении и откреплении, и как программисту вам не нужно об этом беспокоиться.
Ручное закрепление (Manual pinning)
Есть некоторые места, где закрепление просачивается через абстракцию async/await. В своей основе это связано с Pin в сигнатуре Future::poll и Stream::poll_next. При использовании фьючерсов и потоков directly (а не через async/await) нам, возможно, придется учитывать закрепление, чтобы все работало. Некоторые common причины, по которым могут понадобиться закрепленные типы:
- Опрос фьючерса или потока — либо в коде приложения, либо при реализации собственного фьючерса.
- Использование boxed фьючерсов. Если вы используете boxed фьючерсы (или потоки) и, следовательно, выписываете типы фьючерсов вместо использования асинхронных функций, вы, вероятно, увидите много
Pin<...>в этих типах и вам нужно будет использоватьBox::pinдля создания фьючерсов. - Реализация фьючерса — внутри
pollselfзакреплен, и поэтому вам нужно работать с проекцией закрепления и/или небезопасным кодом, чтобы получить изменяемый доступ к полямself. - Комбинирование фьючерсов или потоков. Это mostly просто работает, но если вам нужно взять ссылку на фьючерс, а затем опросить его (например, определить фьючерс вне цикла и использовать его в
select!внутри цикла), то вам нужно будет закрепить ссылку на фьючерс, чтобы использовать ссылку как фьючерс. - Работа с потоками — в настоящее время в Rust вокруг потоков меньше абстракции, чем вокруг фьючерсов, поэтому вы more likely использовать методы-комбинаторы (которые technically не требуют закрепления, но, кажется, делают проблемы, связанные со ссылками или созданием фьючерсов/потоков, more prevalent) или even
pollвручную, чем при работе с фьючерсами.
Альтернативы и расширения
Этот раздел для тех, кому любопытен дизайн языка вокруг закрепления. Вам absolutely не нужно читать этот раздел, если вы просто хотите читать, понимать и писать асинхронные программы.
Закрепление трудно понять и может казаться немного неуклюжим, поэтому люди часто задаются вопросом, есть ли лучшая альтернатива или вариация. Я рассмотрю несколько альтернатив и покажу, почему они либо не работают, либо более сложны, чем вы могли бы ожидать.
Однако before этого важно понять исторический контекст закрепления. Если вы разрабатываете совершенно новый язык и хотите поддержать async/await, самоссылки или неперемещаемые типы, certainly есть лучшие способы сделать это, чем закрепление в Rust. Однако async/await, фьючерсы и закрепление были добавлены в Rust после его релиза 1.0 и разработаны в контексте сильной гарантии обратной совместимости. Помимо этого жесткого требования, было требование желания разработать и реализовать эту функцию в разумные сроки. Некоторые решения (например, те, которые включают линейные типы) потребовали бы фундаментальных исследований, проектирования и реализации, которые реально измерялись бы десятилетиями с учетом ресурсов и ограничений проекта Rust.
Альтернативы
Во-первых, рассмотрим класс решений, которые делают типы Rust неперемещаемыми по умолчанию. Обратите внимание, что это значительное изменение фундаментальной семантики Rust; любое решение в этом классе, вероятно, потребовало бы значительных усилий для достижения обратной совместимости (я не буду строить догадки, возможно ли это для конкретных решений, но с такими техниками, как автотрейты, атрибуты derive, издания, инструменты миграции и т.д., это possibly возможно).
Одно предложение (на самом деле, группа предложений, поскольку есть различные способы определить семантику) — иметь маркерный трейт Move (аналогичный Copy), который помечает объекты как перемещаемые, и все другие типы были бы неперемещаемыми. В contrast с Pin, это свойство values, а не указателей, поэтому эффект much более далеко идущий, например, let a = b; было бы ошибкой, если b не реализует Move.
Фундаментальная проблема этого подхода заключается в том, что закрепление сегодня является фазовым понятием (место начинается незакрепленным и становится закрепленным), а типы применяются ко всему времени жизни значений. (Закрепление также лучше понимать как свойство places, а не values, но типы применяются к values, является ли это фундаментальной проблемой для любого подхода на основе трейтов, я не знаю). Это исследуется в этих двух постах в блоге: Two Ways Not to Move и Ergonomic Self-Referential Types for Rust.
Более того, любой трейт Move, вероятно, будет иметь проблемы с обратной совместимостью и приводить к «заразным ограничениям» (т.е. Move или !Move потребовались бы во many, many местах).
Другое предложение — поддержать конструкторы перемещения, подобные C++. Однако это нарушает фундаментальный инвариант Rust, что объекты всегда могут быть перемещены побитово. Это сделало бы Rust much менее предсказуемым и, следовательно, сделало бы программы Rust more трудными для понимания и отладки. Это обратно несовместимое изменение наихудшего рода, потому что оно молча сломало бы небезопасный код, поскольку изменяет фундаментальное предположение, которое могли сделать авторы кода. Более того, усилия по проектированию и реализации, необходимые для такого фундаментального изменения, были бы огромны. В дополнение к этим практическим проблемам, неясно, сработало ли бы это вообще: конструкторы перемещения могли бы использоваться для исправления ссылок в перемещаемом объекте, но могли бы быть ссылки на объект извне объекта, которые нельзя было бы исправить.
Потенциальное решение другого рода — идея смещенных ссылок (offset references). Это ссылка, которая является относительной, а не абсолютной, т.е. поле, которое является смещенной ссылкой на другое поле, always указывало бы внутри того же объекта, even if объект перемещен в памяти. Проблема со смещенными указателями заключается в том, что поле должно быть either смещенным указателем, либо абсолютным указателем. Но ссылки в асинхронной функции становятся полями, которые sometimes ссылаются на память внутри объекта фьючерса, а sometimes ссылаются на память вне его.
Расширения
Существует несколько предложений по повышению мощности и/или упрощению работы с закреплением. В основном это предложения сделать закрепление более first-class частью языка различными способами, а не чисто библиотечной концепцией (они often включают расширения std, а также языка). Я рассмотрю несколько более разработанных идей, они связаны друг с другом и все имеют общую цель улучшения эргономики закрепления за счет упрощения создания и использования закрепленных мест, в частности вокруг структурного закрепления и drop.
Pinned places развивает идею о том, что закрепление является свойством places, а не values или типов, и добавляет модификатор pin/pinned к ссылкам, аналогичный mut. Это интегрируется с перезаимствованием и разрешением методов для улучшения эргономики вызовов методов с закрепленным self.
UnpinCell расширяет идею закрепленных мест для поддержки нативной проекции закрепления полей. MinPin — это более минимальное (и обратно совместимое) предложение для нативной проекции закрепления и лучшей поддержки drop.
Трейт Overwrite — это предложенный трейт, который делает явным различие между разрешением на изменение части объекта (foo.f = ...) и разрешением на перезапись всего объекта (*foo = ...), оба из которых currently разрешены для всех изменяемых ссылок. Предложение также включает неизменяемые поля. Overwrite — это своего рода замена для Unpin, которая (вместе с некоторыми идеями из закрепленных мест) могла бы улучшить работу с закреплением. К сожалению, although оно могло бы быть принято обратно совместимо, переход был бы much более трудоемким, чем для других расширений.
Ссылки
- std docs источник истины для поведения и гарантий
Pinи т.д. Хорошая документация. - RFC 2349 RFC, предложившее закрепление. Стабилизированный API somewhat отличается от предложенного здесь, но в RFC есть хорошее объяснение основной концепции и обоснования.
- Некоторые посты в блогах или другие ресурсы, объясняющие закрепление:
- Pin от WithoutBoats (основного дизайнера закрепления) об истории, контексте и обосновании закрепления и почему это сложная концепция.
- Why is std::pin::Pin so weird? глубокое погружение в обоснование дизайна закрепления и использование закрепления на практике.
- Pin, Unpin, and why Rust needs them
- Pinning section of async/await
- Pin and suffering тщательный пост в блоге в очень разговорном стиле о понимании асинхронного кода и закрепления с множеством примеров.
- Книга Rust for Rustaceans от Jon Gjengset содержит excellent описание того, почему закрепление необходимо для реализации async/await и как работает закрепление.
-
Стоит отметить, что закрепление — это низкоуровневый строительный блок, разработанный специально для реализации асинхронного Rust. Хотя оно не связано напрямую с асинхронным Rust и может использоваться для других целей, оно не было предназначено для быть механизмом общего назначения и, в частности, не является готовым решением для самоссылающихся полей. Использование закрепления для чего-либо, кроме асинхронного кода, обычно работает только тогда, когда оно обернуто в толстые слои абстракции, поскольку потребует много возни и труднообъяснимого небезопасного кода. ↩
-
Мы немного смешиваем исходный код и время выполнения здесь. Чтобы быть абсолютно ясным, переменные не существуют во время выполнения. (Скомпилированный) фрагмент может выполняться несколько раз (например, если он в цикле или в функции, вызываемой несколько раз). Для каждого выполнения переменные в исходном коде будут представлены разными адресами во время выполнения. ↩
-
Постоянство не является фундаментальным аспектом закрепления, это часть формулировки закрепления в Rust и гарантий безопасности вокруг него. Было бы нормально, чтобы закрепление было временным, если бы это могло быть безопасно выражено и временная область видимости закрепления могла быть relied upon потребителями гарантий закрепления. Однако это невозможно с Rust сегодня или с любым разумным расширением. ↩
-
Нет специального отношения к
Box(или другим std указателям) ни в реализации закрепления, ни в компиляторе.Boxиспользует небезопасные функции в APIPinдля реализацииBox::pin. Требования безопасностиPinудовлетворяются благодаря гарантиям безопасностиBox. ↩ -
Это strictly закрепление в стеке only в неасинхронных функциях. В асинхронной функции все локальные переменные размещаются в псевдо-стеке async, поэтому закрепляемое место, вероятно, хранится в куче как часть фьючерса, лежащего в основе асинхронной функции. ↩