pin!マクロが使ったRustの仕様の抜け道
現在、Unstable Featureのなかに「pin_macro」なるものがあります。これは、まだ不安定機能であるcore::pin::pin!
マクロを有効化するfeatureです。
このpin!
マクロは、スタック領域に値を持つPin
を安全に生成できる、式として使用できるマクロです。しかし、私の記憶ではそのようなマクロは作れないと思っていたので、どういう仕組みになっているのか気になったので調べてみました、という記事です。
ちなみに、このpin!
マクロは、安定化のPull Requestがあるようなので、そう遠くない未来に安定化するのではと思います。
※ この記事のRustのバージョンは、1.67.0-nightlyです。
Pin
とは
Pin
が必要な理由
Pin
についての説明は、「RustのPinチョットワカル - OPTiM TECH BLOG」の記事が、非常に丁寧にわかりやすく説明してくれていますので、よくわからない場合はまずこちらを読んでいただきたいです。
RustにPin
が誕生した理由には、async/await、また、それに関わるトレイトFuture
やGenerator
が関わっています。
Rustでasync/awaitの仕組みを取り入れる際に、async関数やasyncブロック内で、内部の変数への参照を保持したまま安全にawaitできる、つまり、自己参照を持つ構造体を安全に使用できる仕組みが求められました。
let mut async_block = async {
let mut x = String::new();
// yはxの参照、つまり、async_block.xの参照であり、自己参照になる
let y = &mut x;
my_async_fn_inner1(y).await;
// awaitで処理が中断するため、この間でasync_blockがmoveするとxのアドレスが変わり、yは無効なメモリ空間への参照となる
my_async_fn_inner2(y).await;
};
そもそも、Rustのルール上、構造体が所有する参照は、その構造体より長く生きていなければなりません。そのため、自己参照は通常の参照の代わりに、Rustのライフタイムの縛りのない生ポインタを所有することになります。ですが、生ポインタの指すアドレスを参照することは、安全が保証されません。
自己参照の場合に具体的に何が危険かというと、とりあえず初期化前にアクセスされると危険というのは当然あるのですが、一番大きいのは構造体をムーブ出来ないという点です。構造体をムーブしたときに、その構造体への参照は無効なメモリ空間への参照になってしまいます。
つまり、自己参照の安全が保証される期間は、自己参照を作成してから構造体がムーブされるまでとなり、ある地点からムーブを禁止することができれば、それ以降作成された自己参照は常に有効となり、安全なコードとして扱えます。
Pin
構造体とUnpin
トレイトは、このムーブできない構造体を定義するために必要とされたものです。
Pin<P>
とUnpin
の役割
Unpin
は、それ自体には機能を持たない「マーカートレイト」です。通常Unpin
は、どんな構造体にも自動で実装されています。Unpin
を実装していない型が特殊です。Unpin
が実装されない型は、Rustが自動で定義する、自己参照を含む(可能性のある)型、例えばasync
ブロックやasync fn
、staticなgenerator等です。
Pin<P>
のメソッドには全てP: Deref
の条件が設定されています。Deref
は主に参照としての機能を持っている構造体に実装されています。そして、その参照先の型が<P as Deref>::Target
にあたるのですが、<P as Deref>::Target
がUnpin
を実装していない場合は、一度Pin<P>
が生成された後、<P as Deref>::Target
をムーブできないように設計されています。
つまり、Pin
は参照先のムーブを禁止する"ピン止め"を行い、Unpin
はピン止めされてもムーブできる機能を持っていると言えます。
Pin
を安全に生成する条件
Pin
がムーブを禁止する仕組みは、新しくRustにルールが追加されたわけではなく、Pin
構造体が、Unpin
を実装していない値を安全にムーブする手段を用意しないことで実現しています。
Unpin
を実装する型の値を持つPin
を安全に生成する条件
Target
にUnpin
を実装している場合、前述の通り、Pin
を生成したあとにムーブしても問題ありません。つまり、Pin
による特別な安全条件はありません。
生成と破棄
impl<P: Deref<Target: Unpin>> Pin<P> {
pub const fn new(pointer: P) -> Pin<P> {
// 実装
}
}
Unpin
を実装していれば、安全なメソッドで生成できます。
impl<P: Deref<Target: Unpin>> Pin<P> {
pub const fn into_inner(pin: Pin<P>) -> P {
// 実装
}
}
安全にPin
を捨てて中身を取り出せます。
&mut
(可変参照)
impl<P: DerefMut<Target: Unpin>> DerefMut for Pin<P> {
fn deref_mut(&mut self) -> &mut P::Target {
// 実装
}
}
P
がDerefMut
を実装していれば、Pin<P>
にもDerefMut
が実装されているので、安全に&mut
にアクセスできます。
これらは全てTarget: Unpin
が条件となっており、Unpin
を実装していない場合は使えません。
Unpin
を実装しない型の値をヒープ領域に持つPin
を安全に生成する条件
Target
にUnpin
を実装していない場合は、Pin
を生成した後にTarget
がムーブしないことを保証しないといけません。もう少し考えやすくするために条件を分割すると、unsafe
コードを使わない限り、以下の点が守られる必要があります。
-
Pin
がムーブしたときTarget
がムーブしないこと -
Pin
の生成後にTarget
(所有元)を直接ムーブできないこと -
Pin
の生成後に&mut Target
にアクセスできないこと
もし、&mut Target
にアクセスできる場合はcore::mem::swap
やcore::mem::replace
でムーブできてしまいます。もちろん、Pin
の生成後にTarget
にムーブ出来てもだめです。別の表現をすると、どこかに&mut Target
が存在すると仮定して、Rustの借用条件を満たすようになっていれば問題ないです。
unsafe
コードを使った場合、上記の点が保証されていないため、Target
がムーブされないことを自分で守らなければなりません。
生成と破棄
impl<T> Box<T> {
pub fn pin(x: T) -> Pin<Box<T>> {
// 実装
}
}
上記はPin<Box<T>>
の生成メソッドです。他にもPin<Rc<T>>
やPin<Arc<T>>
等があり、どれも安全なメソッドとなっています。つまり、1~3の条件が満たされる生成手段と言っています。本当にそうでしょうか。
まず、ヒープに値を持つスマートポインタは、スマートポインタがムーブしても、ヒープ上の実体のアドレスは変わりません。
つまり、1の条件は満たされていることになります。
また、Pin
がスマートポインタの参照先の所有権を持っているため、直接アクセスできません。Pin
のドロップされるとスマートポインタも、その参照先もドロップします。Pin
からスマートポインタを取り出すメソッドは、
impl<P: Deref> Pin<P> {
pub const unsafe fn into_inner_unchecked(pin: Pin<P>) -> P {
// 実装
}
}
のように、unsafe
メソッドしか用意されていません。つまり、2の条件は満たされていることになります。
&mut
(可変参照)
impl<P: DerefMut> Pin<P> {
pub fn as_mut(&mut self) -> Pin<&mut P::Target> {
// 実装
}
}
impl<'a, T: ?Sized> Pin<&'a mut T> {
pub const unsafe fn get_unchecked_mut(self) -> &'a mut T {
// 実装
}
}
Pin<&mut T>
を生成するまでは安全ですが、Pin<&mut T>
から&mut T
を取り出すメソッドは、unsafe
メソッドしか用意されていません。つまり、安全が保証された手段はなく、3の条件も満たされていることになります。
この&mut
を使用する安全でない処理は、だいたいRustのStandard Library内で行われます。また、自己責任で行う場合は、Target
をムーブさせないことだけ気をつければ安全とみなせます。
Unpin
を実装しない型の値をスタック領域に持つPin
を安全に生成する条件
基本的には、ヒープ領域に値を持っている場合と同じです。ただし、Pin
の生成に関して安全なメソッドは用意されていません。つまりPin
の生成時に、1~3の条件に違反する可能性が無いように気をつけなければなりません。
impl<P: Deref> Pin<P> {
pub const unsafe fn new_unchecked(pointer: P) -> Pin<P> {
// 実装
}
}
スタックに値を持つ場合、Pin<P>
に入れる型P
はだいたい&mut T
、もしくは&mut T
を内部に所有する型となります。というのも、Pin
が値を所有してしまうと、Pin
のムーブに連動して値がムーブしてしまうからです。
なので、とりあえずPin<&mut T>
を安全に生成する条件を考えます。
先の通り、Pin<&mut T>
がムーブしてもT
はムーブしません。つまり、1の条件は満たされます。また、&mut T
は複数生成できないため、Pin<&mut T>
が生存している間、Pin<&mut T>
を介さずに&mut T
にアクセスすることはできません。Pin<&mut T>
から&mut T
を取り出す安全なメソッドは用意されていないため、3の条件は満たされています。
また、Pin<&mut T>
が生存している時T
はムーブできません。ただし、Pin<&mut T>
がドロップした後、T
がムーブしない保証はないため、自己責任で守る必要があります。
fn dengerous_pin<T>(mut v: T) -> T {
{
// Pin生成
let mut pin = unsafe {
core::pin::Pin::new_unchecked(&mut v)
}
// Pinを使った処理
// Pin破棄
}
// 一度Pinで止めたvがムーブされるため、危険なコード
v
}
Pin
の生成時にこの点の安全が保証されれば、安全性が保証されることになり、だいぶ扱いやすくなります。この 「Pin<&mut T>
がドロップした後、T
がムーブしない」を保証するものがpin!
マクロというわけです。
pin_utilsクレートのpin_mut!
マクロ
pin!
マクロの前に、このpin_utilsクレートのpin_mut!
マクロについて触れたいと思います。
pin!
マクロと同様に、このpin_mut!
マクロもこの目的のために作られたマクロです。定義はおおよそ以下のようになっています。(わかりやすくするため、多少改変しています。)
macro_rules! pin_mut {
($($x:ident),* $(,)?) => { $(
let mut $x = $x;
let mut $x = unsafe {
core::pin::Pin::new_unchecked(&mut $x)
};
)* }
}
(実物)
このマクロを実際に展開すると以下のようになります。
let future = create_async();
// ↓pin_mut!(future);の展開後
let mut future = future;
let mut future = unsafe {
core::pin::Pin::new_unchecked(&mut future)
};
このマクロは、Pin
にわたす前の値を、Pin
でシャドーイングしています。そして、シャドーイングした値とPin
のスコープは同じため、Pin
のドロップ後にシャドーイングした値に再度アクセスする手段がなくなります。これによって、「Pin<&mut T>
がドロップした後、T
がムーブしない」を保証しています。
pin!
マクロ
前述のpin_utils::pin_mut!
マクロでも目的は達成できるのですが、pin_mut!
マクロは、let
で値を束縛する文を生成するマクロであり、式ではありません。理想を言えばlet x = pin_mut!(create_async())
や、pin_mut!(create_generator()).as_mut().resume(())
のように、一般的な関数のように使えたほうが便利なのですが、これが出来ないのです。
これを実現するためのものがpin!
マクロです。
// 使い方
async fn async_fn() {
// 右辺で使う
pin!(async {
// 処理
}).await;
// 束縛して使う
let mut pin = pin!(async {
// 処理
});
pin.as_mut().await
}
では、どのような定義でこの仕組みを実現しているでしょうか。
まず、シンプルに以下のような定義を考えてみます。
macro_rules! pin {
($value:expr) => {
unsafe {
core::pin::Pin::new_unchecked(&mut { $value })
}
};
}
これは、let
で束縛することに失敗します。
error[E0716]: temporary value dropped while borrowed
pin!
マクロに渡す値が、let
で&mut
を束縛してすぐにライフタイムが尽きてしまうので、エラーとなってしまいます。
というわけで、pin!
にわたす値を、&mut
と同スコープになるように定義したいのですが、
macro_rules! pin {
($value:expr) => {
let mut x = $value;
unsafe {
core::pin::Pin::new_unchecked(&mut x)
}
};
}
みたいな記述は、let mut pin = let mut x = async {};
のように展開されるため、文法的におかしくなります。
一時変数のライフタイム延長
この一見無理な条件を達成するために、pin!
マクロは、一時変数のライフタイム延長という仕様を使っています。(以前私の書いた、【Rust】変数のスコープまとめでも少し触れていることを宣伝しておきます。)
実際のpin!
マクロの定義は以下のようになっています。(わかりやすくするため、多少改変しています。)
#[allow_internal_unstable(unsafe_pin_internals)]
pub macro pin($value:expr $(,)?) {
core::pin::Pin::<&mut _> { pointer: &mut { $value } }
}
(実物)
マクロの書き方がmacro_rules!
では無いですが、これはまだ安定化していない宣言マクロの書き方(decl_macro feature)です。とりあえず今は無視します。macro_rules!
で書くと、以下のようになります。
#[allow_internal_unstable(unsafe_pin_internals)]
macro_rules! pin {
($value:expr $(,)?) => {
core::pin::Pin::<&mut _> { pointer: &mut { $value } }
};
}
このマクロで展開すると以下のようになります。
async fn async_fn() {
// 右辺で使う
core::pin::Pin::<&mut _> {
pointer: &mut { async {
// 処理
} },
}
.await;
// 束縛して使う
let mut pin = core::pin::Pin::<&mut _> {
pointer: &mut { async {
// 処理
} },
};
pin.as_mut().await
}
さて、ここでpin!
マクロに渡したasync {}
は、一見すぐドロップしてしまいそうですが、この場合、ライフタイムが延長されます。Rust Referenceには、一時変数のライフタイム延長に関して、以下のように記述してあります。
For a let statement with an initializer, an extending expression is an expression which is one of the following:
The initializer expression. The operand of an extending borrow expression. The operand(s) of an extending array, cast, braced struct, or tuple expression. The final expression of any extending block expression.
So the borrow expressions in
&mut 0
,(&1, &mut 2)
, andSome { 0: &mut 3 }
are all extending expressions. The borrows in&0 + &1
andSome(&mut 0)
are not: the latter is syntactically a function call expression.
ここでは、Pin
構造体の初期化式内でpointer
フィールドを、一時変数の&mut
で初期化しているため、一時変数のスコープはPin
構造体のスコープと同一となります。この効果で、pin_utilsのpin_mut!
と同じスコープを、式で実現することができるのです。
まとめると、new_unchecked(&mut $value)
のようなメソッドを通すと、その式の終わりでライフタイムが尽きてしまうが、Pin::<&mut _> { pointer: &mut { $value } }
のような初期化式を使うと、ライフタイムが延長されるので、Pin
と同スコープにできるということです。
allow_internal_unstable feature
しかし、よく考えるとPin
構造体のフィールドpointer
は外部から見えないようになっているはずです。もし見えていたら、こんなマクロを使わなくても、誰でも好きに初期化できるはずです。外部から見えていないのに、初期化式で初期化できるのはなぜでしょうか。
実際には、このpin!
マクロの追加に合わせてPin
構造体のpointer
フィールドがpub
に変更されています。
// 旧
pub struct Pin<P> {
pointer: P,
}
// 新
pub struct Pin<P> {
#[unstable(feature = "unsafe_pin_internals", issue = "none")]
#[doc(hidden)]
pub pointer: P,
}
ということは、外部にフィールドが直接公開されています。しかし、pub
になったのと同時に、属性が追加されているのがわかります。
#[unstable(feature = "unsafe_pin_internals", issue = "none")]
この属性は、Standard Library内でまだ安定化されていない機能に付けられる属性であり、stableバージョンのRustからはアクセスできません。nightlyバージョンでも
#![feature(allow_internal_unstable)]
のようにfeatureを明示して有効化しなければ、アクセスできません。そして、pin!
マクロの方にも、なにやら怪しげな属性がありました。
#[allow_internal_unstable(unsafe_pin_internals)]
このallow_internal_unstable featureは、このマクロ内に限り明示したfeatureを有効にすることができるfeatureです。つまり、unsafe_pin_internalsが通常有効化されていないため、pointer
フィールドにアクセスできないのですが、pin!
マクロ内ではallow_internal_unstable featureによって、unsafe_pin_internalsが有効化されているので、pointer
フィールドにアクセスできるという仕組みになっています。
もっといい代替案は?
この、allow_internal_unstableとunsafe_pin_internalsを使った仕組みは、あまり正攻法とはいえません。これは、pin!
マクロに関する議論でも上がっていますし、ソースのコメントでも以下のように残っています。
pub struct Pin<P> { // FIXME(#93176): this field is made `#[unstable] #[doc(hidden)] pub` to: // - deter downstream users from accessing it (which would be unsound!), // - let the `pin!` macro access it (such a macro requires using struct // literal syntax in order to benefit from lifetime extension). // Long-term, `unsafe` fields or macro hygiene are expected to offer more robust alternatives. #[unstable(feature = "unsafe_pin_internals", issue = "none")] #[doc(hidden)] pub pointer: P, }
// Long-term,
unsafe
fields or macro hygiene are expected to offer more robust alternatives.
将来的には、unstable featureを使用せずにpointer
フィールドを見せないようにすることが期待されているようです。("unsafe fields"に関してはよくわからないですが、"macro hygiene"に関しては、前述のdecl_macroのことと思います)
私の印象としては、ライフタイムの延長を使っているのも、だいぶ仕様の隙間を突いているなぁというところです。とはいえ、このpin!
マクロは便利なので、早く安定化してくれるとありがたいと思っています。
参考リンク
Discussion