Что меняется для вызывающего кода, если Rust-функцию объявить unsafe, хотя её тело не содержит небезопасных операций?
Объявление unsafe fn переносит обязанность доказать выполнение её предусловий на вызывающий код. Вызов такой функции требует небезопасного контекста, даже если внутри тела нет операций, помеченных как unsafe.
Сам unsafe не проверяет корректность предусловий автоматически: он только обозначает место, где программист принимает ответственность за их выполнение. Если функция не требует от вызывающего кода дополнительных гарантий, её обычно следует объявить безопасной.
Rust разделяет безопасный и небезопасный код, чтобы локализовать ручную проверку низкоуровневых инвариантов. Это позволяет использовать указатели, FFI и операции над неинициализированной памятью внутри небольших проверяемых участков, сохраняя безопасный интерфейс для остального приложения.
unsafe fn нужен не потому, что компилятор обязательно видит опасную инструкцию в теле. Он нужен для выражения контракта: корректность вызова зависит от условий, которые компилятор не может проверить, например валидности указателя или существования объекта по заданному адресу.
Функция может принимать raw pointer, размер буфера или дескриптор внешнего ресурса. Даже если её реализация формально не содержит небезопасной операции, она может предполагать, что указатель не равен null, выровнен, указывает на инициализированное значение нужного типа и допускает требуемый вид доступа.
Если объявить такую функцию безопасной, любой код сможет вызвать её без доказательства этих условий. Нарушение контракта может привести к неопределённому поведению, повреждению памяти или нарушению инвариантов владения.
Обратная крайность тоже вредна: без необходимости объявленная unsafe fn усложняет API и заставляет вызывающих писать небезопасный код. Поэтому границу нужно ставить по наличию непроверяемых предусловий у вызова, а не по наличию конкретной инструкции в теле.
У unsafe fn есть две разные зоны ответственности:
Небезопасный блок подтверждает лишь намерение программиста взять ответственность. Компилятор проверяет синтаксические и типовые ограничения, но не доказывает, что raw pointer указывает на живой объект, что длина диапазона корректна или что внешний дескриптор принадлежит нужному API.
Например, внутренний вызов можно скрыть безопасной оболочкой, если оболочка сама проверяет или получает необходимые гарантии из типов:
Здесь read_u32 требует валидный указатель на читаемый u32. Безопасная оболочка получает такой указатель из существующей ссылки внутри среза и вызывает функцию в небезопасном блоке, где доказательство локально очевидно.
Важно различать unsafe fn и unsafe внутри тела. Объявление функции задаёт требования к вызывающему коду, а небезопасный блок ограничивает конкретную операцию, которую автор реализации считает обоснованной. В современных редакциях Rust явные небезопасные блоки внутри unsafe fn помогают не смешивать эти две ответственности и облегчают аудит тела функции.
Документация unsafe fn должна перечислять проверяемые предусловия: допустимость null, выравнивание, инициализированность, срок жизни объекта, уникальность изменяемого доступа, границы диапазона, владение ресурсом и допустимость повторного вызова. Если предусловия можно надёжно проверять во время выполнения, предпочтительнее сделать безопасную оболочку, возвращающую Option или Result.
Команда создала низкоуровневую функцию чтения значения по указателю. Рассматривались три варианта:
unsafe fn и требовать небезопасный блок у каждого вызывающего кода. Граница ответственности ясна, но одинаковые проверки могут дублироваться, а ошибки становятся вероятнее.unsafe fn и предоставить безопасную оболочку над &[u32]. Срез гарантирует валидность, выравнивание, инициализированность и границы элемента; оболочка локализует единственный небезопасный вызов.Выбран третий вариант. Сырый интерфейс остался доступен только коду, который действительно умеет доказывать его контракт, а обычные вызовы работают через типизированный срез. Результатом стала меньшая область аудита без ложного обещания безопасности для произвольных адресов.
1. Достаточно ли поместить вызов unsafe fn в unsafe-блок, чтобы он стал корректным?
Нет. Блок только разрешает операцию на уровне языка; он не создаёт объект, не продлевает его время жизни и не исправляет неверную длину. Перед блоком должны быть установлены все предусловия функции, а сам блок желательно делать минимальным, чтобы было понятно, какое именно доказательство он использует.
2. Должна ли функция быть unsafe, если её тело вызывает только безопасные функции?
Не обязательно. Решение определяется контрактом вызова. Функция с полностью безопасным телом всё равно должна быть unsafe, если корректность зависит, например, от уникальности дескриптора, валидности внешнего ресурса или согласованности указателя и длины. И наоборот, внутреннее использование unsafe-кода можно скрыть за безопасным API, если оболочка полностью проверяет необходимые инварианты.
3. Что произойдёт, если предусловия unsafe fn изменятся, но вызывающие места не будут пересмотрены?
Безопасность API будет нарушена: старые вызовы могут продолжить компилироваться, хотя больше не удовлетворяют новому контракту. Поэтому изменение предусловий требует аудита всех вызовов, обновления документации и, по возможности, изменения типов интерфейса так, чтобы часть гарантий проверялась компилятором. Небезопасный контракт является частью API, даже если он не выражен обычной системой типов.