Небезопасный Rust

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

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

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

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

Мы переключаемся на небезопасный Rust, используя ключевое слово unsafe и создаём новый блок, который содержит небезопасный код. Есть четыре действия, которые вы можете предпринять в небезопасном Rust, которые вы не можете сделать безопасном. Мы называем это «небезопасные суперспособности». Мы не видели большинство этих функций, так как ими можно воспользоваться с unsafe.

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

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

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

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

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

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

Рассмотрим материал главы 4 ещё раз. Там мы говорили о ссылках. Мы изучили, что компилятор всегда проверяет действительность ссылок. Небезопасный Rust предоставляет два новых типа для работы со ссылками. Также как ссылки, мы имеет изменяемые и неизменяемые указатели (*const T и *mut T). В контексте сырых ссылок "неизменяемость" значит, что указателям нельзя присваивать значения непосредственно после разыменования.

Отличия сырых ссылок от умных указателей:

  • разрешается игнорирование правил заимствования, можно иметь изменяемые и неизменяемы указатели или множество указателей на одну и ту же область памяти.
  • нет гарантии действительности указателя
  • возможность иметь null-ссылки
  • нет реализации автоматической очистки памяти

Код 19-1 демонстрирует, как создавать сырые ссылки из обычных ссылок:


# #![allow(unused_variables)]
#fn main() {
let mut num = 5;

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

код 19-1: создание сырых ссылок из ссылок

*const T является неизменяемым сырым указателем. *mut T является изменяемым сырым указателем. Мы создали сырые указатели с помощью ключевого слова as приведя обычные изменяемые и неизменяемые ссылки к этим типам.

Код 19-2 показывает, как создать сырой указатель на конкретный адрес памяти. Это, конечно, весьма опасное действие делать такие присваивания, но это возможно:


# #![allow(unused_variables)]
#fn main() {
let address = 0x012345usize;
let r = address as *const i32;
#}

код 19-2: создание сырых указателей на определённый адрес памяти

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


# #![allow(unused_variables)]
#fn main() {
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: разыменование сырых указателей в блоке unsafe

Создание указателей разрешено. Только при попытке доступа к объекту могут быть какие-либо проблемы.

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

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

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

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


# #![allow(unused_variables)]
#fn main() {
unsafe fn dangerous() {}

unsafe {
    dangerous();
}
#}

Если мы попытаемся вызвать функцию dangerous без блока unsafe, мы получим ошибку:

error[E0133]: call to unsafe function requires unsafe function or block
 --> <anon>:4:5
  |
4 |     dangerous();
  |     ^^^^^^^^^^^ call to unsafe function

Вставив unsafe-блок вокруг вызова функции dangerous, мы сообщаем компилятору, что мы понимаем, что используем небезопасную функцию, мы знаем как её использовать правильным образом и мы соблюли все необходимые условия.

Создание безопасных абстракций вокруг небезопасного кода

Рассмотрим пример из стандартной библиотеки. Рассмотрим функцию split_at_mut и посмотрим как мы можем реализовать её сами. Этот безопасный метод определён в изменяемом срезе. Метод получает срез и разделяет его на два:


# #![allow(unused_variables)]
#fn main() {
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

Эту функцию нельзя реализовать, используя только безопасный Rust. Попытка реализации может выглядеть примерно так (19-5). Для просторы мы реализуем функцию, а не метод и только для целых чисел:

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

    assert!(mid <= len);

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

код 19-5: попытка реализации функции split_at_mut

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

Далее, функция возвращает два среза в кортеже: первый от 0 до значения, второй от значения до конца среза.

При попытке компиляции данной функции вы получите следующее сообщение об ошибке:

error[E0499]: cannot borrow `*slice` as mutable more than once at a time
 --> <anon>:6:11
  |
5 |     (&mut slice[..mid],
  |           ----- first mutable borrow occurs here
6 |      &mut slice[mid..])
  |           ^^^^^ second mutable borrow occurs here
7 | }
  | - first borrow ends here

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

Код 19-6 демонстрирует, как можно использовать unsafe-блок, сырой указатель и вызов небезопасной функции для реализации целей функции split_at_mut:


# #![allow(unused_variables)]
#fn main() {
use std::slice;

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

    assert!(mid <= len);

    unsafe {
        (slice::from_raw_parts_mut(ptr, mid),
         slice::from_raw_parts_mut(ptr.offset(mid as isize), len - mid))
    }
}
#}

код 19-6: использование небезопасного кода для реализации split_at_mut

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

Благодаря небезопасному методу slice::from_raw_parts_mut мы смогли реализовать задуманный в предыдущем решении алгоритм.

Т.к. срезы являются безопасными языковыми конструкциями, после их создания ими можно пользоваться в безопасной части кода. Функция slice::from_raw_parts_mut является небезопасной, т.к. она получает сырой указатель (без проверки на действительность). Метод offset сырого указателя также не является безопасным, т.к. он работает с данными не проверяя их действительность. Мы заключили вызовы данных функций в unsave-блок.

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

В отличии от функции slice::from_raw_parts_mut код 19-7 скорей всего не будет работать. Этот код получает данные по адресу памяти и создаёт срез длинной 10000:


# #![allow(unused_variables)]
#fn main() {
use std::slice;

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

let slice = unsafe {
    slice::from_raw_parts_mut(r, 10000)
};
#}

код 19-7: создание среза по адресу памяти

Использование такого кода может привести к непредсказуемым последствиям.

extern функции вызывающие внешний код являются небезопасными

Иногда появляется необходимость вызвать код написанный на другом языке программирования. Для этой цели существует специальное ключевое слов extern, которое облегчает создание и использование интерфейса внешних функций (Foreign Function Interface (FFI)). Код 19-8 демонстрирует, как установить связь с С-функцией abs. Функции в блоках extern всегда являются небезопасными:

Filename: 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-функции написанной на другом языке программирования

С помощью блока extern "C" мы сообщаем какую функцию мы хотим вызвать. "C" определяет интерфейс какого языка будет использован application binary interface (ABI). Наиболее часто используемым интерфейсом является интерфейс языка C.

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

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

Ключевое слово extern также используется для создания интерфейса, который позволяет вызывать функции Rust из других языков программирования. Вместо блока extern мы можем добавить к описанию функции это ключевое слово. Также необходимо добавить аннотацию #[no_mangle], чтобы сообщить компилятору не анализировать данную функцию. В следующем примере функция call_from_c будет доступна для кода языка программирования Си (С):


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

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

Получение доступа и внесение изменений в изменяемую статическую переменную

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

Глобальные переменные в Rust называют статическими (static). Код 19-9 демонстрирует определение и использование статической переменной имеющий тип строковый срез:

Filename: src/main.rs

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

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

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

static переменные походи на константы. Их имена по договорённости также необходимо писать с большой буквы SCREAMING_SNAKE_CASE. Кроме того у таких переменных обязательно необходимо (must) указывать тип. В данном случае это &'static str. Только ссылки с модификатором 'static могут быть сохранены в статической переменной. По этой причине нет необходимости аннотировать такие переменные модификатором времени жизни.

Доступ к неизменяемым переменным является безопасным. Значения в статических переменных имеют фиксированный адрес в памяти. В отличии от них константам разрешается дублировать свои данные.

Другое отличие статических переменных от констант - они могут быть изменяемыми. Доступ и изменения статических переменных являются небезопасными. Пример кода 19-10 показывает как объявлять, получать доступ и изменять изменяемую статическую переменную COUNTER:

Filename: 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. Каждый раз, когда необходим доступ к такой переменной, используется unsafe-блок. Данный код компилируется и выводит COUNTER: 3. В многопоточной среде при доступе к такой переменной эффект гонок вполне вероятен.

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

Реализация небезопасных типажей

С помощью unsafe также можно реализовать небезопасные типажи. При этом типаж и его реализации помечаются unsafe:


# #![allow(unused_variables)]
#fn main() {
unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}
#}

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

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

As an example, recall the Sync and Send marker traits from Chapter 16, and that the compiler implements these automatically if our types are composed entirely of Send and Sync types. If we implement a type that contains something that’s not Send or Sync such as raw pointers, and we want to mark our type as Send or Sync, that requires using unsafe. Rust can’t verify that our type upholds the guarantees that a type can be safely sent across threads or accessed from multiple threads, so we need to do those checks ourselves and indicate as such with unsafe.

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