Указатели

Unsafe-контекст и указатели

Последнее обновление: 14.05.2024

Одна из отличительных особенностей 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: *const i32 = #
    let n2: *mut i32 = &mut num;
}

Здесь определены два указателя - n1 и n2. n1 - константый указатель на значение типа i32. n2 - изменяемый указатель на значение типа i32.

Причем для создания изменяемого указателя сама переменная должна быть определена с ключевым словом mut:

let mut num = 5;

А для создания изменяемого указателя применяется изменяемая ссылка на эту переменную:

let n2: *mut i32 = &mut num;

Можно также использовать операцию преобразования as для получения из ссылки указателя:

fn main() {
    
	let mut num = 5;

    let n1 = &num as *const i32;
    let n2 = &mut num as *mut i32;
}

Стоит отметить, что сами указатели можно определять вне блока unsafe. Однако вне этого блока нельзя обратиться к значению в области памяти, на которую указывает указатель.

Что хранят указатели? Они хранят адрес на некоторую область в памяти. Например, получим значения выше определенных указателей:

fn main() {
    
	let mut num = 5;

    let n1: *const i32 = #
    let n2: *mut i32 = &mut num;

    println!("n1={:p}", n1);
	println!("n2={:p}", n2);
}

Для вывода адреса, который хранит указатель, применяется спецификатор :p - он указывает, что здесь будет выводиться значение указателя.

Консольный вывод программы в моей случае:

n1=0x38b50ffa94
n2=0x38b50ffa94

Оба указателя имеют одно и то же значение, потому что они оба указывают на один и тот же адрес в памяти - адрес переменной num.

Операция разыменования

С помощью операции разыменования (операция *) можно обратиться к значению по адресу, который хранится в указателе. Однако это обращение должно производиться в блоке unsafe:

fn main() {
    
	let mut num = 5;

    let n1: *const i32 = #
	unsafe{
	
		println!("{}", *n1);	// 5
	}
}

Используя полученное значение в результате операции разыменования мы можем присвоить его другой переменной:

fn main() {
    
	let num = 5;

    let num_pointer: *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 i32 = &mut num;
	unsafe{
		*num_pointer  = 29;		// изменяем значение в памяти, на которую указывает указатель
	}
	println!("num: {}", num);	// num: 29
}

Стоит отметить, что если мы хотим изменять значение в памяти, на которую указывает указатель, это должен быть именно изменяемый указатель. Константный указатель не позволяет изменять значение.

Второй момент: при этом изменяется значение не самого указателя - он по прежнему хранит адрес в памяти, а изменяется значение, которое храниться по этому адресу.

Передача указателю произвольного адреса

Следует отметить, что в принципе мы можем передавать указателю произвольный адрес:

fn main() {
    
	let addr = 0x38b50ff4usize;
    let p: *const i32 = addr;
	println!("Address: {:p}", p);
}

Адрес определяется как шестнадцатеричное число - в данном случае это адрес 0x38b50ff4, которое представляет тип usize. Но такие обращения к произвольным областям памяти программист делает на свой страх и риск.

Методы для получения указателей

Для получения указателей на те или иные объекты Rust может предоставлять специальные методы. Например, у вектора есть метод as_mut_ptr(), который позволяет получить указатель на первый элемент вектора. И с помощью этого указателя можно будет изменить элементы вектора. Например:

fn main() {

    let mut numbers = vec![1, 2, 3, 4, 5];

    let mut_ptr = numbers.as_mut_ptr(); // получаем указатель на первый элемент вектора

    let len = numbers.len();    // длина вектора

    // для использования указателя на вектор определяем блок unsafe
    unsafe {

        for i in 0..len {

            let current_value = *mut_ptr.add(i); // получаем элемент по указателю
            *mut_ptr.add(i) = current_value * 2; // изменяем элемент по указателю
        }
    }

    println!("{:?}", numbers);  // [2, 4, 6, 8, 10]
}

Здесь используем блок unsafe для прямого манипулирования элементами вектора с помощью указателей. Внутри unsafe-блока мы разыменовываем указатель и изменяем элементы.

unsafe-функции

unsafe-функции фактически представляют unsafe-контекст и могут вызываться только из unsafe-контекста. Объявления таких функций предваряется словом unsafe. Например:

fn main() {
    
    unsafe{
        test();     // test unsafe
    }
}
// unsafe-функция
unsafe fn test(){

    println!("test unsafe");
}

Поскольку unsafe-функции представляют unsafe-контекст, то внутри этих функций мы можем проводить стандартные операции с указателями:

fn main() {
    

	let mut num = 5;
    let num_pointer: *mut u32 = &mut num;
    unsafe{
        test(num_pointer);
    }
    println!("num: {}", num);   // num: 10
}

unsafe fn test(num_pointer: *mut u32){

    *num_pointer = *num_pointer * 2;
}

В данном случае в функцию test передаем полученный из ссылки указатель и увеличиваем по нему значение в 2 раза.

Стоит отметить, что в примере выше мы могли бы обойтись без unsafe, просто работая с изменяемыми ссылками:

fn main() {
    
	let mut num = 5;
    test(&mut num);
    
    println!("num: {}", num);   // num: 10
}

fn test(num: &mut u32){

    *num = *num * 2;
}

Вызов FFI-функций

Иногда может потребоваться взаимодействие с кодом, написанным на другом языке. Для этого в Rust есть ключевое слово extern, которое облегчает объявление и использование интерфейса внешних функций (Foreign Function Interface или вкратце FFI) - внешних функций, написанных на другом языке программирования.

Например, используем функции abs() и printf(), которые имеются в стандартной библиотеке языка C. Функции, объявленные внутри блоков extern, всегда небезопасно вызывать из кода Rust. Причина в том, что другие языки не обеспечивают соблюдение правил и гарантий Rust, и Rust не может их проверить, поэтому ответственность за обеспечение безопасности ложится на программиста. А подобные функции вызываются в unsafe-контексте:

extern "C" {    // объявление внешних функций
    fn abs(input: i32) -> i32;
    fn printf(format_str:&[u8;13]);
}

fn main() {
    unsafe {
        println!("Absolute value of -3: {}", abs(-3));
        printf(b"Hello World \n");
    }
}

В блоке extern "C" мы перечисляем имена и сигнатуры внешних функций из другого языка.

Статические переменные

Еще одна сфера применения блока unsafe касается работы со статическими переменными. Статические переменные в Rust представляют глобальные переменные, которые определяются с помощью ключевого слова static. Согласно условностям их названия определяются в верхнем регистре. Статические переменные могут хранить ссылки только со статическим временем жизни, что означает, что компилятор Rust может определить время жизни, и нам не требуется явно его аннотировать. Например:

static COUNTER: u32 = 1;

fn main() {
    println!("COUNTER: {COUNTER}"); // COUNTER: 1
}

В данном случае определена статическая переменная COUNTER, которая равна 1. И в функции main мы обращемся к ней и выводим ее значение на консоль.

Статические переменные могут также быть измененяемыми и неизменяемыми. В примере выше определялась неизменяемая статическая переменная. Доступ к такой переменной происходит как и доступ к обычной переменной в общем случае. Однако для доступа к изменяемой статической переменной и ее изменения треуется блк unsafe:

static mut COUNTER: u32 = 0;

fn add(n: u32) {
    unsafe {
        COUNTER += n;   // для изменения нужен блок unsafe
    }
}
fn main() {
    add(3);
    unsafe {
        println!("COUNTER: {COUNTER}"); // для получения нужен блок unsafe
    }
}

Объединения

Объединения (union_ аналогичны объединениям в языке Си, и для работы с ними также требуется unsafe-контекст. ОБъединение определяется аналогично структуре, только для его определения применяется ключевое слово union. Например:

union Symbol {
    letter: char,
    code: u32,
}

Здесь определено объединение Symbol, которое может хранить либо значение типа char, либо значение типа u32. То есть Symbol может хранить либо символ, либо его код. Создать значение перечисления можно вне unsafe-контекста, а вот обратиться к объединению можно только в unsafe-контексте. Например:

union Symbol {
    letter: char,
    code: u32,
}

fn main() {
    let s1 = Symbol{ letter: 'A' };
    unsafe {
        println!("s1: {}", s1.code);
    }

    let s2 = Symbol{ code: 65 };
    unsafe {
        println!("s2: {}", s2.letter);
    }
}

В данном случае определяем две переменных объединения. Первая переменная устанавливает поле letter - символ, а затем выводим его числовой код. В случае второй переменной, наоборот, сначала устанавливаем числовой код, а потом выводим символ, который представляет этот код. Консольный вывод:

s1: 65
s2: A
Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850