Добро пожаловать в главу 28 нашего курса по Rust — глубокое погружение в Unsafe Rust. В этой лекции мы подробно разберем, что такое unsafe-код, зачем он нужен, как с ним работать и какие подводные камни могут вас поджидать. Мы охватим все аспекты, указанные в плане курса: от базовых концепций до сложных примеров взаимодействия с C-библиотеками. Лекция будет самодостаточной, с избыточным количеством деталей, примерами кода, практическими советами и разбором упражнения. Поехали!
Rust — это язык программирования, который славится своей безопасностью, особенно в управлении памятью. Благодаря строгим правилам заимствования (borrowing) и времени жизни (lifetimes), компилятор Rust предотвращает такие ошибки, как гонки данных (data races), использование освобожденной памяти или разыменование нулевых указателей. Однако бывают ситуации, когда стандартные безопасные механизмы Rust либо слишком ограничивают, либо просто не подходят для выполнения определенных задач. Вот тут-то и приходит на помощь unsafe.
Unsafe Rust — это не "опасный" Rust, а скорее "Rust с отключенными предохранителями". Он позволяет разработчикам выполнять низкоуровневые операции, обходя некоторые проверки компилятора. Но с этим приходит и ответственность: вы сами должны гарантировать корректность кода. Unsafe-код открывает доступ к таким возможностям, как:
Эта лекция не просто расскажет вам, как использовать unsafe, но и научит делать это осознанно, избегая типичных ошибок. Мы разберем каждый пункт плана главы, добавим примеры, обсудим нюансы и дадим практические советы.
Unsafe Rust — это инструмент для особых случаев. Его стоит применять только тогда, когда безопасные средства языка не позволяют достичь цели. Вот основные сценарии, в которых unsafe становится необходимым:
Send
или Sync
, требуют ручного подтверждения безопасности через unsafe, если вы реализуете их для своих типов.static mut
) доступны только через unsafe, так как Rust не может гарантировать их безопасность в многопоточной среде.unsafe
, её можно вызвать только внутри unsafe-блока.
fn main() {
let mut value = 42;
unsafe {
// Здесь можно выполнять операции, недоступные в безопасном Rust
let ptr = &mut value as *mut i32;
*ptr = 100; // Прямое изменение через сырой указатель
}
println!("Value: {}", value); // Выведет: Value: 100
}
Unsafe не отключает безопасность полностью — он лишь снимает некоторые ограничения компилятора. Вы по-прежнему работаете в рамках системы типов Rust, но ответственность за соблюдение инвариантов ложится на вас.
Используйте unsafe только там, где это действительно необходимо. Если задачу можно решить безопасным кодом, даже с дополнительными усилиями, выбирайте безопасный путь — это снизит риск ошибок.
Сырые указатели — это фундамент unsafe Rust. Они позволяют напрямую манипулировать памятью, обходя правила заимствования и проверки времени жизни. В Rust есть два типа сырых указателей:
*const T
— неизменяемый сырой указатель.*mut T
— изменяемый сырой указатель.*mut T
на одну и ту же память.std::ptr::null()
создает нулевой указатель.*ptr
возможна только в unsafe-блоке.
fn main() {
let mut num = 5;
// Создаем сырые указатели из ссылок
let r1 = &num as *const i32; // Неизменяемый указатель
let r2 = &mut num as *mut i32; // Изменяемый указатель
unsafe {
println!("r1: {}", *r1); // Читаем через *const
*r2 = 10; // Пишем через *mut
println!("r2: {}", *r2);
}
println!("num: {}", num); // Выведет: num: 10
}
*mut T
на одну область памяти, что в безопасном Rust запрещено. Это опасно, если не контролировать доступ.null
— неопределенное поведение (UB)..add(offset)
для смещения или .is_null()
для проверки на null.*mut T
.
fn main() {
let ptr: *mut i32 = std::ptr::null_mut();
unsafe {
*ptr = 42; // UB! Разыменование нулевого указателя No newline at end of file
}
}
Используйте сырые указатели только внутри хорошо изолированных функций с четкими предусловиями (например, "указатель не null и указывает на действительные данные").
Foreign Function Interface (FFI) — это механизм, позволяющий Rust взаимодействовать с кодом на других языках, чаще всего с C, благодаря его широко поддерживаемому ABI (Application Binary Interface).
extern "C"
для указания C ABI.#[link]
указывает имя библиотеки.C-код (mylib.c
):
int add(int a, int b) {
return a + b;
}
Скомпилируйте в libmylib.so
:
gcc -shared -o libmylib.so -fPIC mylib.c
Rust-код:
#[link(name = "mylib")]
extern "C" {
fn add(a: i32, b: i32) -> i32;
}
fn main() {
let result = unsafe { add(3, 4) };
println!("Result: {}", result); // Выведет: Result: 7
}
i32
в Rust = int
в C.u8
= unsigned char
.*mut T
и *const T
соответствуют T*
в C.Создавайте безопасные обертки вокруг FFI-вызовов, чтобы пользователи вашего кода не сталкивались с unsafe напрямую.
Выравнивание (alignment) — это требование к адресам памяти, чтобы они были кратны определенному числу (обычно степени двойки). Например, i32
часто требует выравнивания на 4 байта, а f64
— на 8.
В Rust выравнивание типа можно узнать с помощью std::mem::align_of::<T>()
.
Пример:
use std::mem;
fn main() {
println!("Alignment of i32: {}", mem::align_of::<i32>()); // Обычно 4
println!("Alignment of f64: {}", mem::align_of::<f64>()); // Обычно 8
}
Для выделения памяти с учетом выравнивания используется модуль std::alloc
.
Пример выделения памяти:
use std::alloc::{alloc, dealloc, Layout};
fn main() {
let layout = Layout::new::<i32>(); // Layout для i32 (размер 4, выравнивание 4)
let ptr = unsafe { alloc(layout) } as *mut i32;
if ptr.is_null() {
panic!("Allocation failed");
}
unsafe {
*ptr = 42;
println!("Value: {}", *ptr);
dealloc(ptr as *mut u8, layout);
}
}
alloc
на null.Используйте Layout
для всех операций с памятью, чтобы гарантировать правильное выравнивание.
В unsafe-коде компилятор не проверяет инварианты безопасности. Это ваша задача. Вот ключевые инварианты, которые нужно поддерживать:
pub struct MyVec<T> {
ptr: *mut T,
len: usize,
capacity: usize,
}
impl<T> MyVec<T> {
pub fn new() -> Self {
MyVec {
ptr: std::ptr::null_mut(),
len: 0,
capacity: 0,
}
}
pub fn push(&mut self, value: T) {
if self.len == self.capacity {
// Логика перевыделения памяти (упрощено)
self.capacity = if self.capacity == 0 { 1 } else { self.capacity * 2 };
let layout = Layout::array::<T>(self.capacity).unwrap();
let new_ptr = unsafe { alloc(layout) } as *mut T;
if new_ptr.is_null() {
panic!("Allocation failed");
}
if !self.ptr.is_null() {
unsafe {
std::ptr::copy_nonoverlapping(self.ptr, new_ptr, self.len);
dealloc(self.ptr as *mut u8, Layout::array::<T>(self.capacity / 2).unwrap());
}
}
self.ptr = new_ptr;
}
unsafe {
self.ptr.add(self.len).write(value);
self.len += 1;
}
}
pub fn get(&self, index: usize) -> Option<&T> {
if index < self.len {
unsafe { Some(&*self.ptr.add(index)) }
} else {
None
}
}
}
impl<T> Drop for MyVec<T> {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe {
dealloc(self.ptr as *mut u8, Layout::array::<T>(self.capacity).unwrap());
}
}
}
}
fn main() {
let mut vec = MyVec::new();
vec.push(1);
vec.push(2);
println!("vec[0] = {:?}", vec.get(0)); // Some(1)
}
Рассмотрим более сложный пример с выделением памяти в C.
C-код (mylib.c
):
#include <stdlib.h>
int* create_int_array(size_t size) {
int* arr = malloc(size * sizeof(int));
if (arr == NULL) return NULL;
for (size_t i = 0; i < size; i++) {
arr[i] = i;
}
return arr;
}
void free_int_array(int* arr) {
free(arr);
}
Компиляция:
gcc -shared -o libmylib.so -fPIC mylib.c
Rust-код:
#[link(name = "mylib")]
extern "C" {
fn create_int_array(size: usize) -> *mut i32;
fn free_int_array(arr: *mut i32);
}
fn main() {
let size = 5;
let arr_ptr = unsafe { create_int_array(size) };
if arr_ptr.is_null() {
panic!("Failed to allocate memory");
}
unsafe {
for i in 0..size {
println!("arr[{}] = {}", i, *arr_ptr.add(i));
}
free_int_array(arr_ptr);
}
}
is_null()
обязательна.Напишите программу, которая вызывает C-функцию для вычисления факториала.
factorial.c
)
unsigned long long factorial(unsigned int n) {
if (n == 0) return 1;
return n * factorial(n - 1);
}
Компиляция:
gcc -shared -o libfactorial.so -fPIC factorial.c
#[link(name = "factorial")]
extern "C" {
fn factorial(n: u32) -> u64;
}
// Безопасная обертка
fn safe_factorial(n: u32) -> Option<u64> {
if n > 20 { // Предотвращаем переполнение
return None;
}
Some(unsafe { factorial(n) })
}
fn main() {
let n = 5;
match safe_factorial(n) {
Some(result) => println!("Factorial of {} is {}", n, result),
None => println!("Input too large"),
}
}
u64
переполнится при больших n
.safe_factorial
делает код безопасным для пользователей.Unsafe Rust — это мощный инструмент для низкоуровневой работы, но он требует дисциплины и внимания к деталям. Мы рассмотрели, как использовать сырые указатели, взаимодействовать с C, управлять памятью и проверять безопасность вручную. Главное — минимизировать unsafe-код, изолировать его и документировать.
Теперь вы готовы применять unsafe там, где это нужно, и избегать его там, где можно обойтись безопасными средствами. Удачи в освоении Rust!