История: До стабилизации PhantomData в Rust 1.0 разработчики сталкивались с трудностями в выражении типовых отношений для структур, которые концептуально обладали обобщенными данными, но хранили только сырые указатели, например, при обертывании дескрипторов библиотек C. Компилятор полагался исключительно на конкретные поля для вывода вариативности и владения, что приводило к слишком строгим ошибкам о времени жизни или к незаметным нарушениям безопасности памяти, когда borrow checker предполагал, что тип не связан с его содержимым. PhantomData был введен как нулевой маркер, чтобы явно сообщать о вариативности, владении и особенностях трейт без затрат времени выполнения.
Проблема: Рассмотрим пользовательский умный указатель struct RawBox<T> { ptr: *const T }. Хотя *const T является ковариантным по отношению к T, компилятор не имеет явного подтверждения того, что RawBox логически владеет значением T, особенно в отношении проверки уничтожения (drop check). Без PhantomData компилятор рассматривает T как чисто синтетический параметр типа, который структура просто упоминает, но не владеет, что потенциально позволяет уничтожить T, в то время как структура все еще хранит сырой указатель на его память. Это упущение также мешает структуре правильно реализовывать авто-трейты, такие как Send и Sync, на основе свойств T.
Решение: Добавив поле PhantomData<T>, вы явно указываете, что RawBox является ковариантным по отношению к T и говорите о логическом владении. Это гарантирует, что компилятор обеспечивает, чтобы T жил дольше структуры и применяет правильные правила вариативности для подтипов. В случаях, требующих другой вариативности, PhantomData принимает различные конструкты типов: PhantomData<fn(T)> создает контравариантность, тогда как PhantomData<*mut T> или PhantomData<Cell<T>> налагают инвариантность. Этот механизм позволяет безопасно абстрагироваться от сырых указателей, сохраняя нулевые гарантии затрат Rust.
При разработке библиотеки высокопроизводительной обработки аудио мне нужно было обернуть дескриптор API C *mut AudioContext, который фактически был типизирован как структура Rust AudioBuffer<T>, где T мог быть f32 или i16. Обертка AudioHandle<T> хранила только сырой указатель и указатель на таблицу виртуальных функций, но мне нужно было, чтобы она вела себя как Box<AudioBuffer<T>> в отношении сроков жизни и безопасности потоков. В частности, дескриптор должен был быть Send, когда T был Send, и ковариантным по отношению к T, чтобы позволить бесшовную замену типов аудиоданных.
Первый подход заключался в том, чтобы не добавлять никаких маркеров и полагаться исключительно на поле *mut c_void. Эта стратегия поддерживала минимальный размер структуры и избегала рутины, что было ее основным преимуществом. Однако компилятор предполагал, что AudioHandle<T> был инвариантным по отношению к T и отказывался реализовывать Send, даже когда T был Send, поскольку не мог проверить владение, в конечном итоге нарушая контракт API, требующий перемещения дескрипторов между потоками.
Второй подход рассматривал возможность хранения Option<Box<T> исключительно для того, чтобы направить систему типов. Этот метод правильно устанавливал вариацию и выводы Send/Sync, решая проблемы реализации трейта. К сожалению, это удваивало размер структуры и вводило сложную логику уничтожения, рискуя вызывать панику, если имитационное поле не было правильно синхронизировано с указателем C, что противоречило цели нулевой абстракции.
Выбранным решением было добавление marker: PhantomData<AudioBuffer<T>> в структуру. Этот нулевой маркер мгновенно предоставил ковариантную семантику по отношению к T, позволил правильно выводить авто-трейты на основе T и гарантировал, что проверка уничтожения подтверждала, что AudioBuffer<T> не был уничтожен до дескриптора. В конечном итоге, обертка FFI скомпилировалась без ошибок, не налагала никаких накладных расходов во время выполнения и безопасно позволяла перемещение аудиодескрипторов между потоками, когда T был Send, идеально удовлетворяя требованиям библиотеки.
Почему именно PhantomData<T> запускает правило проверки уничтожения (dropck), которое предотвращает уничтожение значения, пока ссылочные данные все еще живы, и какая небезопасность возникла бы без него?
Без PhantomData<T> компилятор предполагает, что структура не владеет T, позволяя пользовательскому коду уничтожить T, пока реализация Drop структуры все еще хранит сырой указатель на память T. Это приводит к использованию после освобождения, когда деструктор выполняется, поскольку память могла быть перераспределена или отравлена. PhantomData сигнализирует dropck, что структура концептуально содержит T, заставляя компилятор проверять, что T строго живет дольше структуры и предотвращает эту небезопасность, даже если T занимает ноль байт в компоновке.
Как можно использовать PhantomData для принуждения контравариантности по отношению к параметру типа, и в каком типе проектирования API это необходимо?
Контравариантность достигается с помощью PhantomData<fn(T)>. Это необходимо для типов хранения обратных вызовов, таких как struct Comparator<T> { compare: fn(T, T) -> Ordering, _marker: PhantomData<fn(T)> }. Поскольку fn(T) является контравариантным по отношению к T, структура правильно моделирует, что компаратор, принимающий &'static str, может быть использован везде, где ожидается компаратор &'short str, что является противоположным отношением к ковариантности и критически важно для подтипирования указателей функций.
Какова различия между вариативными последствиями PhantomData<Cell<T>> и PhantomData<T>, и почему может потребоваться первая структура, обертывающая небезопасный примитив внутренней изменяемости?
PhantomData<T> подразумевает ковариантность, тогда как PhantomData<Cell<T>> подразумевает инвариантность, потому что Cell инвариантен по отношению к своему содержимому. При построении пользовательского контейнера на основе UnsafeCell, такого как MyRefCell<T>, инвариантность обязательна для предотвращения приведения MyRefCell<&'long str> к MyRefCell<&'short str>. Такое приведение позволило бы хранить ссылку с коротким сроком жизни, где ожидалась ссылка с длинным сроком жизни, нарушая правила алиасинга и вызывая висячие указатели при операциях записи, что предотвращает инвариантный маркер.