Недавно читая хабр и смотря на вечные баталии C++ и Rust разработчиков я подумал что-то вроде "А так ли хорошо управление памятью в Rust как о нем говорят?". С этим сегодня мы и попробуем разобраться.

Не будем тянуть, Rust разработчики постоянно говорят о необычный системе "владения" (ownership) которая и управляет памятью в этом языке. Она необычная, пусть работать с ней не сильно сложно, первое время осознавать отличия было достаточно больно. (Для меня как сишника точно). В предпросмотре я упоминал плюсы но в статье их примеров не будет, в них все вроде итак понятно, просто руками выделяешь и освобождаешь память. Я не буду судить что хуже или лучше, как никак это две абсолютно разных технологии которые используются для достижения разных целей.

Что из себя представляет данная система?

Система владения и жизненных циклов в Rust - одно из основных отличий от других языков и главный апостол его memory-safe повадок. Ее суть кроется в трех основных понятиях: ссылках, жизненных циклах и владении. Она разработана для того чтобы писать безопасный для памяти код без сборщика мусор и без ручных malloc() и free()

Правила концепции:

  • Каждое значение имеет владельца — переменную, которая отвечает за освобождение ресурса.

  • В каждый момент времени у ресурса может быть только один владелец.

  • Когда владелец выходит из области видимости, ресурс освобождается.

Эти правила статические и проверяются во время компиляции, в бинарнике их нет. Это мы еще рассмотрим позже. Пока что это не должно быть особо понятно если вы не пользовались подобной системой в прошлом.

❯ Правило первое, владение

fn main() {
    let greet = String::from("Привет Хабр!");
    println!(greet);
}
  • Переменная greet - владелец строки

  • Когда закрывается фигурная скобка (greet выходит из области видимости) память автоматически освобождается. Но это не сборщик мусора, просто вызов drop(greet)

❯ Правило второе, один владелец на одно время

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 теперь недействителен. 

    println!("{}", s2);
    // println!("{}", s1); -> s1 больше не владелец, ошибка
} // drop(s1); 

Ничего сложного, то есть тут переменная передается другому владельцу, а не копируется. К примеру в Python в аналогичном коде вывод был бы два раза hello. Предотвращается двойное освобождение памяти, s1 больше использовать нельзя. Но вообще клонировать тоже можно.

❯ Правило третье, заимствование

fn print_length(s: &String) {
    println!("Length: {}", s.len());
}

fn main() {
    let s = String::from("hello");
    print_length(&s);
    println!("{}", s); // s всё ещё действителен
}

Это по сути указатели, просто интегрированные во всю эту систему

  • &s - это указатель, он памятью не владеет

  • После вызова print_length владелец (s) остается жив.

  • Гарантируется что ссылка не будет жить дольше владельца (Фикс висячих указателей короче)

  • В изменяемых заимствованиях может существовать только одна mut &T. (А это фикс гонок данных)

Более низкий уровень

Что же конкретно происходит когда мы создаем переменную сложного типа данных?

fn main() {
  let some_text = String::from("Привет Хабр!"); // Не путать String с &str!
}

Раз уж на то пошло давайте посмотрим что происходит в ассемблере

rustc --emit asm string_mem.rs 

В сравнении с тем же С где ассемблера на вышло 23 строки в Rust получится примерно 850, я отбросил не важные нам проверки и другое, оставил только самое важное.

_ZN7habr_ex4main17h514a1b0e6cb23afdE:
	.cfi_startproc
	subq	$24, %rsp
	.cfi_def_cfa_offset 32
	movq	%rsp, %rdi
	leaq	.Lanon.ba62d6a9bc58e2b5279d52a507255ab6.11(%rip), %rsi
	movl	$22, %edx
	callq	_ZN76_$LT$alloc..string..String$u20$as$u20$core..convert..From$LT$$RF$str$GT$$GT$4from17hcbb3a0442af0ee98E
	movq	%rsp, %rdi
	callq	_ZN4core3ptr42drop_in_place$LT$alloc..string..String$GT$17h2373330cd786f1baE
	addq	$24, %rsp
	.cfi_def_cfa_offset 8
	retq

Думаю всем уже должно быть ясно что основные концепции работы с памятью тут не отличаются, все те-же стек и куча, статическая и динамическая память, сложные и простые типы данных.

Вообще строка хранит ссылку на метаданные и само содержание строки в куче, это что-то вроде структуры такой формы

// Псевдокод 
String {
    ptr: *mut u8,   // указатель на данные в куче
    len: usize,     // длина строки
    capacity: usize // ёмкость буфера
}
  • Но вернемся к ассемблеру, название функции ZN7habrex4main17h514a1b0e6cb23afdE это зашифрованное (mangled) название. Манглирование нужно из-за допуска нескольких одинаковых названий функций с разными параметрами, также оно содержит хэш типов и путь к функции. Здесь невооруженным взглядом видно что это habr_ex::main (Название_файла::Название_функции).

  • .cfi_startproc - это директива отладки, помогает дебаггеру понимать где сохраняются регистры, как восстанавливать стек и т.п.

  • subq $24, %rsp выделяет 24 байта в стеке (уменьшая указатель стека %rsp). Это место нужно для локальной переменной, о которой надеюсь вы еще помните,some_text.

  • .cfi_de_cfa_offset 32 - опять стек, нужно для того чтобы "обновить описание" стека для отладчика, теперь CFA = %rsp + 32.

  • Следующие же 3 строки требуются для передачи аргументов вызов String::from()

    	movq	%rsp, %rdi
    	leaq	.Lanon.ba62d6a9bc58e2b5279d52a507255ab6.11(%rip), %rsi
    	movl	$22, %edx

    Мы используем SysV ABI amd64. Первая строка значит что-то вроде %rdi = %rsp , это мы показываем куда конкретно поместить ту структуру с метаданными и саму строку. (%rsp - Stack Pointer Register. Важно пояснить что сама строка хранится в куче, а мета структура (приводил пример выше) лежит в стеке)

    leaq .Lanon...(%rip), %rsi - загружает адрес строкового литерала "Привет Хабр!" в %rsi.

    movl $22, %edx - длина строки в байтах: 22 (в UTF-8, «Привет Хабр!» столько и занимает)

  • callq ZN76$LT$alloc.....2af0ee98E это вызов функции

    <alloc::string::String as core::convert::From<&str>>::from

    То есть уже создает сам String в куче и копирует туда данные той строки что мы указали изначально.

  • Последние же строчки вызывают drop - это та функция которая вызывается для того чтобы освободить ресурсы если владелец выйдет из области видимости.

Кстати еще существует такая штука как unsafe{}. Она отключает всю безопасность и позволяет управлять памятью как тебе угодно. Сырые указатели (как в Си), некоторые FFI, обращения к union полям, инлайн ассемблер asm!().

Много чего можно быть unsafe, например функция, трейт, конкретный блок кода и т.п. Нужно чаще всего в программировании микроконтроллеров и других подобных задачах. Но особого внимания я думаю там уделять нечему.

❯ Как ownership уходит

Rust имеет LLVM компилятор, то есть исходники на rust превращаются в промежуточный MIR который мы можем поглядеть точно также как и выше посмотрели ассемблер.

rustc --emit mir string_mem.rs  

Это похожий код просто там уходит ownership и явно указываются инструкции.

// WARNING: This output format is intended for human consumers only
// and is subject to change without notice. Knock yourself out.
// HINT: See also -Z dump-mir for MIR at specific points during compilation.
fn main() -> () {
    let mut _0: ();
    let _1: std::string::String;
    scope 1 {
        debug some_text => _1;
    }

    bb0: {
        _1 = <String as From<&str>>::from(const "Привет Хабр!") -> [return: bb1, unwind continue];
    }

    bb1: {
        drop(_1) -> [return: bb2, unwind continue];
    }

    bb2: {
        return;
    }
}

В ассемблере есть функции с аналогичными названиями, но эта тема еще на три статьи. А на эту статью я уже все.

Выводы

В общем, на мой взгляд система заслуживает внимания но это определенно не панацея. Как и в конфликтах Rust разработчиков и Плюсовиков, чаще всего там не прав никто. Rust не замена плюсам, скорее аналог. У каждого свои плюсы.

Также хотел бы попросить о обратной связи. Я не сильно опытен в написании статей, так что это важно. Если было интересно прошу поставить плюсик.

Лично на мой взгляд немного подкачал с форматированием ну и чуть чуть контента маловато.

Комментарии (8)


  1. Lewigh
    11.10.2025 17:17

    Кстти еще существует такая штука как unsafe{}. Она отключает всю безопасность и позволяет управлять памятью как тебе угодно. 

    Это не вполне корректное утверждение.unsafe{} ничего не отключает а скорей наоборот включает внутри области возможность работать с небезопасными операциями: разыменование сырого указателя, вызов небезопасных функций, модификация статической переменной, доступ к полям union. Безопасность работает так же как и прежде, просто внутри области видимости программист берет на себя отвественность за возможное возникновение неопределенного поведения для вышеуказанных операций.


    1. Namilsky Автор
      11.10.2025 17:17

      Спасибо за уточнение


    1. kotan-11
      11.10.2025 17:17

      "Безопасность работает так же как и прежде" - как безопасность может работать как и прежде, если можно свободно обращаться по любым указателям без никаких проверок, а доступ к полям union это самый настоящий reinterpret_cast? Поясните пожалуйста.


      1. Lewigh
        11.10.2025 17:17

        "Безопасность работает так же как и прежде" - как безопасность может работать как и прежде, если можно свободно обращаться по любым указателям без никаких проверок, а доступ к полям union это самый настоящий reinterpret_cast? Поясните пожалуйста.

        Я имел ввиду что никакие существующие правила и проверки не отключаются а работают также как и прежде и unsafe к примеру не позволяет брать несколько мутабельных ссылок, но разумеется работа с сырыми указателями в области unsafe может привести к UB.


  1. Dhwtj
    11.10.2025 17:17

    std::string_view get_name() {
        std::string s = "John";
        return s;  // неявное преобразование string -> string_view
    }
    int main() {
        auto name = get_name();
        std::cout << name;  // UB, крэш или мусор
    }

    И

    // Rust - не скомпилируется
    fn get_name() -> &str {
        let s = String::from("John");
        &s  // error: cannot return reference to local variable
    }

    https://stackoverflow.com/questions/63166194/implicitly-convert-string-to-string-view

    Жесть вообще


  1. rsashka
    11.10.2025 17:17

  1. kotan-11
    11.10.2025 17:17

    Еще ни разу не видел ни одной статьи, которая рассказывает, как ссылочная модель Раста строит иерархии объектов в хипе. Как предотвращаются утечки памяти и дедлоки, как гарантируется иммутабельность шареных объектов. Я тут попытался реализовать на Расте вот этот бенчмарк DOM-структур данных, и пока застрял на невозможности иметь одновременно объект с единственным владельцем, на который можно ссылаться weak-указателями из других объектов. Кроме того для двухпроходной операции копирования с сохранением топологии графа мне нужен map(оригинальный объект->копия). И указатели в этой мапе должны потом присваиваться типизированным указателям - Weak<RefCell<Card>> и Weak<RefCell<dyn CardItem>> . Хотя я даже не уверен, что мне нужны именно эти указатели. Не мобли бы вы подсказать, как в Расте реализуются структуры данных в хипе. Не обязательно в комментарии. Это могло бы стать хорошей темой для отдельной статьи.


    1. Namilsky Автор
      11.10.2025 17:17

      И правда интересная идея для статьи, но пока что я сам не смотрел как реализуется иерархия объектов в куче. В ближайшее время поищу и постараюсь ответить