При извлечении из FFI-памяти значения Rust-типа с Drop через ptr::read какой инвариант владения нужно сохранить?
Нужно доказать, что после ptr::read исходное место больше не будет рассматриваться как владеющее извлечённым значением. ptr::read побитово перемещает значение в новый объект, не оставляя в исходной памяти корректно уничтожаемого экземпляра; повторное уничтожение исходного и нового значения может привести к двойному освобождению или другому неопределённому поведению.
Низкоуровневому коду Rust нужны операции, которые работают с памятью без обычного перемещения через безопасные ссылки. ptr::read решает задачу извлечения значения из адреса, когда компилятор не может выразить расположение объекта в безопасной системе типов, например при работе с неинициализированной памятью, вручную управляемыми буферами или FFI.
Цена такой гибкости — ответственность за владение, инициализированность, корректное уничтожение и дальнейшее использование исходной памяти. Сам raw pointer не сообщает компилятору, кто владеет значением и кто обязан вызвать его деструктор.
Значение типа с Drop может владеть ресурсом: памятью, дескриптором, сокетом или другим объектом, который освобождается при уничтожении. При ptr::read Rust создаёт отдельное значение побитовой копией содержимого, фактически передавая владение новому месту без автоматического обнуления или пометки старого места.
Если исходный буфер впоследствии будет освобождён как содержащий прежний Rust-объект, его деструктор может повторно освободить тот же ресурс. Обратная ошибка тоже опасна: если новый объект забыть уничтожить, ресурс утечёт.
Перед ptr::read нужно установить следующие факты:
ptr::read не вызывает Drop для исходного места и не создаёт безопасную ссылку. Семантически это перемещение значения из памяти, но физически исходные байты обычно остаются там. Поэтому после операции нельзя считать исходный объект пригодным для обычного использования или повторного уничтожения.
Минимальный пример показывает ключевой риск:
Здесь Resource содержит Box, поэтому два вызова деструктора для одинакового побитового содержимого могут освободить одну аллокацию дважды. В реальном коде исходное место обычно оформляют как память, не содержащую значения, например через MaybeUninit, либо передают владение буфером в явный протокол, где его освобождает только одна сторона.
Для типов Copy проблема двойного уничтожения отсутствует, но требования к валидности адреса, выравниванию и инициализированности всё равно сохраняются. Если память может быть невыровненной, используют специальную операцию чтения без выравнивания, но она не отменяет инварианты владения и корректности самого значения.
При FFI особенно важно не путать побитовое представление Rust-типа с допустимым ABI. Нельзя извлекать таким способом произвольную C-структуру и объявлять её Rust-значением с Drop, если контракт не гарантирует совместимое представление, инициализацию каждого поля и правила владения всеми ресурсами.
C-библиотека возвращает указатель на область памяти, где Rust-код разместил объект-обёртку над дескриптором. Разработчик вызывает ptr::read, получает объект по значению, а затем освобождает исходный буфер обычным Rust-способом. В результате деструктор исходного объекта повторно закрывает или освобождает уже переданный дескриптор.
Рассматривались три варианта. Оставить ptr::read и продолжить освобождать исходный буфер как объект — просто, но небезопасно из-за двойного уничтожения. Скопировать байты через обычное присваивание — не решает проблему для типа с Drop, поскольку копирование владения запрещено безопасными правилами и может создать две копии ресурса. Передавать только указатель и явно определить, какая сторона уничтожает объект, безопаснее по смыслу, но требует аккуратного протокола времени жизни.
Выбранное решение — хранить объект в области MaybeUninit, извлекать его один раз, а затем освобождать только сырую память без вызова деструктора исходного значения. Контракт также фиксирует, что C не читает и не освобождает этот объект после передачи владения. Это устраняет двойное уничтожение и делает ответственность за ресурс однозначной.
1. Можно ли после ptr::read записать новое значение в исходное место?
Да, если память остаётся валидной для записи, корректно выровнена и старое значение уже считается перемещённым. Запись нового значения не должна пытаться уничтожить прежнее значение повторно. Если новое значение имеет Drop, его жизненный цикл начинается с момента корректного размещения, а уничтожить его нужно ровно один раз.
2. Чем ptr::read отличается от создания ссылки на исходный объект с последующим перемещением?
Ссылка &T предполагает, что по адресу существует валидный объект T, а обычное перемещение через ссылку не извлекает значение без дополнительных гарантий. ptr::read напрямую выполняет небезопасное побитовое извлечение и не обновляет состояние исходной памяти. Поэтому вызывающий код сам обязан обеспечить, что после операции старое место не используется как живой объект.
3. Достаточно ли, чтобы FFI-буфер содержал байты, совпадающие с размером Rust-типа?
Нет. Нужны не только размер и выравнивание, но и допустимое значение каждого поля, совместимое представление, корректная инициализация и соответствующая модель владения. Для типов с Drop дополнительно требуется доказать, что все ресурсы были получены в форме, которую Rust-деструктор вправе освобождать, и что ни C, ни другой Rust-код не уничтожит их повторно.