Одна из отличительных особенностей Rust состоит в том, что он может быть использоваться в низкоуровневом системном программировании, то есть он позволяет напрямую обращаться к операционной системе, писать драйверы и даже свои операционные системы. Однако такие возможности в первую очередь реализуются через так называемый "небезопасный код" (unsafe). Почему "небезопасный"? Потому что при компиляции такой код позволяет избежать проверок компилятора при использовании определенных возможностей языка Rust. С одной стороны, написание небезопасного кода дает нам большие возможности, в частности, как было сказано выше, использовать низкоуровневые возможности языка. Но с другой стороны, лишает нас большего контроля со стороны компилятора и соответственно обеспечивает меньшую защиту от потенциальных багов и ошибок.
Для написания "небезопасного кода" применяется блок кода unsafe:
unsafe{ // небезопасный код }
При этом в блоке unsafe также проверяется владение переменных, использование ссылок и ряд других стандартных проверок Rust. Ключевое слово unsafe лишь дает доступ к пяти возможностям языка Rust, которые не проверяются компилятором на безопасность памяти:
Указатели
unsafe-функции
Статические переменные
unsafe-трейты
Тип union
Указетели позволяют обращаться к значениями по определенным адресам в памяти. Для определения указателей применяется оператор разыменования (dereference operator) - оператор *. В Rust есть два типа указателей:
Неизменяемые или константны указатели. Они определяются в виде *const T
, где T
предоставляет конкретный тип.
Изменяемые указатели. Они определяются в виде *mut T
.
Создадим по указателю для каждого типа:
fn main() { let mut num = 5; let n1 = &num as *const i32; let n2 = &mut num as *mut i32; }
Здесь определены два указателя - n1
и n2
. n1
- константый указатель на значение типа i32
.
n2
- изменяемый указатель на значение типа i32
.
Для получения указателя на адрес переменной к ссылке на объект применяется операция as, которая преобразует объект к типу указателя.
Причем для создания изменяемого указателя сама переменная должна быть определена с ключевым словом mut:
let mut num = 5;
А для создания изменяемого указателя применяется изменяемая ссылка на эту переменную:
let n2 = &mut num as *mut i32;
Стоит отметить, что сами указатели можно определять вне блока unsafe. Однако вне этого блока нельзя обратиться к значению в области памяти, на которую указывает указатель.
Что хранят указатели? Они хранят адрес на некоторую область в памяти. Например, получим значения выше определенных указателей:
fn main() { let mut num = 5; let n1 = &num as *const i32; let n2 = &mut num as *mut i32; println!("n1={:p}", n1); println!("n2={:p}", n2); }
Для вывода адреса, который хранит указатель, применяется спецификатор :p - он указывает, что здесь будет выводиться значение указателя.
Консольный вывод программы в моей случае:
n1=0x38b50ffa94 n2=0x38b50ffa94
Оба указателя имеют одно и то же значение, потому что они оба указывают на один и тот же адрес в памяти - адрес переменной num
.
С помощью операции разыменования (операция *) можно обратиться к значению по адресу, который хранится в указателе. Однако это обращение должно производиться в блоке unsafe:
fn main() { let mut num = 5; let n1 = &num as *const i32; unsafe{ println!("{}", *n1); // 5 } }
Используя полученное значение в результате операции разыменования мы можем присвоить его другой переменной:
fn main() { let num = 5; let num_pointer = &num as *const i32; unsafe{ let number: i32 = *num_pointer; println!("number: {}", number); } }
Причем стоит отметить, что указатель возвращает значение того типа, на объект которого он указывает. Так, в данном случае указатель num_pointer
представляет указатель на объект типа i32, поэтому операция разыменования возвратить значение типа i32
И также используя указатель, мы можем менять значение по адресу, который хранится в указателе:
fn main() { let mut num = 5; let num_pointer = &mut num as *mut i32; unsafe{ *num_pointer = 29; // изменяем значение в памяти, на которую указывает указатель } println!("num: {}", num); // num: 29 }
Стоит отметить, что если мы хотим изменять значение в памяти, на которую указывает указатель, это должен быть именно изменяемый указатель. Константный указатель не позволяет изменять значение.
Второй момент: при этом изменяется значение не самого указателя - он по прежнему хранит адрес в памяти, а изменяется значение, которое храниться по этому адресу.
Следует отметить, что в принципе мы можем передавать указателю произвольный адрес:
fn main() { let addr = 0x38b50ff4usize; let p = addr as *const i32; println!("Address: {:p}", p); }
Адрес определяется как шестнадцатиричное число - в данном случае это адрес 0x38b50ff4
, которое представляет тип usize
.
Но такие обращения к произвольным областям памяти программист делает на свой страх и риск.