Зачем обёртке над FFI-указателем нужен PhantomData, если он не хранит дополнительных данных?
PhantomData сообщает компилятору о логической связи между обёрткой и типом, временем жизни или владением, которых нет среди её фактических полей. Для FFI-указателя это позволяет выразить, например, что объект представляет заимствованный буфер и не должен пережить источник.
PhantomData не проверяет адрес, выравнивание, инициализацию или фактическое время жизни памяти. Он только участвует в проверке заимствований, определении дисперсии, анализе авто-трейтов и правилах уничтожения; исходные инварианты всё равно должен доказать unsafe-код.
Raw pointers намеренно почти не несут семантики владения: компилятор не знает, кто отвечает за память, как долго она существует и можно ли безопасно получить из указателя ссылку. Это необходимо для низкоуровневого кода и FFI, но без дополнительной информации типобезопасная обёртка не может выразить часть своих контрактов.
PhantomData решает проблему «логического поля»: тип получает сведения о связи с другим типом или временем жизни без добавления данных в память и изменения ABI. Такой маркер позволяет использовать систему типов для документирования и частичной проверки контракта, сохраняя нулевую стоимость представления.
Рассмотрим Rust-обёртку над указателем на буфер, которым владеет вызывающая сторона или библиотека C. Если обёртка содержит только *const T, компилятор не видит, что её существование должно быть связано с временем жизни буфера.
В результате разработчик может случайно вернуть такую обёртку после окончания заимствования, получить из неё ссылку на уже освобождённую память или ошибочно трактовать её как владеющий объект. Последствия включают use-after-free, чтение неинициализированной памяти и неопределённое поведение, хотя сам raw pointer продолжает хранить числовой адрес.
Маркер PhantomData<&'a [T]> сообщает: значение логически заимствует срез T на время жизни 'a. Поэтому безопасный интерфейс обёртки не сможет выдать её за пределами 'a, а компилятор будет учитывать это заимствование при проверке владельца исходного буфера.
Безопасность конструктора здесь основана на внешнем контракте: указатель должен быть корректным для чтения len элементов, элементы должны быть инициализированы, память должна иметь подходящее выравнивание, а буфер обязан оставаться живым в течение 'a. PhantomData не доказывает ни одно из этих условий и не препятствует C-коду освободить буфер раньше времени.
Выбор параметра PhantomData зависит от семантики обёртки. PhantomData<&'a T> моделирует заимствование, PhantomData<T> — логическое владение значением, а варианты на основе fn(T) или fn() -> T позволяют отдельно влиять на дисперсию. Маркер также может повлиять на Send, Sync и анализ уничтожения, поэтому его нельзя выбирать только по принципу «размер всё равно нулевой».
Для владеющего FFI-дескриптора обычно моделируют владение ресурсом и реализуют освобождение в Drop, если библиотека предоставляет соответствующий deallocation API. Для невладеющего указателя используют маркер, отражающий заимствование или отсутствие владения; освобождать память в Drop в таком случае нельзя.
Главное ограничение — маркер не меняет ABI и не создаёт runtime-проверок. Если обёртку можно создать из произвольного указателя, unsafe-конструктор должен явно зафиксировать все требования к указателю; безопасные методы могут полагаться только на эти требования и на ограничения времени жизни, выраженные типом.
C-библиотека возвращает указатель на внутренний массив и обещает сохранять его действительным, пока живёт переданный ей контекст. Нужно предоставить Rust-коду представление для чтения массива, не передавая владение буфером.
Вариант без PhantomData прост: хранить указатель и длину. Его плюс — минимальная форма типа, но компилятор не связывает представление с контекстом, поэтому безопасный API легко допускает использование после уничтожения контекста.
Вариант с PhantomData<&'a [T]> выражает заимствование контекста через время жизни. Он не защищает от нарушения обещаний самой C-библиотеки, зато не позволяет безопасному Rust-коду продлить представление за пределы известного заимствования. Цена — необходимость правильно спроектировать lifetime-параметр и сохранить контракт C в документации.
Вариант с владеющим маркером и Drop был бы ошибочен: Rust начал бы считать обёртку владельцем и мог бы освободить память, которой фактически управляет C. Поэтому выбирается невладеющая обёртка с PhantomData заимствованного среза, а создание выполняется через unsafe-конструктор после проверки контракта библиотеки.
Вопрос 1. Может ли PhantomData сделать некорректный raw pointer безопасным?
Нет. Он не проверяет ненулевость, выравнивание, принадлежность объекту, инициализацию, границы диапазона или отсутствие конкурирующего доступа. Эти условия должны быть доказаны перед созданием ссылки, среза или выполнением чтения и записи; маркер лишь позволяет компилятору проверить выраженные типом связи и время жизни.
Вопрос 2. Чем отличается PhantomData<&'a T> от PhantomData<T> в FFI-обёртке?
Первый вариант моделирует заимствованный ресурс: обёртка логически зависит от 'a и не должна пережить источник. Второй моделирует логическое владение T, что влияет на правила уничтожения и авто-трейты и может означать ответственность за ресурс.
Поэтому для указателя на чужой буфер нельзя автоматически выбирать PhantomData<T>: такой выбор может неверно сообщить компилятору и читателю API, что обёртка владеет объектом. Маркер должен соответствовать реальному контракту, а не только типу указателя.
Вопрос 3. Гарантирует ли lifetime в PhantomData действительное время жизни памяти, управляемой C?
Нет. Lifetime — это статическая модель, которую Rust-код обязан поддерживать; C-библиотека не становится автоматически подчинённой правилам заимствований Rust. Если C освобождает или перемещает буфер вопреки обещанному контракту, маркер не обнаружит нарушение во время выполнения.
Поэтому безопасная оболочка должна не только добавить PhantomData, но и организовать протокол владения: удерживать контекст, запретить освобождение ресурса до уничтожения представлений либо оставить конструктор и операции unsafe. Если такой протокол нельзя выразить и гарантировать, безопасный API должен ограничить область действия представления или сохранить unsafe на границе.