PhantomData 3/4: времена жизни для внешних ресурсов
Инварианты внешних ресурсов часто совпадают с тем, что можно выразить правилами времён жизни.
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0
// use std::marker::PhantomData;
/// Прямой FFI к библиотеке базы данных на C.
/// Мы получили этот 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() {}
-
Вспомните API транзакций из примера Aliasing XOR Mutability.
Внутри типа транзакции мы хранили изменяемую ссылку на подключение к базе данных, чтобы заблокировать базу, пока транзакция активна.
В этом примере мы хотим реализовать API
Transactionповерх внешнего API, который написан не на Rust.Начнём с определения типа
Transaction, который хранит&mut DatabaseConnection. -
Спросите: каковы ограничения этой реализации? Считайте, что
u8реализован корректно и что у нас достаточно информации, чтобы использовать внешний API.Ожидаемые ответы:
- Косвенность занимает на 7 байт больше, чем нужно, на 64-битной платформе, а также требует разыменования указателя во время выполнения.
-
Проблема: мы хотим, чтобы транзакция заимствовала подключение к базе данных, которое её создало, но не хотим, чтобы объект
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) и в её реализации показана именно такая работа. Хотя анализатор заимствований строг, всё же есть способы воспользоваться «запасными выходами» и затем показать, что способы их использования непротиворечивы и безопасны.