HN 日本語サマリー

← 一覧へ戻る
プログラミング

Beyond the &

Beyond the & (lwn.net)

83 pointsby PaulHoule9 コメント

要約

Rustの言語チームは、ユーザー定義のスマートポインタを組み込み参照と同等に柔軟にするための「Beyond the &」プロジェクトを進めています。このプロジェクトは、コンパイラの内部概念である「place」をユーザーコードに公開することで、ポインタの操作性を向上させ、学習曲線上の複雑さを軽減することを目指しています。

全文翻訳

Beyond the &

Rustには、標準ライブラリ内およびユーザー定義のものを含め、数多くの種類のスマートポインタが存在します。しかし、組み込み参照で可能な一部の操作は、ユーザー定義のスマートポインタでは実行できません。Rustプロジェクトの言語チームのリーダーであるTyler Mandry氏は、スマートポインタを組み込み参照と同等に柔軟にするための長年の取り組みについて、RustConf 2026で講演しました。

Mandry氏によると、この問題に取り組むプロジェクト「Beyond the &」は、2026年を通じて、言語チームの他の関心メンバーと共に進められてきました。多くの検討を経て、彼らが到達した設計は、ポインタと参照の仕組みに関するメンタルモデルを大幅に簡素化できるはずです。この設計が解決する問題を説明するために、彼は動的なコンテンツを計算し、それをハッシュマップにキャッシュする簡略化されたRustプログラムの例を提示しました。この例では、RustのマップエントリーAPIを使用しており、エントリーが存在しない場合にコールバックを受け取ってハッシュマップのエントリーを生成するor_insert_with()関数が提供されています。

    struct RenderState {
        cache: HashMap<String, Arc<String>>,
        template: String,
    }
    
    fn render_page(state: &mut RenderState, name: &str) -> Arc<String> {
        // Entry API を使用して `state.cache` を可変借用します。
        let entry = state.cache.entry(name.to_string());
        entry.or_insert_with(|| {
            // エントリーが存在しない場合は作成します。
            Arc::new(state.template.replace("$name", name))
        }).clone()
    }

このコードは、ハッシュマップ内でキャッシュキー(name)を検索し、キャッシュされた値のコピーを返すか、存在しない場合はnameをテンプレートに代入して計算します。共有状態は、組み込みの可変参照&mut RenderStateを使用してアクセスされます。この例は機能しますが、スレッド間で安全に共有することはできません。キャッシュを共有可能にするためにミューテックスを追加すると、ボローチェッカーがプログラムを拒否します。

    fn render_page(state: &Mutex<RenderState>, name: &str) -> Arc<String> {
        let state: MutexGuard <' _ , RenderState> = state.lock().unwrap();

        // ボローチェッカーはここで "state" が可変借用されていると不平を言います:
        let entry = state.cache.entry(name.to_string());
        entry.or_insert_with(|| {
            // ... しかし、まだ借用されている間にここで使用されます:
            Arc::new(state.template.replace("$name", name))
        }).clone()
    }

元のコードが機能したのは、ボローチェッカーがstate.cacheとstate.templateが別々のフィールドであることを追跡できたため、それらに同時にアクセスしても安全だと判断できたからだとMandry氏は述べました。ミューテックスを追加すると、ボローチェッカーは単にMutexGuard構造体への不透明なアクセスとしてしか見なせず、アクセスが互いに独立していることを認識できなくなります。これは、コールバックが状態を読み取っている間に可変借用がそれを使用する可能性があり、潜在的に無効なデータ競合を引き起こすケースを考慮しなければならないためです。このケースでの簡単な修正は、MutexGuardを一度逆参照し、その背後にある構造体を再借用することです。

        let state: &mut RenderState = &mut *state.lock().unwrap();

これにより、元のコードが使用したような新しい組み込み参照が構築され、ボローチェッカーは再びアクセスが互いに干渉しないことを認識できるようになります。これは機能しますが、直感的ではありません。Mandry氏は、このような複雑さがRustの学習曲線が難しい一因となっていると述べました。ユーザー定義のポインタ型(MutexGuardなど)が組み込み参照のように振る舞う方が良いでしょう。

[画像: [Tyler Mandry]]

ポインタ型はどこにでもあると氏は続けました。そして、それらはすべてわずかに異なるセマンティクスを持っています。一部のポインタは安全に逆参照できない(生のポインタ)、あるいは書き込みはできるが読み込みはできない(MaybeUninit)などです。これはRustの汎用性を示していますが、多くの可能なポインタ型に対応できる設計を考案することを困難にしています。

言語チームが採用した解決策は、Nadrieril氏とBenno Lossin氏の作業に基づき、コンパイラの内部的な「place」という概念をユーザーコードに公開することです。「place」は、C言語の左辺値(lvalues)に相当するRustの概念です。これは、値の読み書きが可能な場所です。placeとポインタの違いは、placeがコンパイル時に存在する抽象的な式(例: state.cache)であるのに対し、ポインタは実行時にplaceを表す方法の一つであることです。

アイデアは、各スマートポインタに対して新しいハンドル型を作成することです。多くの場合、ハンドルは単に同じ場所への安全でないポインタのラッパーになります。次に、コンパイラはプログラムが参照するplaceを表すハンドルを自動的に作成します。これにより、ユーザーコードは、既存のDerefおよびDerefMutトレイトよりも高い柔軟性で、ボローチェッカーがハンドルとどのようにやり取りするかを制御するハンドル型にトレイトを実装できるようになります。Mandry氏は、ライブラリ作成者がボローチェッカーにNonNullポインタ経由でアクセスされるplaceへの書き込みをどのように処理するかを教える方法を示す例を提示しました。

    unsafe impl<T> WritePlace for NonNullHandle<T> {
        const SAFE: bool = false;
        unsafe fn write_place(self, value: T) {
            unsafe { self.0.write(value) }
        }
    }

WritePlaceトレイトは、特定の種類のハンドル(この場合はNonNullポインタを参照するハンドル)がどのように書き込み可能であるかをボローチェッカーに伝えるために使用されます。SAFEがfalseに設定されている場合、ボローチェッカーは関連するplaceへの書き込みを安全でない操作として扱います。実際の書き込みは、NonNullがラップしている生のポインタに転送されます。トレイトの実装全体がunsafeであるのは、トレイトの実装が不正確だと、ボローチェッカーが誤りを犯し、健全でない動作につながる可能性があるためです。

対応するReadPlaceトレイトは、ハンドルからの読み取り方法をエンコードします。より興味深いのはProjectPlaceとBorrowPlaceです。前者は、構造体を含むplaceをそのフィールドのいずれかを含むplaceに変換する方法をボローチェッカーに伝えます。例えば、state.templateと記述したときにMutexGuard<RenderState>のハンドルをMutexGuard<String>に変換する方法です。後者は、与えられたplaceを借用する新しいスマートポインタを作成する方法をボローチェッカーに伝えます。これは、&が組み込み参照に対して行うのと同じ方法です。

言語チームは、新しいスマートポインタを作成するための構文についてまだ議論中です。BorrowPlaceを使用する場合、&記号を使用することも可能ですが、それは混乱を招き、型推論の使用を困難にする可能性があります。一つの提案は@記号を使用することですが、それも完全に満足のいくものではありません。最終的に決定される構文が何であれ、BorrowPlaceトレイトは、ボローチェッカーがポインタ型を安全に処理するために必要なすべての情報をエンコードします。これは、現在組み込みの動作も同じメカニズムで定義できることを意味します。例えば、組み込み参照型&Tの実装は次のようになります。

    unsafe impl<'a, T> BorrowPlace<&'a T> for RefHandle<'a, T> {
        // ボローチェッカーに、同じplaceへの複数の参照を同時に作成できることを伝えます:
        const ACCESS: AccessKind = AccessKind::Shared;
        // そして、その借用はライフタイム 'a の間続くことを伝えます:
        type Timing = Lifetime<'a>;
        // そして、参照の作成は安全な操作であることを伝えます:
        const SAFE: bool = true;

        unsafe fn borrow(self) -> &'a T {
            unsafe { &*self.ptr.as_ptr() }
        }
    }

これにより、ユーザーは参照のように動作する独自のスマートポインタを作成する方法の例を得ることができ、標準ライブラリのメンテナーは既存の直感的でない組み込み動作を文書化する場所を得ることができます。MutexGuardのハンドル型に対する同様の実装により、前述のrender_page()例はエラーなしで機能します。Mandry氏はこれを「この例にとっては小さなコード変更ですが、意味論的には大きな転換です」と呼びました。

スマートポインタをコンパイラの既存の内部構造にさらに密接に結びつけることには、他の利点もあります。現在、参照の背後にある値に対してパターンマッチングを使用することは可能ですが、スマートポインタの背後にある値に対してはできません。少なくとも、ポインタを逆参照してその背後にある値を再借用しない限りは。BorrowPlaceによって公開される詳細は、コンパイラがこれらのパターンマッチングを安全に実装するのに十分です。

Mandry氏は、言語チームが新しい機能に求める基準の一つは、コンポーザビリティ、つまり、候補となる機能が既存の言語構造にどれだけうまく統合され、その機能の複数の使用が互いにどのように組み合わされるかであると述べました。ハンドルとプレースは、「美しくコンポーズ」します。なぜなら、それはコンパイラがすでに言語を理解し処理している方法のいくつかの詳細を公開するだけだからです。

それにもかかわらず、プレースとハンドルをライブラリコードに公開することはまだプロトタイプ段階です。Mandry氏は、この設計がすべての人のユースケースで機能することを確認するために協力を求めました。彼は聴衆にデザインドキュメントを見て、奇妙なセマンティクスを持つスマートポインタの例を追加するように求めました。「もし言語にさらに深く統合したい抽象化があれば、これを試して、障害があれば教えてください。」

次に、エラーと診断メッセージを具体化する予定です。理想的には、ユーザーは新しく追加されたトレイトに言及するエラーを見るべきではありません。それらは内部的なままで、エラーメッセージは組み込み参照の場合と同様に問題を直接説明するはずです。安定した言語機能になるまでにはまだ多くの作業がありますが、Mandry氏はこの設計に楽観的です。「私の希望は、ライブラリがRustをさらに強力で使いやすくすることを可能にするということです。」

聴衆の一人が、この設計が破壊的パターンマッチング(値の所有権を取得しながら可能なパターンに対してマッチングを行い、その構成要素に分解する複合操作)もサポートするかどうかを知りたいと尋ねました。Mandry氏は、開発者が適切なハンドルに対してVariantPlaceトレイトを実装している限り、サポートすると述べました。ハンドル型に対するすべての操作をサポートするには6〜8の操作を実装する必要があり、それもその一つだと彼は言いました。

別の聴衆は、この設計が一般的な列挙型にも適用できるかどうかを尋ねました。Mandry氏は一時停止し、しばらく宙を見つめ、それからためらいがちに長い「はい」と答えました。一般的な列挙型からフィールドをプロジェクションすることは、おそらく不可能だろうと彼は付け加えましたが、その方向での探索的な作業がいくつか行われています。例えば、Optionの内部からフィールドをプロジェクションして、フィールド値のOptionを取得することは可能でしょうか?「答えは分かりませんが、興味深い質問です」とMandry氏はセッションの時間が終わる直前に述べました。

[この記事の執筆にあたり、RustConfへの渡航を支援してくれたLinux Foundation、LWNのトラベルスポンサーに感謝します。]

この記事のインデックスエントリ

Conference

RustConf/2026