From 0f4128df21e8b7f3ea894e18f31bbb3bbb653475 Mon Sep 17 00:00:00 2001 From: Akulij Date: Wed, 19 Nov 2025 08:48:35 +0700 Subject: [PATCH] vault backup: 2025-11-19 08:48:35 --- 4.2/2.md | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 167 insertions(+), 7 deletions(-) diff --git a/4.2/2.md b/4.2/2.md index e5ae0cd..e3f3db0 100644 --- a/4.2/2.md +++ b/4.2/2.md @@ -5,18 +5,18 @@ - [ ] Знает best practice для написания unsafe кода. - [ ] Можно использовать примеры из third party крейтов. -- [ ] Начать с проблемы, когда компилятор не может гарантировать безопасность по памяти (но без этого невозможно написать программу), возможно из ub -- [ ] Рассказать про причины ub -- [ ] Рассказать, чем является unsafe, ответственность на программисте, про ub (НЕ является избавлением от borrow checker) -- [ ] Рассказать про применение unsafe (взаимодействие с С, оптимизация (вспомнить небезопасную либу для бэкенда: rocket или actix), написание основы/базы языка) +- [x] Начать с проблемы, когда компилятор не может гарантировать безопасность по памяти (но без этого невозможно написать программу), возможно из ub ✅ 2025-11-19 +- [x] Рассказать про причины ub ✅ 2025-11-19 +- [x] Рассказать, чем является unsafe, ответственность на программисте, про ub (НЕ является избавлением от borrow checker) ✅ 2025-11-19 +- [x] Рассказать про применение unsafe (взаимодействие с С, оптимизация (вспомнить небезопасную либу для бэкенда: rocket или actix), написание основы/базы языка) ✅ 2025-11-19 - [ ] Определении функции unsafe если соблюдение инвариантов висит на пользователе (при написании такой функции смотреть - является ли сам интерфейс функции safe) - [ ] примеры из third party - [ ] Рассказать про бест практис при написании unsafe: - [ ] Лучший unsafe - отсутствующий unsafe - [ ] Уменьшение зоны unsafe (легче найти баг + проще просчитать ub) - - [ ] Safety + - [x] Safety ✅ 2025-11-19 - [ ] SAFETY - - [ ] assert_unsafe_precondition! + - [x] assert_unsafe_precondition! ✅ 2025-11-19 - [ ] ... - [ ] Практика - [ ] Лайфтаймы как помошники при взаимодействии с c abi @@ -78,7 +78,167 @@ fn main() { `*mut T` - для изменяемого указателя на тип T Внутри себя он, точно так же, как и референс, содержит адрес на память и опционально метадату, но сырой указатель не дает абсолютно никаких гарантий по памяти. Это нетипично для rust, но так необходимо при взаимодействии с внешними функциями, так для них компилятор никак не может гарантировать безопасность. ## Взаимодействие с сырым указателем -Если же сырой указатель не дает +С появлением сырых указателей возникает важный нюанс: раз компилятор не может контролировать корректность памяти, ответственность за её корректное использование будет ложится на программиста. Так как такой функционал необходим, но перечит памяти-безопасности языка, такой код отделяется в специальный unsafe блок. +## Unsafe +Ключевое слово unsafe означает потенциально не безопасный код с точки зрения компилятора. При написании блока unsafe ответственность за правильность и безопасность ложится на программиста. Но, при этом, rust внутри блока не превращается в некий аналог C. Компилятор просто разрешает программисту выполнять некоторые {{дополнительные действия}}[https://doc.rust-lang.org/reference/unsafety.html], в которые входят: +- Дереференс сырого указателя +- Вызов unsafe функции (к примеру, определенной через extern) +- Чтение/запись mut static переменной +- Имплиментация unsafe трейта +- Доступ к полям union +- Объявление extern блока (использовали его, когда объявляли C функции) +- Использование unsafe атрибута (к примеру, `#[unsafe(no_mangle)]`, использованный ранее) +- Вызов функции с таким `target_feature`, который в текущей функции не определен (к примеру, вызов функции, для которой требуется расширение x86_64 `avx` из функции, для вызова которой не требуются никакие расширения) + +## Undefined Behavior +ОР - Понимает причины UB и знает, как его не допустить. (откуда взялось, что значит и как может нанести вред работе программы) +Как думаете, что произойдет, если какой-нибудь из пунктов выше выполнится не так, как задумано? Возьмем пример: если вы пришли из C, то хорошо знаете функцию malloc: при вызове аллоцирует память нужного размера, но если выделить память не получиться, то вернется нулевой указатель. Если забыть проверить, что указатель не нулевой, и прочитать значение по нему, то что произойдет? Это поведение предсказать невозможно, а в стандарте языка (что C, что Rust) это поведение обозначено как **неопределенное поведение**. +**Неопределенное поведение** - свойство какого-то действия (список этих действий определяется языком), при выполнении которого последствия не определены стандартом языка. +Для rust этот список можно посмотреть {{тут}}[doc.rust-lang.org/reference/behavior-considered-undefined.html]. Хоть этот список и больше списка действий, возможных в блоке unsafe, все они являются последствиями неправильного выполнения unsafe действий. Поэтому главная задача программиста при использовании блока unsafe: cледить за тем, чтобы не возникали неопределенные поведения. +## Зачем все это? +При изучении undefined behavior, может возникнуть вопрос: нельзя ли определить это поведение, и упростить программистам жизнь? Ответом на этот вопрос является то, что создатели языков их хотели бы не создавать ub, но это не является возможным. К примеру, для деления на ноль поведение определить невозможно, ведь это поведение неопределено и в самой математике. Так же, невозможно определить и результат гонки данных, изученой в теме по многопоточности, ведь не известно, в каком порядке потоки будут исполнены. +Еще одним важным вопросом является отличие поведения платформ. К примеру, x86_64 при делении на ноль выдаст исключение. В это же время arm может выдать любое мусорное значение (в частности, на apple silicon выдает 0). +И последнее: производительность. Да, это поведение можно было бы определить, к примеру, сделав дополнительные runtime проверки, к примеру, вместо такой функции: +```rust +fn in_five(x: u32) -> u32 { + 5 / x +} +``` +Сделать такую: +```rust +fn in_five(x: u32) -> u32 { + if x == 0 { + panic!("attempt to divide by zero"); + } + 5 / x +} +``` +Но, из-за таких проверок код может стать гораздо медленнее, а нам нужна производительность. +Заметка: +*Если вы обратили внимание, что мы производим деление, где может возникнуть ub, в safe коде, вы хорошо поняли, когда код не безопасен. В реальности, создатели rust по умолчанию решили сделать деление безопасным, поэтому при каждом таком делении производится проверка на ноль, и в выходной программе функция будет выглядеть примерно так, как написано выше с проверкой на ноль. При этом, возможность выполнить деление без проверки возможно через std::intrinsics::unchecked_div, которая помечена как unsafe.* +## Написание unsafe кода +Давайте попробуем эксперимент: у нас есть игровой движок, который хранит список игровых энтити в векторе. И нам нужно за раз достать несколько из них по определенным индексам. Поэтому у нас в коде будет такая функция: +```rust +/// Returns mutable references to many indices at once +fn get_entities_at(entities: &mut [T], indices: [usize; N]) -> [&mut T; N] { + unsafe { + // преобразование референса в указатель, чтобы можно было вызвать + // get_unchecked_mut в цикле + let entities: *mut [T] = entities; + // просто аллокация памяти на стеке с неинициализироваными значениями + // Небольшая оптимизация, так как лишний раз не записываем нули, + // которые потом все равно будут перезаписаны + let mut output: [&mut T; N] = MaybeUninit::uninit().assume_init(); + for i in 0..N { + let index: usize = *indices.get_unchecked(i); + output[i] = (&mut *entities).get_unchecked_mut(index); + } + + output + } +} +``` + +Подумайте, какие ошибки допущены в этой функции? Какие неправильные данные можно в нее подать, что код станет не безопасным? + +*Тогл* +Ответ: +- индексы могут пересекаться, что приведет к содержанию нескольких мутабельных референсов на одно и то же значение +- индексы могут больше, чем длина массива, что приведет к невалидным референсам, либо чтению/записи из не аллоцированной памяти, что привидет к панике +Такой код - проблема для безопасности всего кода, но мы можем её решить: пометив ее, как unsafe, либо сделав её логику безопасной +### Путь первый - unsafe функция +Если функция может принимать данные, которые могут быть невалидными и вызывать undefined behavior, такая функция должна быть помечена ключевым словом unsafe перед fn, чтобы сказать пользователю функции, что при ее использовании нужно дополнительное внимание, а компилятору - что здесь есть не безопасные операции, и поэтому быть вызвана такая функция может только в блоке unsafe. Теперь интерфейс выглядит так: +```rust +/// Returns mutable references to many indices at once +unsafe fn get_entities_at(entities: &mut [T], indices: [usize; N]) -> [&mut T; N] { + // ... +} +``` +Но тут у пользователя функции возникнут вопросы: почему функция помечена unsafe? раз помечена unsafe, то какие данные я НЕ могу передавать? (не забывайте, что функция должна быть понятна по ее интерфейсу, а не исходному коду). Поэтому, в rust в сроке документации функции принято писать, почему функция не безопасна и то, какие инварианты должны быть соблюдены при вызове этой функции, чтобы не возникало неопределенное поведение. +Для нашего примера это будет выглядеть так: +```rust +/// Returns mutable references to many indices at once +/// Safety: +/// * indices do not overlap +/// * indices are not out of bound of entities array +unsafe fn get_entities_at(entities: &mut [T], indices: [usize; N]) -> [&mut T; N] { + // ... +} +``` +Теперь, пользователь функции будет знать, какие инварианты он будет должен соблюсти, чтобы код оставался безопасным. Но, к сожалению, люди не идеальны, и по случайности могут не соблюсти это инварианты. Поэтому хорошей практикой является проверка соблюдение этих условий в debug сборках, для этого используется макрос debug_assert (в будущем возможно станет стабильным более специфичный assert_unsafe_precondition или аналогичный макрос). Давайте напишем функцию проверки: +```rust +fn check_indicies_valid(indices: &[usize; N], len: usize) -> bool { + for index in indices { + // out of bound + if *index >= len { + return false; + } + // index overlap + if indices.iter().filter(|i| *i == index).count() > 1 { + return false; + } + } + true +} +``` +И добавим функцию +```rust +/// Returns mutable references to many indices at once +/// Safety: +/// * indices do not overlap +/// * indices are not out of bound of entities array +unsafe fn get_entities_at(entities: &mut [T], indices: [usize; N]) -> [&mut T; N] { + unsafe { + // Check that indicies do not overlap and are not out of bound + debug_assert!(check_indicies_valid(&indices, entities.len())) + // преобразование референса в указатель, чтобы можно было вызвать + // get_unchecked_mut в цикле + let entities: *mut [T] = entities; + // просто аллокация памяти на стеке с неинициализироваными значениями + // Небольшая оптимизация, так как лишний раз не записываем нули, + // которые потом все равно будут перезаписаны + let mut output: [&mut T; N] = MaybeUninit::uninit().assume_init(); + for i in 0..N { + let index: usize = *indices.get_unchecked(i); + output[i] = (&mut *entities).get_unchecked_mut(index); + } + + output + } +} +``` +Как пример похожей логики из стандартной библиотеки можно привести NonNull::new_unchecked (пример упрощен, оставлены только нужные для примера строки): +```rust +/// Creates a new `NonNull`. +/// # Safety +/// `ptr` must be non-null. +pub const unsafe fn new_unchecked(ptr: *mut T) -> Self { + unsafe { + assert_unsafe_precondition!( + check_language_ub, + "NonNull::new_unchecked requires that the pointer is non-null", + (ptr: *mut () = ptr as *mut ()) => !ptr.is_null() + ); + NonNull { pointer: ptr as _ } + } +} +``` +### Путь второй - оборачивание в безопасную обертку +unsafe как эффект, не должен распространяться бескончено вверх по графу вызова, иначе всё было бы unsafe. Поэтому в какой-то момент эти инварианты должны гарантироваться. Поэтому, давайте обернем наш код в безопасный, гарантировав инварианты: +```rust +fn get_entities_at(entities: &mut [T], indices: [usize; N]) -> Result<[&mut T; N], ()> { + if !check_indicies_valid(&indices, entities.len()) { + return Err(()); + } + + let entities = unsafe { + // тот же код, что и был + }; + + Ok(entities) +} +``` +Теперь гарантии происходят внутри самой функции. **Начать с проблемы, когда компилятор не может гарантировать безопасность по памяти (но без этого невозможно написать программу), возможно из ub** Допустим, на вход вашей функции