Unsafe Rust
Во всех предыдущих главах этой книги мы обсуждали код на Rust, безопасность памяти в котором гарантируется во время компиляции. Однако внутри Rust скрывается другой язык - небезопасный Rust, который не обеспечивает безопасной работы с памятью. Этот язык называется unsafe Rust и работает также как и первый, но предоставляет вам дополнительные возможности.
Небезопасный Rust существует потому что по своей природе статический анализ довольно консервативен. Когда компилятор пытается определить, соответствует ли код гарантиям, то он скорее отвергнет несколько допустимых программ, чем пропустит несколько недопустимых. Не смотря на то, что код может быть в порядке, если компилятор Rust не будет располагать достаточной информацией, чтобы убедиться в этом, он отвергнет код. В таких случаях вы можете использовать небезопасный код, чтобы сказать компилятору: "Поверь мне, я знаю, что делаю". Однако имейте в виду, что вы используете небезопасный Rust на свой страх и риск: если вы неправильно используете небезопасный код, могут возникнуть проблемы, связанные с нарушением безопасности памяти, например, разыменование нулевого указателя.
Другая причина, по которой у Rust есть небезопасное альтер эго, заключается в том, что по существу аппаратное обеспечение компьютера небезопасно. Если Rust не позволял бы вам выполнять небезопасные операции, вы не могли бы выполнять определённые задачи. Rust должен позволить вам использовать системное, низкоуровневое программирование, такое как прямое взаимодействие с операционной системой, или даже написание вашей собственной операционной системы. Возможность написания низкоуровневого, системного кода является одной из целей языка. Давайте рассмотрим, что и как можно делать с небезопасным Rust.
Небезопасные сверхспособности
Чтобы переключиться на небезопасный Rust, используйте ключевое слово unsafe
, а затем начните новый блок, содержащий небезопасный код. В небезопасном Rust можно выполнять пять действий, которые недоступны в безопасном Rust, которые мы называем небезопасными супер силами. Эти супер силы включают в себя следующее:
- Разыменование сырого указателя
- Вызов небезопасной функции или небезопасного метода
- Доступ или изменение изменяемой статической переменной
- Реализация небезопасного типажа
- Доступ к полям в
union
Важно понимать, что unsafe
не отключает проверку заимствования или любые другие проверки безопасности Rust: если вы используете ссылку в небезопасном коде, она всё равно будет проверена. Единственное, что делает ключевое слово unsafe
- даёт вам доступ к этим пяти возможностям, безопасность работы с памятью в которых не проверяет компилятор. Вы по-прежнему получаете некоторую степень безопасности внутри небезопасного блока.
Кроме того, unsafe
не означает, что код внутри этого блока является неизбежно опасным или он точно будет иметь проблемы с безопасностью памяти: цель состоит в том, что вы, как программист, гарантируете, что код внутри блока unsafe
будет обращаться к действительной памяти корректным образом.
Люди подвержены ошибкам и ошибки будут происходить, но требуя размещение этих четырёх небезопасных операции внутри блоков, помеченных как unsafe
, вы будете знать, что любые ошибки, связанные с безопасностью памяти, будут находиться внутри unsafe
блоков. Делайте unsafe
блоки маленькими; вы будете благодарны себе за это позже, при исследовании ошибок с памятью.
Чтобы максимально изолировать небезопасный код, рекомендуется заключить небезопасный код в безопасную абстракцию и предоставить безопасный API, который мы обсудим позже, когда будем обсуждать небезопасные функции и методы. Части стандартной библиотеки реализованы как проверенные, безопасные абстракции над небезопасным кодом. Оборачивание небезопасного кода в безопасную абстракцию предотвращает возможную утечку использования unsafe
кода во всех местах, где вы или ваши пользователи могли бы захотеть напрямую использовать функциональность, реализованную unsafe
кодом, потому что использование безопасной абстракции само безопасно.
Давайте поговорим о каждой из четырёх небезопасных сверх способностей, и по ходу дела рассмотрим некоторые абстракции, которые обеспечивают безопасный интерфейс для небезопасного кода.
Разыменование сырых указателей
В главе 4 раздела "Недействительные ссылки" мы упоминали, что компилятор гарантирует, что ссылки всегда действительны. Небезопасный Rust имеет два новых типа, называемых сырыми указателями (raw pointers), которые похожи на ссылки. Как и в случае ссылок, сырые указатели могут быть неизменяемыми или изменяемыми и записываться как *const T
и *mut T
соответственно. Звёздочка не является оператором разыменования; это часть имени типа. В контексте сырых указателей неизменяемый (immutable) означает, что указателю нельзя напрямую присвоить что-то после того как он разыменован.
В отличие от ссылок и умных указателей, сырые указатели:
- могут игнорировать правила заимствования и иметь неизменяемые и изменяемые указатели, или множество изменяемых указателей на одну и ту же область памяти
- не гарантируют что ссылаются на действительную память
- могут быть null
- не реализуют автоматическую очистку памяти
Отказавшись от этих гарантий, вы можете обменять безопасность на большую производительность или возможность взаимодействия с другим языком или оборудованием, где гарантии Rust не применяются.
В листинге 19-1 показано, как создать неизменяемый и изменяемый сырой указатель из ссылок.
#![allow(unused)] fn main() { {{#rustdoc_include ../listings/ch19-advanced-features/listing-19-01/src/main.rs:here}} }
Обратите внимание, что мы не используем ключевое слово unsafe
в этом коде. Можно создавать сырые указатели в безопасном коде; мы просто не можем разыменовывать сырые указатели за пределами небезопасного блока, как вы увидите чуть позже.
Мы создали сырые указатели, используя as
для приведения неизменяемой и изменяемой ссылки к соответствующим им типам сырых указателей. Поскольку мы создали их непосредственно из ссылок, которые гарантированно являются действительными, мы знаем, что эти конкретные сырые указатели являются действительными, но мы не можем делать такое же предположение о любом сыром указателе.
Чтобы продемонстрировать это, создадим сырой указатель, в достоверности которого мы не можем быть так уверены. В листинге 19-2 показано, как создать необработанный указатель на произвольное место в памяти. Попытка использовать произвольную память является непредсказуемой: по этому адресу могут быть данные, а могут и не быть, компилятор может оптимизировать код так, что доступа к памяти не будет, или программа может завершиться с ошибкой сегментации. Обычно нет веских причин писать такой код, но это возможно.
#![allow(unused)] fn main() { {{#rustdoc_include ../listings/ch19-advanced-features/listing-19-02/src/main.rs:here}} }
Напомним, что можно создавать сырые указатели в безопасном коде, но нельзя разыменовывать сырые указатели и читать данные, на которые они указывают. В листинге 19-3 мы используем оператор разыменования *
для сырого указателя, который требует unsafe
блока.
#![allow(unused)] fn main() { {{#rustdoc_include ../listings/ch19-advanced-features/listing-19-03/src/main.rs:here}} }
Создание указателей безопасно. Только при попытке доступа к объекту по адресу в указателе мы можем получить недопустимое значение.
Также обратите внимание, что в примерах кода 19-1 и 19-3 мы создали *const i32
и *mut i32
, которые ссылаются на одну и ту же область памяти, где хранится num
. Если мы попытаемся создать неизменяемую и изменяемую ссылку на num
вместо сырых указателей, такой код не скомпилируется, т.к. будут нарушены правила заимствования, запрещающие наличие изменяемой ссылки одновременно с неизменяемыми ссылками. С помощью сырых указателей мы можем создать изменяемый указатель и неизменяемый указатель на одну и ту же область памяти и изменять данные с помощью изменяемого указателя, потенциально создавая эффект гонки данных. Будьте осторожны!
С учётом всех этих опасностей, зачем тогда использовать сырые указатели? Одним из основных применений является взаимодействие с кодом C, как вы увидите в следующем разделе "Вызов небезопасной функции или метода". Другой случай это создание безопасных абстракций, которые не понимает анализатор заимствований. Мы введём понятие небезопасных функций и затем рассмотрим пример безопасной абстракции, которая использует небезопасный код.
Вызов небезопасной функции или метода
Второй тип операций, которые можно выполнять в небезопасном блоке - это вызов небезопасных функций. Небезопасные функции и методы выглядят точно так же, как обычные функции и методы, но перед остальным определением у них есть дополнительное unsafe
. Ключевое слово unsafe
в данном контексте указывает на то, что к функции предъявляются требования, которые мы должны соблюдать при вызове этой функции, поскольку Rust не может гарантировать, что мы их выполняем. Вызывая небезопасную функцию внутри блока unsafe
, мы говорим, что прочитали документацию к этой функции и берём на себя ответственность за соблюдение её условий.
Вот небезопасная функция с именем dangerous
которая ничего не делает в своём теле:
#![allow(unused)] fn main() { {{#rustdoc_include ../listings/ch19-advanced-features/no-listing-01-unsafe-fn/src/main.rs:here}} }
Мы должны вызвать функцию dangerous
в отдельном unsafe
блоке. Если мы попробуем вызвать dangerous
без unsafe
блока, мы получим ошибку:
{{#include ../listings/ch19-advanced-features/output-only-01-missing-unsafe/output.txt}}
С помощью блока unsafe
мы сообщаем Rust, что прочитали документацию к функции, поняли, как правильно её использовать, и убедились, что выполняем контракт функции.
Тела небезопасных функций являются фактически unsafe
блоками, поэтому для выполнения других небезопасных операций внутри небезопасной функции не нужно добавлять ещё один unsafe
блок.
Создание безопасных абстракций вокруг небезопасного кода
То, что функция содержит небезопасный код, не означает, что мы должны пометить всю функцию как небезопасную. На самом деле, обёртывание небезопасного кода в безопасную функцию - это обычная абстракция. В качестве примера рассмотрим функцию split_at_mut
из стандартной библиотеки, которая требует некоторого небезопасного кода. Рассмотрим, как мы могли бы её реализовать. Этот безопасный метод определён для изменяемых срезов: он берет один срез и превращает его в два, разделяя срез по индексу, указанному в качестве аргумента. В листинге 19-4 показано, как использовать split_at_mut
.
#![allow(unused)] fn main() { {{#rustdoc_include ../listings/ch19-advanced-features/listing-19-04/src/main.rs:here}} }
Эту функцию нельзя реализовать, используя только безопасный Rust. Попытка реализации могла бы выглядеть примерно как в листинге 19-5, который не компилируется. Для простоты мы реализуем split_at_mut
как функцию, а не как метод, и только для значений типа i32
, а не обобщённого типа T
.
{{#rustdoc_include ../listings/ch19-advanced-features/listing-19-05/src/main.rs:here}}
Эта функция сначала получает общую длину среза. Затем она проверяет (assert), что индекс, переданный в качестве параметра, находится в границах среза, сравнивая его с длиной. Assert означает, что если мы передадим индекс, который больше, чем длина среза, функция запаникует ещё до попытки использования этого индекса.
Затем мы возвращаем два изменяемых фрагмента в кортеже: один от начала исходного фрагмента до mid
индекса (не включая сам mid), а другой - от mid
(включая сам mid) до конца фрагмента.
При попытке скомпилировать код в листинге 19-5, мы получим ошибку.
{{#include ../listings/ch19-advanced-features/listing-19-05/output.txt}}
Анализатор заимствований Rust не может понять, что мы заимствуем различные части среза, он понимает лишь, что мы хотим осуществить заимствование частей одного среза дважды. Заимствование различных частей среза в принципе нормально, потому что они не перекрываются, но Rust недостаточно умён, чтобы это понять. Когда мы знаем, что код верный, но Rust этого не понимает, значит пришло время прибегнуть к небезопасному коду.
Листинг 19-6 демонстрирует, как можно использовать unsafe
блок, сырой указатель и вызовы небезопасных функций чтобы split_at_mut
заработала:
#![allow(unused)] fn main() { {{#rustdoc_include ../listings/ch19-advanced-features/listing-19-06/src/main.rs:here}} }
Напомним, из раздела "Тип срез" главы 4, что срезы состоят из указателя на некоторые данные и длины. Мы используем метод len
для получения длины среза и метод as_mut_ptr
для доступа к сырому указателю среза. Поскольку у нас есть изменяемый срез на значения типа i32
, функция as_mut_ptr
возвращает сырой указатель типа *mut i32
, который мы сохранили в переменной ptr
.
Далее проверяем, что индекс mid
находится в границах среза. Затем мы обращаемся к небезопасному коду: функция slice::from_raw_parts_mut
принимает сырой указатель, длину и создаёт срез. Мы используем эту функцию для создания среза, начинающегося с ptr
и имеющего длину в mid
элементов. Затем мы вызываем метод add
у ptr
с mid
в качестве аргумента, чтобы получить сырой указатель, который начинается с mid
, и создаём срез, используя этот указатель и оставшееся количество элементов после mid
в качестве длины.
Функция slice::from_raw_parts_mut
является небезопасной, потому что она принимает необработанный указатель и должна полагаться на то, что этот указатель действителен. Метод add
для необработанных указателей также небезопасен, поскольку он должен считать, что местоположение смещения также является действительным указателем. Поэтому мы были вынуждены разместить unsafe
блок вокруг наших вызовов slice::from_raw_parts_mut
и add
, чтобы иметь возможность вызвать их. Посмотрев на код и добавив утверждение, что mid
должен быть меньше или равен len
, мы можем сказать, что все необработанные указатели, используемые в блоке unsafe
, будут корректными указателями на данные внутри среза. Это приемлемое и уместное использование unsafe
.
Обратите внимание, что нам не нужно помечать результирующую функцию split_at_mut
как unsafe
, и мы можем вызвать эту функцию из безопасного Rust. Мы создали безопасную абстракцию для небезопасного кода с помощью реализации функции, которая использует код unsafe
блока безопасным образом, поскольку она создаёт только допустимые указатели из данных, к которым эта функция имеет доступ.
Напротив, использование slice::from_raw_parts_mut
в листинге 19-7 приведёт к вероятному сбою при использовании среза. Этот код использует произвольный адрес памяти и создаёт срез из 10000 элементов.
#![allow(unused)] fn main() { {{#rustdoc_include ../listings/ch19-advanced-features/listing-19-07/src/main.rs:here}} }
Мы не владеем памятью в этом произвольном месте, и нет никакой гарантии, что созданный этим кодом фрагмент содержит допустимые значения i32
. Попытка использовать values
так, как будто это допустимый срез, приводит к неопределённому поведению.
Использование extern
функций для вызова внешнего кода
Иногда вашему коду на языке Rust может потребоваться взаимодействие с кодом, написанным на другом языке. Для этого в Rust есть ключевое слово extern
, которое облегчает создание и использование интерфейса внешних функций (Foreign Function Interface - FFI). FFI - это способ для языка программирования определить функции и позволить другому (внешнему) языку программирования вызывать эти функции.
Листинг 19-8 демонстрирует, как настроить интеграцию с функцией abs
из стандартной библиотеки C. Функции, объявленные внутри блоков extern
, всегда небезопасны для вызова из кода Rust. Причина в том, что другие языки не обеспечивают соблюдение правил и гарантий Rust, Rust также не может проверить гарантии, поэтому ответственность за безопасность ложится на программиста.
Имя файла: src/main.rs
#![allow(unused)] fn main() { {{#rustdoc_include ../listings/ch19-advanced-features/listing-19-08/src/main.rs}} }
Внутри блока extern "C"
мы перечисляем имена и сигнатуры внешних функций из другого языка, которые мы хотим вызвать. Часть "C"
определяет какой application binary interface (ABI - бинарный интерфейс приложений) использует внешняя функция. Интерфейс ABI определяет как вызвать функцию на уровне ассемблера. Использование ABI "C"
является наиболее часто используемым и следует правилам ABI интерфейса языка Си.
Вызов функций Rust из других языков
Также можно использовать
extern
для создания интерфейса, позволяющего другим языкам вызывать функции Rust. Вместо того чтобы создавать целый блокextern
, мы добавляем ключевое словоextern
и указываем ABI для использования непосредственно перед ключевым словомfn
для необходимой функции. Нам также нужно добавить аннотацию#[no_mangle]
, чтобы сказать компилятору Rust не искажать имя этой функции. Искажение - это когда компилятор меняет имя, которое мы дали функции, на другое имя, которое содержит больше информации для других частей процесса компиляции, но менее читабельно для человека. Компилятор каждого языка программирования искажает имена по-разному, поэтому, чтобы функция Rust могла быть использована другими языками, мы должны отключить искажение имён в компиляторе Rust.В следующем примере мы делаем функцию
call_from_c
доступной из кода на C, после того как она будет скомпилирована в разделяемую библиотеку и прилинкована с C:#![allow(unused)] fn main() { #[no_mangle] pub extern "C" fn call_from_c() { println!("Just called a Rust function from C!"); } }
Такое использование
extern
не требуетunsafe
.
Получение доступа и внесение изменений в изменяемую статическую переменную
В этой книге мы ещё не говорили о глобальных переменных, которые Rust поддерживает, но с которыми могут возникнуть проблемы из-за действующих в Rust правил владения. Если два потока обращаются к одной и той же изменяемой глобальной переменной, это может привести к гонке данных.
Глобальные переменные в Rust называют статическими (static). Листинг 19-9 демонстрирует пример объявления и использования в качестве значения статической переменной, имеющей тип строкового среза:
Имя файла: src/main.rs
#![allow(unused)] fn main() { {{#rustdoc_include ../listings/ch19-advanced-features/listing-19-09/src/main.rs}} }
Статические переменные похожи на константы, которые мы обсуждали в разделе “Различия между переменными и константами” главы 3. Имена статических переменных по общему соглашению пишутся в нотации SCREAMING_SNAKE_CASE
, и мы должны указывать тип переменной, которым в данном случае является &'static str
. Статические переменные могут хранить только ссылки со временем жизни 'static
, это означает что компилятор Rust может вывести время жизни и нам не нужно прописывать его явно. Доступ к неизменяемой статической переменной является безопасным.
Тонкое различие между константами и неизменяемыми статическими переменными заключается в том, что значения в статической переменной имеют фиксированный адрес в памяти. При использовании значения всегда будут доступны одни и те же данные. Константы, с другой стороны, могут дублировать свои данные при каждом использовании. Ещё одно отличие заключается в том, что статические переменные могут быть изменяемыми. Обращение к изменяемым статическим переменным и их изменение является небезопасным. В листинге 19-10 показано, как объявить, получить доступ и модифицировать изменяемую статическую переменную с именем COUNTER
.
Имя файла: src/main.rs
#![allow(unused)] fn main() { {{#rustdoc_include ../listings/ch19-advanced-features/listing-19-10/src/main.rs}} }
Как и с обычными переменными, мы определяем изменяемость с помощью ключевого слова mut
. Любой код, который читает из или пишет в переменную COUNTER
должен находиться в unsafe
блоке. Этот код компилируется и печатает COUNTER: 3
, как и следовало ожидать, потому что выполняется в одном потоке. Наличие нескольких потоков с доступом к COUNTER
приведёт к ситуации гонки данных.
Наличие изменяемых данных, которые доступны глобально, делает трудным реализацию гарантии отсутствия гонок данных, поэтому Rust считает изменяемые статические переменные небезопасными. Там, где это возможно, предпочтительно использовать техники многопоточности и умные указатели, ориентированные на многопоточное исполнение, которые мы обсуждали в главе 16. Таким образом, компилятор сможет проверить, что обращение к данным, доступным из разных потоков, выполняется безопасно.
Реализация небезопасных типажей
Мы можем использовать unsafe
для реализации небезопасного трейта. Трейт является небезопасным, если хотя бы один из его методов имеет некоторый инвариант, который компилятор не может проверить. Мы объявляем трейты unsafe
, добавляя ключевое слово unsafe
перед trait
и помечая реализацию трейта как unsafe
, как показано в листинге 19-11.
#![allow(unused)] fn main() { {{#rustdoc_include ../listings/ch19-advanced-features/listing-19-11/src/main.rs}} }
Используя unsafe impl
, мы даём обещание поддерживать инварианты, которые компилятор не может проверить.
Для примера вспомним маркерные типажи Sync
и Send
, которые мы обсуждали в разделе "Расширяемый параллелизм с помощью типажей Sync
и Send
" главы 16: компилятор реализует эти типажи автоматически, если наши типы полностью состоят из типов Send
и Sync
. Если мы создадим тип, который содержит тип, не являющийся Send
или Sync
, такой, как сырой указатель, и мы хотим пометить этот тип как Send
или Sync
, мы должны использовать unsafe
блок. Rust не может проверить, что наш тип поддерживает гарантии того, что он может быть безопасно передан между потоками или доступен из нескольких потоков; поэтому нам нужно добавить эти проверки вручную и указать это с помощью unsafe
.
Доступ к полям объединений (union)
Последнее действие, которое работает только с unsafe
- это доступ к полям union. union
похож на struct
, но в каждом конкретном экземпляре одновременно может использоваться только одно объявленное поле. Объединения в основном используются для взаимодействия с объединениями в коде на языке Си. Доступ к полям объединений небезопасен, поскольку Rust не может гарантированно определить тип данных, которые в данный момент хранятся в экземпляре объединения. Подробнее об объединениях вы можете узнать в the Rust Reference.
Когда использовать небезопасный код
Использование unsafe
для выполнения одного из пяти действий (супер способностей), которые только что обсуждались, не является ошибочным или не одобренным. Но получить корректный unsafe
код сложнее, потому что компилятор не может помочь в обеспечении безопасности памяти. Если у вас есть причина использовать unsafe
код, вы можете делать это, а наличие явной unsafe
аннотации облегчает отслеживание источника проблем, если они возникают.