Исследование сверхспособностей небезопасного Rust

RustRustBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в Unsafe Rust. Эта лабораторная работа является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

В этой лабораторной работе мы будем изучать unsafe Rust, функцию, которая позволяет нам обойти гарантии безопасности памяти, налагаемые во время компиляции, и дает нам дополнительные суперспособности, а также понять риски и ответственности, связанные с ее использованием.

Unsafe Rust

Все код, который мы обсуждали до сих пор, имел гарантии безопасности памяти Rust, налагаемые во время компиляции. Однако, внутри Rust скрывается вторая языковая часть, которая не налагает эти гарантии безопасности памяти: это называется unsafe Rust и работает так же, как и обычный Rust, но дает нам дополнительные суперспособности.

Unsafe Rust существует потому, что по своей природе статический анализ консервативен. Когда компилятор пытается определить, соответствует ли код гарантиям, для него лучше отвергнуть некоторые валидные программы, чем принять некоторые невалидные программы. Хотя код возможно будет в порядке, если у Rust компилятора недостаточно информации, чтобы быть уверенным, он отвергнет код. В таких случаях вы можете использовать небезопасный код, чтобы сказать компилятору: "Доверьте мне, я знаю, что делаю". Будьте осторожны, однако, поскольку вы используете unsafe Rust на свою ответственность: если вы неправильно используете небезопасный код, могут возникнуть проблемы из-за небезопасности памяти, такой как разыменование нулевого указателя.

Другой reason Rust имеет небезопасную альтернативу заключается в том, что базовый компьютерный аппарат по своей природе небезопасен. Если Rust не позволял бы вам выполнять небезопасные операции, вы не могли бы выполнить определенные задачи. Rust должен позволить вам выполнять низкоуровневую системную программирование, например, непосредственно взаимодействовать с операционной системой или даже писать свою собственную операционную систему. Работа с низкоуровневым системным программированием является одной из целей языка. Давайте исследуем, что мы можем сделать с unsafe Rust и как это сделать.

Небезопасные суперспособности

Для перехода к unsafe Rust используйте ключевое слово unsafe, а затем начните новый блок, содержащий небезопасный код. В unsafe Rust вы можете совершать пять действий, которые невозможно сделать в safe Rust, которые мы называем небезопасными суперспособностями. Эти суперспособности включают в себя возможность:

  1. Разыменовывать сырой указатель
  2. Вызывать небезопасную функцию или метод
  3. Доступать к или изменять изменяемую статическую переменную
  4. Реализовать небезопасный трейт
  5. Доступать к полям unionов

Важно понимать, что unsafe не отключает проверку ссылок или отключает любые другие проверки безопасности Rust: если вы используете ссылку в небезопасном коде, она по-прежнему будет проверяться. Ключевое слово unsafe только дает вам доступ к этим пяти функциям, которые затем не проверяются компилятором на безопасность памяти. Вы по-прежнему получите некоторую степень безопасности внутри небезопасного блока.

此外, unsafe не означает, что код внутри блока обязательно опасен или что у него обязательно будут проблемы с безопасностью памяти: цель заключается в том, что, как программист, вы должны убедиться, что код внутри небезопасного блока будет доступать к памяти в допустимом порядке.

Люди ошибочны, и ошибки будут соверщаться, но, требуюя, чтобы эти пять небезопасных операций были внутри блоков, помеченных unsafe, вы будете знать, что любые ошибки, связанные с безопасностью памяти, должны быть внутри небезопасного блока. Сделайте небезопасные блоки небольшими; вы будете благодарны позже, когда будете изучать ошибки памяти.

Для максимально возможного изоляции небезопасного кода лучше заключать такой код в безопасную абстракцию и предоставить безопасный API, о котором мы поговорим позже в главе, когда будем рассматривать небезопасные функции и методы. Части стандартной библиотеки реализованы в виде безопасных абстракций над небезопасным кодом, который был проверен. Оборачивание небезопасного кода в безопасную абстракцию предотвращает то, чтобы использование unsafe вылезало во все места, где вы или ваши пользователи могут хотеть использовать функциональность, реализованную с использованием небезопасного кода, потому что использование безопасной абстракции безопасно.

Давайте依次 рассмотрим каждую из пяти небезопасных суперспособностей. Мы также рассмотрим некоторые абстракции, которые предоставляют безопасный интерфейс к небезопасному коду.

Разыменование сырого указателя

В разделе "Проматывающиеся ссылки" мы упоминали, что компилятор гарантирует, что ссылки всегда действительны. В unsafe Rust есть два новых типа, называемых сырыми указателями, которые похожи на ссылки. Как и в случае с ссылками, сырой указатель может быть неизменяемым или изменяемым и записывается соответственно как *const T и *mut T. Звездочка не является оператором разыменования; это часть имени типа. В контексте сырых указателей неизменяемым означает, что после разыменования указатель нельзя напрямую присвоить.

В отличие от ссылок и умных указателей, сырой указатель:

  • Может игнорировать правила заимствования, имея как неизменяемый, так и изменяемый указатель или несколько изменяемых указателей на одну и ту же область памяти
  • Не гарантируется, что указывает на действительную память
  • Может быть равен нулю
  • Не реализует автоматическое очищение

Отказавшись от того, чтобы Rust гарантировал эти условия, вы можете отказаться от гарантированной безопасности в обмен на большую производительность или возможность взаимодействовать с другой языковой средой или оборудованием, где гарантии Rust не действуют.

Листинг 19-1 показывает, как создать неизменяемый и изменяемый сырой указатель из ссылок.

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

Листинг 19-1: Создание сырых указателей из ссылок

Заметьте, что в этом коде мы не используем ключевое слово unsafe. Мы можем создавать сырые указатели в безопасном коде; мы просто не можем разыменовывать сырой указатель вне небезопасного блока, как вы вскоре увидите.

Мы создали сырые указатели, используя as для приведения неизменяемой и изменяемой ссылки к соответствующим типам сырых указателей. Поскольку мы создали их напрямую из ссылок, гарантированных как действительных, мы знаем, что эти конкретные сырые указатели действительны, но мы не можем делать этого предположения о любом сыром указателе.

Чтобы продемонстрировать это, далее мы создадим сырой указатель, действительность которого мы не можем так уверенно определить. Листинг 19-2 показывает, как создать сырой указатель на произвольное место в памяти. Попытка использовать произвольную память имеет неопределенное поведение: может быть, там есть данные, или может быть их и нет, компилятор может оптимизировать код так, чтобы не было доступа к памяти, или программа может завершиться с ошибкой сегментации. Обычно нет никакой причины писать такой код, но это возможно.

let address = 0x012345usize;
let r = address as *const i32;

Листинг 19-2: Создание сырого указателя на произвольный адрес памяти

Помните, что мы можем создавать сырые указатели в безопасном коде, но мы не можем разыменовывать сырой указатель и прочитать данные, на которые он указывает. В Листинге 19-3 мы используем оператор разыменования * для сырого указателя, что требует небезопасного блока.

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe {
    println!("r1 is: {}", *r1);
    println!("r2 is: {}", *r2);
}

Листинг 19-3: Разыменование сырых указателей внутри небезопасного блока

Создание указателя не наносит вреда; только когда мы пытаемся получить доступ к значению, на которое он указывает, мы можем столкнуться с недействительным значением.

Обратите внимание также, что в Листингах 19-1 и 19-3 мы создали сырые указатели *const i32 и *mut i32, которые оба указывали на одну и ту же область памяти, где хранится num. Если бы мы вместо этого попытались создать неизменяемую и изменяемую ссылку на num, код не скомпилировался бы, потому что правила владения Rust не позволяют иметь изменяемую ссылку одновременно с любой неизменяемой ссылкой. С сырыми указателями мы можем создать изменяемый и неизменяемый указатель на одну и ту же область памяти и изменить данные через изменяемый указатель, что потенциально может привести к гонке данных. Будьте осторожны!

С учетом всех этих опасностей, зачем вы когда-либо будете использовать сырые указатели? Одним из основных случаев использования является взаимодействие с кодом на C, как вы увидите в разделе "Вызов небезопасной функции или метода". Другой случай - это создание безопасных абстракций, которые не понимает проверщик заимствования. Мы представим небезопасные функции и затем рассмотрим пример безопасной абстракции, которая использует небезопасный код.

Вызов небезопасной функции или метода

Второй тип операции, который можно выполнять в небезопасном блоке, - это вызов небезопасных функций. Незопасные функции и методы выглядят точно так же, как обычные функции и методы, но перед остальной частью определения у них есть дополнительное ключевое слово unsafe. Ключевое слово unsafe в этом контексте означает, что функция имеет требования, которые мы должны соблюдать при вызове этой функции, потому что Rust не может гарантировать, что мы выполнили эти требования. Вызовом небезопасной функции внутри небезопасного блока мы говорим, что прочитали документацию по этой функции и берем на себя ответственность за соблюдение контрактов функции.

Вот небезопасная функция под названием dangerous, которая ничего не делает в своем теле:

unsafe fn dangerous() {}

unsafe {
    dangerous();
}

Мы должны вызывать функцию dangerous внутри отдельного небезопасного блока. Если мы попытаемся вызвать dangerous без небезопасного блока, мы получим ошибку:

error[E0133]: call to unsafe function is unsafe and requires
unsafe function or block
 --> src/main.rs:4:5
  |
4 |     dangerous();
  |     ^^^^^^^^^^^ call to unsafe function
  |
  = note: consult the function's documentation for information on
how to avoid undefined behavior

С помощью небезопасного блока мы утверждаем Rust, что прочитали документацию по функции, понимаем, как ее правильно использовать, и проверили, что выполняем контракт функции.

Тела небезопасных функций по существу являются небезопасными блоками, поэтому для выполнения других небезопасных операций внутри небезопасной функции нам не нужно добавлять еще один небезопасный блок.

Создание безопасной абстракции над небезопасным кодом

Просто потому, что функция содержит небезопасный код, это не означает, что мы должны пометить всю функцию как небезопасную. На самом деле оборачивание небезопасного кода в безопасную функцию - это распространенная абстракция. В качестве примера давайте изучим функцию split_at_mut из стандартной библиотеки, которая требует некоторого небезопасного кода. Мы исследуем, как ее можно реализовать. Этот безопасный метод определяется для изменяемых срезов: он берет один срез и делит его на два, деля срез по индексу, заданному в качестве аргумента. Листинг 19-4 показывает, как использовать split_at_mut.

let mut v = vec![1, 2, 3, 4, 5, 6];

let r = &mut v[..];

let (a, b) = r.split_at_mut(3);

assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);

Листинг 19-4: Использование безопасной функции split_at_mut

Мы не можем реализовать эту функцию только с использованием safe Rust. Попытка может выглядеть примерно как в Листинге 19-5, который не скомпилируется. Для простоты мы реализуем split_at_mut в виде функции, а не метода, и только для срезов значений i32, а не для обобщенного типа T.

fn split_at_mut(
    values: &mut [i32],
    mid: usize,
) -> (&mut [i32], &mut [i32]) {
    let len = values.len();

    assert!(mid <= len);

    (&mut values[..mid], &mut values[mid..])
}

Листинг 19-5: Пытаемая реализация split_at_mut только с использованием safe Rust

Эта функция сначала получает общую длину среза. Затем она проверяет, что индекс, заданный в качестве параметра, находится внутри среза, сравнивая его с длиной. Это утверждение означает, что если мы передадим индекс, превышающий длину, чтобы разделить срез, функция будет завершаться с паникой, прежде чем попытается использовать этот индекс.

Затем мы возвращаем два изменяемых среза в кортеже: один от начала исходного среза до индекса mid и другой от mid до конца среза.

Когда мы пытаемся скомпилировать код из Листинга 19-5, мы получим ошибку:

error[E0499]: cannot borrow `*values` as mutable more than once at a time
 --> src/main.rs:9:31
  |
2 |     values: &mut [i32],
  |             - let's call the lifetime of this reference `'1`
...
9 |     (&mut values[..mid], &mut values[mid..])
  |     --------------------------^^^^^^--------
  |     |     |                   |
  |     |     |                   second mutable borrow occurs here
  |     |     first mutable borrow occurs here
  |     returning this value requires that `*values` is borrowed for `'1`

Проверщик ссылок Rust не может понять, что мы берем разные части среза; он только знает, что мы берем из одного и того же среза дважды. Взятие разных частей среза по существу допустимо, потому что два среза не перекрываются, но Rust недостаточно умный, чтобы это знать. Когда мы знаем, что код корректен, но Rust не знает, настало время использовать небезопасный код.

Листинг 19-6 показывает, как использовать небезопасный блок, сырой указатель и несколько вызовов небезопасных функций, чтобы сделать реализацию split_at_mut работоспособной.

use std::slice;

fn split_at_mut(
    values: &mut [i32],
    mid: usize,
) -> (&mut [i32], &mut [i32]) {
  1 let len = values.len();
  2 let ptr = values.as_mut_ptr();

  3 assert!(mid <= len);

  4 unsafe {
        (
          5 slice::from_raw_parts_mut(ptr, mid),
          6 slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

Листинг 19-6: Использование небезопасного кода в реализации функции split_at_mut

Помните из раздела "Тип среза", что срез - это указатель на некоторые данные и длина среза. Мы используем метод len, чтобы получить длину среза [1], и метод as_mut_ptr, чтобы получить сырой указатель среза [2]. В этом случае, поскольку у нас есть изменяемый срез значений i32, as_mut_ptr возвращает сырой указатель с типом *mut i32, который мы сохранили в переменной ptr.

Мы сохраняем утверждение, что индекс mid находится внутри среза [3]. Затем мы доходим до небезопасного кода [4]: функция slice::from_raw_parts_mut принимает сырой указатель и длину и создает срез. Мы используем ее, чтобы создать срез, который начинается с ptr и имеет длину mid элементов [5]. Затем мы вызываем метод add на ptr с аргументом mid, чтобы получить сырой указатель, который начинается с mid, и создаем срез с использованием этого указателя и оставшимся количеством элементов после mid в качестве длины [6].

Функция slice::from_raw_parts_mut небезопасна, потому что она принимает сырой указатель и должна доверять, что этот указатель действителен. Метод add для сырых указателей также небезопасен, потому что он должен доверять, что смещение также является действительным указателем. Поэтому мы должны поместить небезопасный блок вокруг вызовов slice::from_raw_parts_mut и add, чтобы мы могли их вызвать. Смотрев на код и добавив утверждение, что mid должен быть меньше или равен len, мы можем сказать, что все сырые указатели, используемые внутри небезопасного блока, будут действительными указателями на данные внутри среза. Это приемлемое и подходящее использование unsafe.

Обратите внимание, что мы не должны помечать результирующую функцию split_at_mut как unsafe, и мы можем вызвать эту функцию из safe Rust. Мы создали безопасную абстракцию для небезопасного кода с реализацией функции, которая использует небезопасный код безопасным образом, потому что она создает только действительные указатели из данных, к которым имеет доступ эта функция.

В отличие от этого, использование slice::from_raw_parts_mut в Листинге 19-7, вероятно, вызовет аварийное завершение при использовании среза. Этот код берет произвольное место в памяти и создает срез длиной в 10 000 элементов.

use std::slice;

let address = 0x01234usize;
let r = address as *mut i32;

let values: &[i32] = unsafe {
    slice::from_raw_parts_mut(r, 10000)
};

Листинг 19-7: Создание среза из произвольного места в памяти

Мы не владеем памятью в этом произвольном месте, и не гарантируется, что срез, созданный этим кодом, содержит действительные значения i32. Попытка использовать values как действительный срез приводит к неопределенному поведению.

Использование внешних функций для вызова внешнего кода

Иногда ваш код на Rust может потребовать взаимодействия с кодом, написанным на другом языке. Для этого в Rust есть ключевое слово extern, которое облегчает создание и использование Foreign Function Interface (FFI), который представляет собой способ программирования языка определить функции и позволить другим (иностранным) языкам программирования вызывать эти функции.

Листинг 19-8 демонстрирует, как настроить интеграцию с функцией abs из стандартной библиотеки C. Функции, объявленные внутри блоков extern, всегда небезопасны для вызова из кода на Rust. Причина заключается в том, что другие языки не налагают правила и гарантии Rust, и Rust не может проверить их, поэтому ответственность ложится на программиста обеспечить безопасность.

Имя файла: src/main.rs

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!(
            "Absolute value of -3 according to C: {}",
            abs(-3)
        );
    }
}

Листинг 19-8: Объявление и вызов внешней функции, определенной в другом языке

Внутри блока extern "C" мы перечисляем имена и сигнатуры внешних функций из другого языка, которые мы хотим вызвать. Часть "C" определяет, какой application binary interface (ABI) использует внешняя функция: ABI определяет, как вызывать функцию на уровне ассемблера. ABI "C" - это наиболее распространенный и соответствует ABI языка программирования C.

Вызов функций на Rust из других языков

Мы также можем использовать extern, чтобы создать интерфейс, позволяющий другим языкам вызывать функции на Rust. Вместо создания целого блока extern мы добавляем ключевое слово extern и указываем ABI для использования непосредственно перед ключевым словом fn для соответствующей функции. Мы также должны добавить аннотацию #[no_mangle], чтобы сообщить компилятору Rust не преобразовывать имя этой функции. Преобразование имени - это когда компилятор изменяет имя, которое мы дали функции, на другое имя, которое содержит больше информации для других частей процесса компиляции, но менее читаемое для человека. Каждый компилятор языка программирования преобразует имена несколько по-разному, поэтому для того, чтобы функция на Rust была именуемой из других языков, мы должны отключить преобразование имен компилятора Rust.

В следующем примере мы делаем функцию call_from_c доступной из кода на C, после того, как она скомпилирована в динамическую библиотеку и связана из C:

#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

Это использование extern не требует unsafe.

Доступ к изменяемой статической переменной или ее изменение

В этой книге мы еще не говорили о глобальных переменных, которые Rust поддерживает, но могут быть проблематичными в контексте правил владения Rust. Если два потока обращаются к одной и той же изменяемой глобальной переменной, это может привести к гонке данных.

В Rust глобальные переменные называются статическими переменными. Листинг 19-9 показывает пример объявления и использования статической переменной со строковым срезом в качестве значения.

Имя файла: src/main.rs

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("value is: {HELLO_WORLD}");
}

Листинг 19-9: Определение и использование неизменяемой статической переменной

Статические переменные похожи на константы, о которых мы говорили в разделе "Константы". Имена статических переменных по соглашению записываются в стиле SCREAMING_SNAKE_CASE. Статические переменные могут хранить только ссылки с временем жизни 'static, что означает, что Rust-компилятор может определить время жизни, и нам не нужно явно его аннотировать. Доступ к неизменяемой статической переменной безопасен.

Некоторое тонкое отличие между константами и неизменяемыми статическими переменными заключается в том, что значения в статической переменной имеют фиксированный адрес в памяти. Использование значения всегда будет обращаться к тем же данным. В то же время константы могут дублировать свои данные при каждом использовании. Еще одно отличие заключается в том, что статические переменные могут быть изменяемыми. Доступ к и изменение изменяемой статической переменной небезопасны. Листинг 19-10 показывает, как объявить, получить доступ к и изменить изменяемую статическую переменную под названием COUNTER.

Имя файла: src/main.rs

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {COUNTER}");
    }
}

Листинг 19-10: Чтение из или запись в изменяемую статическую переменную - это небезопасно.

Как и в случае с обычными переменными, мы указываем изменяемость с помощью ключевого слова mut. Любой код, который читает или записывает из COUNTER, должен находиться внутри небезопасного блока. Этот код компилируется и выводит COUNTER: 3, как мы ожидаем, потому что это однопоточный код. Если несколько потоков обращаются к COUNTER, это, скорее всего, приведет к гонке данных.

При наличии изменяемой данных, доступной глобально,很难确保不存在数据竞争,这就是为什么 Rust 认为可变静态变量是不安全的原因。只要有可能,最好使用我们在第 16 章中讨论的并发技术和线程安全的智能指针,这样编译器就能检查不同线程对数据的访问是否安全。

Реализация небезопасного трейта

Мы можем использовать unsafe, чтобы реализовать небезопасный трейт. Трейт считается небезопасным, когда по крайней мере один из его методов имеет некоторое инвариантное свойство, которое компилятор не может проверить. Мы объявляем, что трейт небезопасный, добавляя ключевое слово unsafe перед trait и помечая реализацию трейта как unsafe также, как показано в Листинге 19-11.

unsafe trait Foo {
    // методы здесь
}

unsafe impl Foo for i32 {
    // реализации методов здесь
}

Листинг 19-11: Определение и реализация небезопасного трейта

С помощью unsafe impl мы обязуемся соблюдать инварианты, которые компилятор не может проверить.

В качестве примера вспомните маркерные трейты Send и Sync, о которых мы говорили в разделе "Расширяемая параллельность с трейтами Send и Sync": компилятор автоматически реализует эти трейты, если наши типы полностью состоят из типов Send и Sync. Если мы реализуем тип, содержащий тип, который не является Send или Sync, например, сырой указатель, и хотим пометить этот тип как Send или Sync, мы должны использовать unsafe. Rust не может проверить, что наш тип соблюдает гарантии, что его можно безопасно передавать между потоками или доступать из нескольких потоков; поэтому мы должны проводить эти проверки вручную и указать это с помощью unsafe.

Доступ к полям объединения

Последнее действие, которое работает только с unsafe, - это доступ к полям объединения. union похож на struct, но в конкретном экземпляре используется только одно объявленное поле за раз. Объединения в основном используются для взаимодействия с объединениями в коде на C. Доступ к полям объединения небезопасен, потому что Rust не может гарантировать тип данных, который в данный момент хранится в экземпляре объединения. Вы можете узнать больше о объединениях в Rust Reference по адресу *https://doc.rust-lang.org/reference/items/unions.html**.*

Когда использовать небезопасный код

Использование unsafe для использования одного из пяти сверхспособностей, которые мы только что обсудили, не является ошибкой и даже не вызывает недовольства, но правильность написания небезопасного кода требует большего внимания, так как компилятор не может гарантировать безопасность памяти. Если у вас есть причина использовать небезопасный код, вы можете hacerlo, и явная аннотация unsafe упрощает поиск источника проблем, когда они возникают.

Резюме

Поздравляем! Вы завершили лабораторную работу по Rust. Вы можете выполнить больше лабораторных работ в LabEx, чтобы повысить свои навыки.