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 の上に
TransactionAPI を実装したいと考えています。まず、
&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 には制約がありますが、 それでも抜け道を使う方法はあり、そのうえで そうした抜け道の使い方が 一貫していて安全であることを示す ことができます。