Хабрахабр

[Из песочницы] Что значит unsafe в Rust?

Представляю вашему вниманию перевод статьи "What Is Rust's unsafe?" автора Nora Codes. Привет, Хабр!

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

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

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

Безопасность в Python

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

Во-первых, много типов должны быть "специальными". У этого есть два следствия. Вместо этого интерпретатор CPython имеет их внутреннюю реализацию. Например, невозможно реализовать эффективный список или словарь на чистом Python. Во-вторых, доступ к внешним функциям (функциям, реализованным не на Python), называемый интерфейсом внешней функции, требует использования специального модуля ctypes и нарушает гарантии безопасности языка.

В каком-то смысле это значит, что все, что написано на Python, не гарантирует безопасного доступа к памяти.

Безопасность в Rust

Это означает, что фундаментальные структуры данных в Rust, такие как Vec, VecDeque, BTreeMap и String, реализованы на языке Rust. Rust тоже предоставляет безопасность, но вместо реализации небезопасных структур на C, он предоставляет уловку: ключевое слово unsafe.

Вы спросите: "Но, если Rust предоставляет уловку против его гарантий безопасности кода, и стандартная библиотека реализована с использованием этой уловки, разве не все в Rust будет считаться небезопасным?"

Давайте разберем это подробнее. Одним словом, уважаемый читатель, — да, именно так, как это было в Python.

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

  • Разыменовывать указатель, указывающий не на тот тип, о котором знает компилятор. Это значит, что не существует никаких указателей на null (потому что они никуда не указывают), никаких ошибок выхода за границы и/или ошибок сегментации (segmentation faults), никаких переполнений буфера. Но также это значит и то, что нет никаких использований после освобождения памяти или повторного освобождения памяти (потому что освобождение памяти считается разыменованием указателя) и никакого каламбура типизации.
  • Иметь несколько изменяемых ссылок на объект или одновременно изменяемые и неизменяемые ссылки на объект. То есть если у вас есть изменяемая ссылка на объект, у вас может быть только она, а если у вас есть неизменяемая ссылка на объект, он не изменится, пока она у вас сохраняется. Это означает невозможность вызвать гонку данных в безопасном Rust, что является гарантией, которую большинство других безопасных языков предоставить не могут.

Rust кодирует эту информацию в системе типов или используя алгебраические типы данных, такие как Option для обозначения существования/отсутствия значения и Result<T, E> для обозначения ошибки/успеха, или ссылки и их время жизни, например, &T vs &mut T для обозначения общей (неизменяемой) ссылки и эксклюзивной (изменяемой) ссылки и &'a T vs &'b T для различения ссылок, которые являются корректными в различных контекстах (такое, как правило, опускается, так как компилятор достаточно умный, чтобы сам понять это).

Примеры

Конкретнее, my_struct does not live enough. Например, следующий код не будет компилироваться, так как он содержит висячую ссылку. Иными словами, функция вернет ссылку на что-то, что уже не существует, и поэтому компилятор не сможет (и, на самом деле, даже не знает, как) скомпилировать это.

fn dangling_reference(v: &u64) -> &MyStruct ; // Возвращаем ссылку на локальную переменную my_struct. return &my_struct; // Память из-под my_struct освобождается (снимается со стека).
}

Этот код делает то же самое, но он пытается обойти эту проблему, размещая значение в куче (Box — это имя базового умного указателя в Rust).

fn dangling_heap_reference(v: &u64) -> &Box<MyStruct> { let my_struct = MyStruct { value: v }; // Помещаем структуру в Box с выделением места в куче и перемещением ее туда. let my_box = Box::new(my_struct); // Возвращаем ссылку на локальную переменную my_box. return &my_box; // my_box снимается со стека. Эта переменная "владеет" my_struct и поэтому ответственна за освобождение памяти из-под нее, // так что память из-под MyStruct тоже освобождается.
}

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

fn no_dangling_reference(v: &u64) -> Box<MyStruct> { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct); // Возвращаем локальную переменную my_box по значению. return my_box; // Никакая память не освобождается. Вызывающий код теперь ответственен за управление памятью в куче, // выделенной в этой функции; она почти точно будет освобождена автоматически // когда Box<MyStruct> выйдет из области действия в вызывающем коде, за исключением случая возникновения двойной паники.
}

Например, разрешено с точки зрения компилятора:
Некоторые плохие вещи не запрещены в безопасном Rust.

  • вызвать deadlock в программе
  • совершить утечку произвольно большого объема памяти
  • не суметь закрыть хендлы файлов, соединения баз данных или крышки ракетных шахт

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

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

Иными словами, unsafe у функции значит "тебе надо все проверить", а на блоке кода — "я уже все проверил".

Как отмечено в книге The Rust Programming Language, код в блоке, отмеченном ключевым словом unsafe, может:

  • Разыменовывать указатель. Это ключевая "суперспособность", которая позволяет реализовывать двусвязные списки, hashmap, и другие фундаментальные структуры данных.
  • Вызывать небезопасную функцию или метод. Больше об этом ниже.
  • Получать доступ или модифицировать изменяемую статическую переменную. Статические переменные, у которых область видимости не контролируется, не могут быть статически проверены, поэтому их использование небезопасно.
  • Имплементировать небезопасный типаж (trait). Небезопасные типажи используются для того, чтобы помечать, гарантируют ли конкретные типы определенные инварианты. Например, Send и Sync определяют, может ли тип пересылаться между границами потоков или быть использован несколькими потоками одновременно.

Добавьте слово unsafe, и компилятор станет ругаться вдвое больше, потому что ему не нравится использование unsafe там, где оно не требуется. Помните те примеры с висячими указателями, приведенные выше?

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

Например, мы могли бы сделать то же самое, что и в примере no_dangling_reference, но с беспричинным использованием unsafe:

fn manual_heap_reference(v: u64) -> *mut MyStruct { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct); // Преобразовать Box в старый добрый указатель. let struct_pointer = Box::into_raw(my_box); return struct_pointer; // Ничего не разыменовывается; эта функция просто возвращает указатель. // MyStruct остается там же в куче.
}

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

fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct = unsafe { Box::from_raw(my_pointer) }; // Печатает "Value: 1337" println!("Value: {}", my_boxed_struct.value); // my_boxed_struct выходит из области видимости. Эта переменная теперь владеет памятью в куче, поэтому // она освобождает память из-под MyStruct
}

Box — безопасная абстракция на основе указателей, потому что она предотвращает распространение указателей повсюду. После оптимизации этот код эквивалентен простому возвращению Box. Например, следующая версия main приведет к двойному освобождению памяти (double-free).

fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct_1 = unsafe { Box::from_raw(my_pointer) }; // DOUBLE FREE BUG! let my_boxed_struct_2 = unsafe { Box::from_raw(my_pointer) }; // Печатает "Value: 1337" дважды. println!("Value: {}", my_boxed_struct_1.value); println!("Value: {}", my_boxed_struct_2.value); // my_boxed_struct_2 выходит из области видимости. Он владеет памятью в куче, поэтому // он освобождает память из-под MyStruct. // Затем my_boxed_struct_1 выходит из области видимости. Он также владеет памятью в куче, // поэтому он также освобождает память из-под MyStruct. Это double-free bug.
}

Так что такое безопасная абстракция?

Box безопаснее *mut T, так как он не может привести к двойному освобождению памяти, проиллюстрированному выше. Безопасная абстракция — это такая абстракция, которая использует систему типов для предоставления API, который не может быть использован для нарушения гарантий безопасности, которые были упомянуты выше.

Это указатель с подсчетом ссылок — не допускающая изменение ссылка на данные, расположенные в куче. Другой пример — тип Rc в Rust. Так как она допускает множественный одновременный доступ к одной области памяти, она должна предотвращать изменение для того, чтобы считаться безопасной.

Если вам нужна потокобезопасность, вам придется использовать тип Arc (Atomic Reference Counting), который имеет штраф к производительности из-за использования atomic значений для подсчета ссылок и предотвращения возможных гонок данных в многопоточных средах. В дополнение к этому она не потокобезопасна.

Если бы они сделали это, это было бы необоснованно: ложное обещание безопасности. Компилятор не позволит вам использовать Rc там, где вы должны использовать Arc, потому что создатели типа Rc не отметили его как потокобезопасный.

Когда необходим небезопасный Rust?

Например, в двусвязном списке отсутствие изменяемых ссылок на одни и те же данные (у следующего элемента и предыдущего элемента) полностью лишает его пользы. Небезопасный Rust необходим всегда, когда необходимо произвести операцию, нарушающую одно из тех двух правил, описанных выше. С unsafe имплементатор двусвязного списка может написать код, используя указатели *mut Node и затем инкапсулировать это в безопасную абстракцию.

Часто микроконтроллеры используют набор регистров, чьи значения определяются физическим состоянием устройства. Другой пример — работа со встраиваемыми системами. Как правило, такие крэйты инкапсулируют состояние в прозрачных безопасных обертках, которые по возможности копируют данные, или же используют другие техники, обеспечивающие гарантии компилятора. Мир не может остановиться, пока вы берете &mut u8 с такого регистра, поэтому для работы с крэйтами поддержки устройства необходим unsafe.

Но до тех пор, пока есть возможность убедиться в поддержании безопасных инвариантов перед тем, как пользователь безопасного (то есть не отмеченного unsafe) кода коснется чего-то, все в порядке. Иногда необходимо провести операцию, которая может привести к одновременному чтению и записи, или небезопасному доступу к памяти, и именно здесь нужен unsafe.

Несмотря на то, что это сделано несколько иначе, чем небезопасная имплементация основных структур данных в Python, реализация Vec, Hashmap и т. Мы приходим к сделанному раньше утверждению — да, полезность кода на Rust основана на небезопасном коде. должна использовать манипуляции указателями в какой-либо степени. д.

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

В C это бремя лежит на каждом программисте. В Python бремя проверки безопасности манипуляций с памятью лежит только на разработчиках интерпретаторов и пользователях интерфейсов внешних функций.

Это очевидно, так как внутри такого кода инварианты должны поддерживаться вручную, и поэтому необходимо стремиться к наименьшему объему такого кода в библиотеке или коде приложения. В Rust оно лежит на пользователях ключевого слова unsafe. Поэтому, если в вашем коде на Rust возникают segfaults, то вы нашли либо ошибку в компиляторе, либо ошибку в нескольких строках вашего небезопасного кода. Небезопасность обнаруживается, выделяется и обозначается.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть