📌

pin!マクロが使ったRustの仕様の抜け道

2022/12/03に公開約13,200字

現在、Unstable Featureのなかに「pin_macro」なるものがあります。これは、まだ不安定機能であるcore::pin::pin!マクロを有効化するfeatureです。

https://doc.rust-lang.org/core/pin/macro.pin.html

このpin!マクロは、スタック領域に値を持つPinを安全に生成できる、式として使用できるマクロです。しかし、私の記憶ではそのようなマクロは作れないと思っていたので、どういう仕組みになっているのか気になったので調べてみました、という記事です。

ちなみに、このpin!マクロは、安定化のPull Requestがあるようなので、そう遠くない未来に安定化するのではと思います。

※ この記事のRustのバージョンは、1.67.0-nightlyです。

Pinとは

Pinが必要な理由

Pinについての説明は、「RustのPinチョットワカル - OPTiM TECH BLOG」の記事が、非常に丁寧にわかりやすく説明してくれていますので、よくわからない場合はまずこちらを読んでいただきたいです。

https://tech-blog.optim.co.jp/entry/2020/03/05/160000

RustにPinが誕生した理由には、async/await、また、それに関わるトレイトFutureGeneratorが関わっています。

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>::TargetUnpinを実装していない場合は、一度Pin<P>が生成された後、<P as Deref>::Targetをムーブできないように設計されています。
つまり、Pinは参照先のムーブを禁止する"ピン止め"を行い、Unpinはピン止めされてもムーブできる機能を持っていると言えます。

Pinを安全に生成する条件

Pinがムーブを禁止する仕組みは、新しくRustにルールが追加されたわけではなく、Pin構造体が、Unpinを実装していない値を安全にムーブする手段を用意しないことで実現しています。

Unpinを実装する型の値を持つPinを安全に生成する条件

TargetUnpinを実装している場合、前述の通り、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 {
        // 実装
    }
}

PDerefMutを実装していれば、Pin<P>にもDerefMutが実装されているので、安全に&mutにアクセスできます。

これらは全てTarget: Unpinが条件となっており、Unpinを実装していない場合は使えません。

Unpinを実装しない型の値をヒープ領域に持つPinを安全に生成する条件

TargetUnpinを実装していない場合は、Pinを生成した後にTargetがムーブしないことを保証しないといけません。もう少し考えやすくするために条件を分割すると、unsafeコードを使わない限り、以下の点が守られる必要があります。

  1. PinがムーブしたときTargetがムーブしないこと
  2. Pinの生成後にTarget(所有元)を直接ムーブできないこと
  3. Pinの生成後に&mut Targetにアクセスできないこと

もし、&mut Targetにアクセスできる場合はcore::mem::swapcore::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!マクロについて触れたいと思います。

https://crates.io/crates/pin-utils

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), and Some { 0: &mut 3 } are all extending expressions. The borrows in &0 + &1 and Some(&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!マクロは便利なので、早く安定化してくれるとありがたいと思っています。

参考リンク

https://doc.rust-lang.org/core/pin/macro.pin.html
https://crates.io/crates/pin-utils
https://tech-blog.optim.co.jp/entry/2020/03/05/160000
https://doc.rust-lang.org/reference/destructors.html

Discussion

ログインするとコメントできます