PhantomData 3/4: 外部リソースのライフタイム

外部リソースの不変条件は、多くの場合、ライフタイム規則で表現できることと一致します。

// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0

// use std::marker::PhantomData;

/// C のデータベースライブラリへの直接的な FFI。
/// この API はそのまま提供されたものであり、こちらからは変更できません。
mod ffi {
    pub type DatabaseHandle = u8; // 同時に開けるデータベースは最大 255 個

    fn database_open(name: *const std::os::raw::c_char) -> DatabaseHandle {
        unimplemented!()
    }
    // ... など。
}

struct DatabaseConnection(ffi::DatabaseHandle);

struct Transaction<'a>(&'a mut DatabaseConnection);

impl DatabaseConnection {
    fn new_transaction(&mut self) -> Transaction<'_> {
        Transaction(self)
    }
}

fn main() {}
  • Aliasing XOR Mutability の例にあった トランザクション API を思い出してください。

    トランザクションがアクティブな間データベースをロックするため、 トランザクション型の中でデータベース接続への可変参照を保持していました。

    この例では、外部の非 Rust API の上に Transaction API を実装したいと考えています。

    まず、&mut DatabaseConnection を保持する Transaction 型を定義するところから始めます。

  • 質問: この実装の限界は何でしょうか。u8 は実装上正確であり、 外部 API を使うための情報として十分であると仮定してください。

    想定:

    • 64 ビットプラットフォームでは、間接参照のために必要以上に 7 バイト多く使い、 実行時にはポインタのデリファレンスのコストもかかります。
  • 問題: トランザクションには、それを作成したデータベース接続を借用させたい一方で、 Transaction オブジェクトには実際の参照を格納したくありません。

  • 質問: ライフタイムパラメータを残したまま、 Transaction から可変参照を取り除くとどうなるでしょうか。

    想定: 未使用のライフタイムパラメータになります。

  • 前のスライドの型タグ付けと同様に、この未使用のライフタイムパラメータを 表すために PhantomData を使うことができます。

    違いは、このライフタイムを別の型と一緒に使う必要があることですが、 その別の型自体はそれほど重要ではないという点です。

  • 実演: Transaction を次のように変更します。

    #![allow(unused)]
    fn main() {
    // Copyright 2025 Google LLC
    // SPDX-License-Identifier: Apache-2.0
    
    struct Transaction<'a> {
        connection: DatabaseConnection,
        _phantom: PhantomData<&'a mut DatabaseConnection>,
    }
    }

    DatabaseConnection::new_transaction() メソッドを更新します。

    #![allow(unused)]
    fn main() {
    // Copyright 2025 Google LLC
    // SPDX-License-Identifier: Apache-2.0
    
    impl DatabaseConnection {
        fn new_transaction<'a>(&'a mut self) -> Transaction<'a> {
            Transaction { connection: DatabaseConnection(self.0), _phantom: PhantomData }
        }
    }
    }

    これにより、それを作成した DatabaseConnection に結び付けられた 所有されたデータベース接続を得られますが、参照を格納する版よりも 実行時のメモリフットプリントを小さくできます。

    PhantomData はゼロサイズ型(()struct MyZeroSizedType; のようなもの)なので、 Transaction のサイズは теперь u8 と同じになります。

    代わりに参照を保持していた実装は、usize と同じ大きさでした。

さらに探る

  • 型と値の間の関係をこのようにエンコードする方法は、unsafe と組み合わせると 非常に強力です。というのも、ライフタイムを操作する方法がほとんど任意になるからです。 これは危険でもありますが、外部の機械検証済み証明のようなツールと組み合わせることで、 関連するデータ型にライフタイムと安全性に関する期待をエンコードしつつ、 循環/自己参照型を安全にエンコードできます。

  • GhostCell (2021) の論文と、 その関連実装は、 この種の取り組みを示しています。borrow checker には制約がありますが、 それでも抜け道を使う方法はあり、そのうえで そうした抜け道の使い方が 一貫していて安全であることを示す ことができます。