Недавно читая хабр и смотря на вечные баталии 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)
Dhwtj
11.10.2025 17:17std::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
Жесть вообще
rsashka
11.10.2025 17:17Низкоуровневая реализация тут не причем, так как у Rust есть проблемы с доказательством самой модели безопасной работы с памятью.
kotan-11
11.10.2025 17:17Еще ни разу не видел ни одной статьи, которая рассказывает, как ссылочная модель Раста строит иерархии объектов в хипе. Как предотвращаются утечки памяти и дедлоки, как гарантируется иммутабельность шареных объектов. Я тут попытался реализовать на Расте вот этот бенчмарк DOM-структур данных, и пока застрял на невозможности иметь одновременно объект с единственным владельцем, на который можно ссылаться weak-указателями из других объектов. Кроме того для двухпроходной операции копирования с сохранением топологии графа мне нужен map(оригинальный объект->копия). И указатели в этой мапе должны потом присваиваться типизированным указателям -
Weak<RefCell<Card>>
иWeak<RefCell<dyn CardItem>>
. Хотя я даже не уверен, что мне нужны именно эти указатели. Не мобли бы вы подсказать, как в Расте реализуются структуры данных в хипе. Не обязательно в комментарии. Это могло бы стать хорошей темой для отдельной статьи.Namilsky Автор
11.10.2025 17:17И правда интересная идея для статьи, но пока что я сам не смотрел как реализуется иерархия объектов в куче. В ближайшее время поищу и постараюсь ответить
Lewigh
Это не вполне корректное утверждение.
unsafe{}
ничего не отключает а скорей наоборот включает внутри области возможность работать с небезопасными операциями: разыменование сырого указателя, вызов небезопасных функций, модификация статической переменной, доступ к полям union. Безопасность работает так же как и прежде, просто внутри области видимости программист берет на себя отвественность за возможное возникновение неопределенного поведения для вышеуказанных операций.Namilsky Автор
Спасибо за уточнение
kotan-11
"Безопасность работает так же как и прежде" - как безопасность может работать как и прежде, если можно свободно обращаться по любым указателям без никаких проверок, а доступ к полям union это самый настоящий reinterpret_cast? Поясните пожалуйста.
Lewigh
Я имел ввиду что никакие существующие правила и проверки не отключаются а работают также как и прежде и unsafe к примеру не позволяет брать несколько мутабельных ссылок, но разумеется работа с сырыми указателями в области unsafe может привести к UB.