Прим. пер.: Кто-то должен был сделать перевод этой статьи, несмотря на то, что она достаточно стара (2015 год), поскольку она показывает очень важную особенность работы с памятью в Rust — с помощью безопасного (не помеченного как unsafe) кода можно создавать утечки памяти. Это должно отрезвлять народ, верящий во всемогущность borrow checker'а.
Спойлер — внутри про невозможность отслеживания циклических ссылок, а также старые болезни некоторых типов из std, на момент перевода благополучно вылеченные.
Несмотря на наличие в Книге главы про безопасный код (спасибо за напоминание ozkriff), а также разъяснительной статьи русскоязычного сообщества (спасибо за напоминание mkpankov), я решил выполнить перевод для наглядной демонстрации серьезности непонимания возможностей управления памятью Rust.
Вероятнее всего, данная статья ранее не переводилась по причине весьма специфических терминов автора, которые НЛО в тираж не пропустит. По этой причине перевод не совсем дословный. Словно гром посреди ясного неба, баг с циклическими ссылками в std::thread::JoinGuard поверг сообщество в пучину экзистенциального ужаса. Если вы следите за новостями и уже в курсе утечкокалипсиса, данную секцию можете смело пропускать и переходить к следующей. Если вы следите за моими комментариями в сообществе, то в принципе вам что-либо новое здесь читать нечего, можете смело закрывать окно и возвращаться к дальнейшему изучению моих комментариев. Итак, баг: Крайне серьезное заявление, поскольку все используемые API помечены как безопасные, что (по изначальному ожиданию авторов языка — прим.пер.) обязано исключить такое поведение безопасного кода в принципе. Основной фокус приходится на API thread::scoped, который создает поток с доступом к окну стека другого потока, безопасность которого гарантирована компилятором статически. Идея в основе безопасности состоит в JoinGuard, возвращаемом thread::scoped, чей деструктор блокирует выполнение ожидающего и владеющего стеком потока; соответственно, ничто более, переданное в ожидаемый поток, не может пережить данный JoinGuard. Это позволяет реализовывать довольно полезные вещи вроде: Здесь выполняется некая полезная работа для каждого из десяти элементов массива в отдельном потоке (ну, типа как. Здесь просто пример, представьте там полезную нагрузку сами.). Магическим образом Rust способен статически гарантировать безопасность доступа к данным даже не зная, что именно происходит в дочерних потоках! Ему нужно просто видеть массив (Vec) JoinGuard'ов потоков, которые заимствуют v, соответственно v не может умереть раньше, чем потоки завершатся. (А если точнее, Rust и про массив не знает особо, ему достаточно, что threads заимствует v, даже если threads вообще пуст). И это очень прикольно. И увы, неверно. Предполагается, что деструкторы (здесь и далее — реализации Drop — прим.пер.) гарантированно исполняются в безопасном, не помеченном unsafe коде. Что уж говорить, ваш покорный слуга, как и многие в сообществе выросли с верой в данный постулат. В конце концов, у нас даже есть отдельная функция, которая специально удаляет связь с элементом без вызова деструктора, mem::forget, и во имя данного постулата она намеренно помечена как unsafe! Как оказалось, это всего лишь отголоски старых вариантов API. На самом деле mem::forget в стандартной библиотеке в некоторых местах используется в безопасном коде. И если почти все из этих мест были позже отнесены как ошибки имплементации, то одно оставшееся достаточно фундаментально. Это rc::Rc. Rc у нас — умный указатель со счетчиком ссылок. Он весьма прост — клади в конструктор Rc::new данные для разделения между несколькими ссылками да пользуйся. Клонируешь (clone()) Rc — счетчик увеличивается. Удаляешь (drop()) клон — счетчик уменьшается. Все работает только за счет borrow checker'а — отслеживание времен жизни гарантирует, что ссылки на данные освобождены до момента удаления Rc, через который они (ссылки) получены.  Сам по себе Rc вполне торт — благодаря принципу работы счетчика мы работает только со ссылками на внутренности, сами данные остаются на месте в плане освобождения памяти, прочесть что-то вне этой памяти невозможно… кроме случаев внутренней изменяемости (internal mutability). Хотя раздача копий ссылок на данные в Rust подразумевает неизменяемость (данных, не ссылок), сами данные все же возможно изменять, в качестве исключения из правил. Для этих целей служит Cell, чьи внутренности можно менять даже в случае множественного доступа. Собственно, поэтому типы Cell помечены как непотокобезопасные. Для потоков есть sync::Mutex. А теперь смешаем Rc с drop и напишем безопасный forget: Похоже на лютую ересь. Вкратце, можно создавать циклически зависимые считаемые ссылки с помощью Rc и RefCell. Итогом будет тот факт, что деструктор типа, положенного внутрь Leak<T> никогда не будет вызван, хотя ссылок на Rc у нас больше нет. В принципе невызов деструктора сам по себе не страшен, можно же завершить программу извне или работать в бесконечном цикле. Но не в данном случае — Rc нам сказал, что вызывал деструктор. Это даже можно проверить: Оно работает. Теперь надо бы разобраться в проблеме из исходного тикета, вызвавшего панику. Уменьшим пример из тикета до следующего: Мы раскрыли неопределенное поведение.
И это бесспорно дыра в стандартной библиотеке Rust, поскольку невозможность use-after-free и гонок данных вроде как обещана и статически гарантирована, а мы тривиальным примером добились и того, и другого. Возникает вопрос — что именно сделано неверно. Исходный тикет предсказуемо бросает тень на thread::scoped, так как создан разработчиком компилятора, который 100% в курсе возможности утечки деструктора из "безопасного" кода. Это немедленно породило волну фрустрации в сообществе — до этого момента они видели утечки только в unsafe. thread::scoped стабилизирован. mem::forget помечен как unsafe, только лишь потому, что никак, никогда, ни в коем случае и совсем нельзя течь не из-под unsafe!
После разбирательства в истоках бага было выявлено стечение обстоятельств: при которых, и никак более, баг проявляется. Это немедленно привело к появлению личностей (увы, меня тоже), требующих в той или иной форме запрета существания любых комбинаций данных обстоятельств на уровне компилятора. Чтобы вы понимали уровень отчаяния — даже появился настойчивый запрос на внутренний типаж Leak, помечающий возможность утечки деструктора в safe. Внимание. Релиз 1.0 через три недели, внедрять что-либо вышеупомянутое просто нет времени. Выводы сделаны единственно верные — вынуть mem::forget из unsafe (поскольку он ничего скрыто опасного не делает), переделать thread::scoped. Соответствующие RFC: Обратите внимание: Rust никогда не гарантировал отсутствие утечек в принципе. Утечки, равно как и гонки данных — не имеющий четких границ концепт, потому фактически непредотвратимый. Если сложить что-то в HashMap и никогда его не спросить — тоже в своем роде утечка. Аллоцировать что-то на стеке, после чего запустить бесконечеый цикл — туда же. Любой случай складывания данных в кучу с последующим забыванием об их существовании это самая что ни на есть утечка, которая почему-то именно в данном виде вызывает у людей панический ужас. Ошибкой можно считать данные, у которых забыли вызвать деструктор во время обычного "безопасного" выполнения, потому что с точки зрения статического анализа этих данных не существует. Таким образом, я был удручен и растерян. Работа с коллекциями в моем понимании всегда подразумевала гарантированное выполнение деструкторов. Были прекрасные возможности как собрать дескриптор для сколь угодно сложного и зависимого типа, действующий прозрачно, но прячущий все внутренности, так и разобрать его корректно при удалении. При работе с временами жизни Rust это гарантировало, однако, что неопределенное состояние данных типа вне этого дескриптора статически невыделяемо! То есть Rust еще более беспечен, чем C!
Каноничный этому пример — Vec::drain_range: Тут даже корректно работает unwind, класс! А деструктор не работает, смотрите: Не круто. Я порылся в коде коллекций стандартной библиотеки на предмет чего-то, что надеется на деструктор для обеспечения безопасности, и, к счастью, ничего не нашел (что, безусловно, совсем не значит, что его там нет). Если конкретней, искал я нечто, укладывающееся в следующую иерархию гадостей текущего деструктора: В общем случае, следует отодвигать зависимость от деструктора настолько дальше согласно этой иерархии, насколько практически возможно, с исключением кода из последней категории из всех API, не помеченных unsafe.
Данная иерархия базируется на предположении, что текущий деструктор в работающей программе — баг. Ни более, ни менее, код приложения обязан предполагать, что в произвольном его месте или при адекватных в нем условиях утечек не будет. В качестве обычной практики в Rust сторонние библиотеки единолично обязаны подтверждать невозможность утечек. Факт наличия кода, возможно провоцирующего утечку, в API, наподобие Rc, не должен расцениваться как баг API или его реализации. Более того, если утечка возможна лишь при определенных, созданных конечным пользователем условиях. Несмотря на извлечение mem::forget из unsafe и возможность его вызовов в безопасном коде, в общих случаях необходимо помнить, что последствия его вызова небезопасны. (скорее всего этот участок и есть причина, почему данная статья мало цитируется — прим.пер.) Как же нас обезопасить использование Vec::drain_range и передвинуть его выше по иерархии? Добавлением следующей строки в конструктор drain_range. Все вот так просто! Теперь, если DrainRange утечет, у нас утекут деструкторы элементов, которые нужно повынимать, поскольку мы совсем потеряли сдвигаемые в начало вектора значения. Это вполне относимо к категории "неопределенное, но безопасное состояние", ну, еще прибавляются возможные и разнообразные другие утечки. Все еще хреново, но уже большой прогресс относительно use-after-free, происходившего ранее! Это то, что я называю паттерном "Уж Лучше Обделаться" (Pre-Poop Your Pants (PPYP) pattern) — спасти ситуацию в конструкторе хотя бы частично, если деструктор не включится. Почему такое стремное название? Мм. Я привык представлять жизненный цикл между конструктором и деструктором как процесс пищеварения. Что-то можно съесть (конструктор), обработать и сходить в туалет (деструктор). Если деструктор пропал, туалет уже не светит никогда. В зависимости от жизненной ситуации (деструктора), возможны разнообразные последствия: Собственно, паттерн "Уж Лучше Обделаться" является следующим отклонением от обычного процесса пищеварения: если вы что-то неожиданное съели, будьте готовы к "преждевременным неприятностям". Обычно неприятности не настигают, и вы успеваете дойти до туалета согласно плану его посещения. Но если они все же настигли, уж лучше обделаться и засмущаться, чем упасть в бессознании и очнуться на столе у хирурга. Паттерн выделяет следующее: Да кстати. Vec::drain_range удалось спасти!