273 lines
25 KiB
Markdown
273 lines
25 KiB
Markdown
- [ ] Умеет писать unsafe код и безопасные обёртки для него.
|
||
- [x] Понимает причины UB и знает, как его не допустить. (откуда взялось, что значит и как может нанести вред работе программы) ✅ 2025-11-19
|
||
- [x] для автора - проверка ОРа квизами, включая примеры кода. ✅ 2025-11-19
|
||
- [ ] Умеет проектировать безопасный интерфейс для unsafe кода.
|
||
- [x] Знает best practice для написания unsafe кода. ✅ 2025-11-19
|
||
- [x] Можно использовать примеры из third party крейтов. ✅ 2025-11-19
|
||
|
||
- [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
|
||
- [x] Определении функции unsafe если соблюдение инвариантов висит на пользователе (при написании такой функции смотреть - является ли сам интерфейс функции safe) ✅ 2025-11-19
|
||
- [x] примеры из third party ✅ 2025-11-19
|
||
- [ ] Рассказать про бест практис при написании unsafe:
|
||
- [x] Лучший unsafe - отсутствующий unsafe ✅ 2025-11-19
|
||
- [x] Уменьшение зоны unsafe (легче найти баг + проще просчитать ub) ✅ 2025-11-19
|
||
- [x] Safety ✅ 2025-11-19
|
||
- [x] SAFETY ✅ 2025-11-19
|
||
- [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)
|
||
}
|
||
```
|
||
Теперь гарантии происходят внутри самой функции.
|
||
|
||
**Начать с проблемы, когда компилятор не может гарантировать безопасность по памяти (но без этого невозможно написать программу), возможно из 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 |