Что именно гарантирует компилятор Rust, когда разыменование raw pointer помещено в unsafe-блок?
unsafe-блок лишь разрешает компилятору принять операцию, которую он не может доказать безопасной. Он не проверяет, что raw pointer не равен null, указывает на живой объект правильного типа и выровнен; доказательство этих условий остаётся ответственностью разработчика. Если они нарушены, программа может иметь неопределённое поведение, несмотря на наличие unsafe-блока.
Rust проектировался как системный язык, которому нужны прямой доступ к памяти, взаимодействие с аппаратурой и интеграция с библиотеками C. Полностью проверить такие операции статически невозможно: компилятор не знает контрактов внешних библиотек, фактического времени жизни адресов и свойств памяти, выделенной вне Rust.
Поэтому язык отделяет проверяемый безопасный код от явно обозначенной зоны Unsafe Rust. Это не отключение всех проверок и не подтверждение корректности, а маркировка места, где разработчик принимает на себя доказательство дополнительных инвариантов.
Raw pointer может быть получен из FFI, адреса устройства или внутреннего низкоуровневого кода. Сам тип указателя не сообщает компилятору, указывает ли он на живой объект, доступный для чтения, и сохраняется ли необходимое выравнивание.
Даже проверка на null не устраняет остальные риски: указатель может быть висячим, указывать на неинициализированную память или нарушать правила доступа Rust. Ошибка может проявиться не в месте разыменования, а значительно позже, поскольку неопределённое поведение не обязано давать стабильный сбой.
Компилятор проверяет, что потенциально опасная операция находится в разрешённом unsafe-контексте, а обычные правила Rust продолжают действовать. Он не выполняет проверку адреса во время компиляции и не превращает raw pointer в безопасную ссылку автоматически.
Минимальный пример показывает разницу между разрешением операции и доказательством её корректности:
Вызов корректен только при выполнении контракта load: p должен быть ненулевым, указывать на живой и инициализированный u32, быть корректно выровнен и обеспечивать допустимый доступ на чтение. Код не проверяет эти условия; их должен гарантировать вызывающий код или безопасная обёртка.
unsafe не отменяет требования к алиасингу. Например, запись через raw pointer не должна конфликтовать с существующими активными доступами, если это нарушает модель памяти Rust. При работе с внешней памятью дополнительно нужно учитывать её владение, синхронизацию, время жизни и контракт внешнего API.
Практический компромисс таков: небольшой unsafe-фрагмент помещают внутрь безопасной абстракции, а её публичные условия делают проверяемыми. Если условия невозможно надёжно проверить, интерфейс обычно объявляют unsafe и явно документируют обязанности вызывающего кода.
Библиотека C возвращает указатель на внутренний объект и отдельную функцию его уничтожения. Разработчик помещает чтение полей этого объекта в unsafe-блок и считает проблему решённой. Однако C-код может уничтожить объект между двумя вызовами Rust, поэтому сам unsafe-блок не защищает от обращения к освобождённой памяти.
Рассматриваются варианты:
Выбирается обёртка с контролируемым временем жизни, а для данных, которые должны пережить C-объект, выполняется копирование. В результате raw pointer не покидает внутреннего слоя, а безопасный интерфейс не позволяет пользоваться уже уничтоженным ресурсом.
1. Достаточно ли проверить raw pointer на null перед разыменованием?
Нет. Проверка на null устраняет только один класс ошибок. Указатель всё ещё может быть висячим, невыравненным, направленным на неинициализированную память или не иметь права на требуемый тип доступа. Безопасность определяется полным контрактом указателя, а не одним условием.
2. Делает ли объявление функции unsafe доказательство корректности её тела?
Нет. unsafe fn сообщает, что вызывающий код обязан выполнить документированные условия, но не проверяет реализацию. Внутри функции всё равно нужно корректно использовать unsafe-операции и поддерживать инварианты; полезно ограничивать их отдельными unsafe-блоками, чтобы место ручного доказательства было явным.
3. Можно ли после разыменования raw pointer передать полученную ссылку в безопасный код?
Можно только после доказательства более сильных условий: объект жив, корректно выровнен и инициализирован, ссылка действительно указывает на нужный тип, а её время жизни и правила совместного доступа соблюдены. Создание ссылки не исправляет плохой указатель — оно переносит его предполагаемую корректность в гарантии, на которые затем будет полагаться безопасный код. Если доказательство неверно, неопределённое поведение может возникнуть уже из-за дальнейшего использования ссылки.