2025-11-19 09:45:31 +07:00

282 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

- [ ] Умеет писать unsafe код и безопасные обёртки для него.
- [ ] Понимает причины UB и знает, как его не допустить. (откуда взялось, что значит и как может нанести вред работе программы)
- [ ] для автора - проверка ОРа квизами, включая примеры кода.
- [ ] Умеет проектировать безопасный интерфейс для unsafe кода.
- [ ] Знает best practice для написания unsafe кода.
- [ ] Можно использовать примеры из third party крейтов.
- [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)
- [x] Safety ✅ 2025-11-19
- [ ] SAFETY
- [x] assert_unsafe_precondition! ✅ 2025-11-19
- [ ] ...
- [ ] Практика
- [ ] Лайфтаймы как помошники при взаимодействии с c abi
- [ ] Drop
Почему unsafe внутри unsafe fn:
фн показывает, что инварианты должен соблюдать пользователь функции
ансейф блок же показывает скоуп, где нужно искать баги (где они и будут тк анйсейф операция)
подводный камень:
при ансейф баг может возникать в любом из блоков анйсефа, даже если он возникает в другом месте
ub: из-за различий архитектру цп и ос и для оптимизаций
еслм возникает баг, значит неправильно реализованы инварианты в одном из unsafe блоков с которым взаимодействовали (даже в совершенно другом месте)
Возможно рассказать еще про это все?
- pointers
- nonnull
- phantomdata (инвариантность/ковариантность и тд)
- unsafecell
- send/sync + unsafe trait markers (мб практический пример: написание собстенного mutex)
- unsafe traits
-
При описании бест практис про Safety, привести пример, почему нужно (а еще потому, что при изменении логики функции самим тоже нужно следить, какие инварианты висят за пользователем)
Практика: подумайте над тем, какие инварианты тут должны быть соблюдены и какие из них не соблюдены. или пропробовать найти несоблюденный инвариант и "взломать" программу?)
допустим, нам нужно дя создания сокета узнать, можем ли мы использовать tcp/udp/icmp/etc, для этого используем сискол WSAEnumProtocolsA
# Start
В прошлом уроке мы научились использовать C функции в своем rust коде. Давайте же расширим свои знания, и научимся взаимодействовать с таким кодом. В прошлом уроке в практическом задании вам встречался такой код:
```rust
include!(concat!(env!("OUT_DIR"), "/bindgen.rs"));
use std::ffi::{CStr, CString};
const TEST_JSON: &CStr = c"{
\"meaning_of_life\": 42
}";
fn main() {
let json: *mut cJSON = unsafe { cJSON_Parse(TEST_JSON.as_ptr()) };
let json_str = unsafe { cJSON_PrintUnformatted(json) };
let json_str = unsafe { CString::from_raw(json_str) };
let json_str = json_str.to_str().unwrap();
assert_eq!(json_str, r#"{"meaning_of_life":42}"#);
let meaning_of_life = unsafe { cJSON_GetObjectItem(json, c"meaning_of_life".as_ptr()) };
let meaning_of_life = unsafe { cJSON_GetNumberValue(meaning_of_life) };
println!("Meaning of life: {}", meaning_of_life);
assert_eq!(meaning_of_life, 42f64);
}
```
У json обозначен не встречавшийся до этого тип `*mut _`
## Сырые указатели
Вы уже неоднократно пользовались референсами в своем коде. В этих типах компилятор следит за временем жизни, чтобы не обратится к памяти после освобождения, а так же за валидностью памяти. Но, когда дело доходит до вызова внешней функции компилятор уже будет не способен отследить время жизни и валидность памяти. Поэтому в rust есть аналогичный тип - сырые указатели. Он унаследован по синтаксису и смыслу от своих предшественников (в частности от С), поэтому его описание выглядит так:
`*const T` - для неизменяемого указателя на тип T
`*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<T, const N: usize>(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<T, const N: usize>(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<T, const N: usize>(entities: &mut [T], indices: [usize; N]) -> [&mut T; N] {
// ...
}
```
Теперь, пользователь функции будет знать, какие инварианты он будет должен соблюсти, чтобы код оставался безопасным. Но, к сожалению, люди не идеальны, и по случайности могут не соблюсти это инварианты. Поэтому хорошей практикой является проверка соблюдение этих условий в debug сборках, для этого используется макрос debug_assert (в будущем возможно станет стабильным более специфичный assert_unsafe_precondition или аналогичный макрос). Давайте напишем функцию проверки:
```rust
fn check_indicies_valid<const N: usize>(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<T, const N: usize>(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<T, const N: usize>(entities: &mut [T], indices: [usize; N]) -> Result<[&mut T; N], ()> {
if !check_indicies_valid(&indices, entities.len()) {
return Err(());
}
let entities = unsafe {
// тот же код, что и был
};
Ok(entities)
}
```
Теперь гарантии происходят внутри самой функции.
Спустя некоторое время вашему коллеге поступило ТЗ, что эта функция должна выдавать индексы только по энтити, которые видимы (пускай будет трейт с методом is_visible). Изменим код:
**код**
Но останется ли такой код безопасным? Читающему код придётся снова разбирать, какие инварианты нужно соблюсти, чтобы код оставался безопасным. Но этого можно было бы избежать, если бы мы обозначали, какие инварианты мы соблюли. По примеру Safety у функций, у unsafe блоков обозначают соблюдены инварианты словом SAFETY:
```rust
**Kod**
```
Но, оно нужно не только для этого. При написании такого комментария программист лишний раз подумает, какие инварианты нужно соблюсти. А ещё, упрощает нахождение бага, так как можно сравнить условия, когда возникает баг, с теми условиями, что прописаны в комментариями. И баг по безопасности будет возникать возникать только в unsafe блоках. Правда, при всплытии в одном месте, причиной возникновения может стать unsafe блок в совершенно другом месте. Поэтому, хотелось бы сузить количество кода для поиска бага, поэтому старайтесь уменьшать блоки unsafe.
### Хорош тот unsafe, которого нет
Лучшее, что можно сделать с unsafe: это не писать его. Если ту же логику можно можно написать в safe rust, то пишите её в safe rust (разве что, можно как небольшое исключение сказать про случай, когда с unsafe мы можем получить хороший прирост производительности в критическом для производительности месте). Стоит что раз подумать, прежде чем писать unsafe код.
**Начать с проблемы, когда компилятор не может гарантировать безопасность по памяти (но без этого невозможно написать программу), возможно из ub**
Допустим, на вход вашей функции
**Рассказать про причины ub**
- как в математике есть неопределенности (к примеру, для деления на ноль), так и в языках программирования тоже есть свои неопределенности.
Именно для этого и было создана такая вещь, как неопределённое поведение.
Неопределено, значит никогда не случается. Значит компилятор может оптимизировать код только соблюдая верность тех случаев, что не являются ub. (пример с nullptr дереф)
**Рассказать, чем является unsafe, ответственность на программисте, про ub (НЕ является избавлением от borrow checker)**
**Как про отдельную вещь рассказать про вызов unsafe функции**
**Рассказать про применение unsafe (взаимодействие с С, оптимизация (вспомнить небезопасную либу для бэкенда: rocket или actix), написание основы/базы языка)**
**Практика по пути**
**Мб рассказать про кейс с лайфтаймами**
- [ ] Определении функции unsafe если соблюдение инвариантов висит на пользователе (при написании такой функции смотреть - является ли сам интерфейс функции safe)
- [ ] примеры из third party
- [ ] Рассказать про бест практис при написании unsafe:
- [ ] Лучший unsafe - отсутствующий unsafe
- [ ] Уменьшение зоны unsafe (легче найти баг + проще просчитать ub)
- [ ] Safety
- [ ] SAFETY
- [ ] assert_unsafe_precondition!
- [ ] ...
- [ ] Практика
- [ ] Лайфтаймы как помошники при взаимодействии с c abi
- [ ] Drop