Паника в Rust: что я перестала повторять вслед за старыми статьями
Если честно, я долго жила с одной удобной мантрой. Открываешь любую статью про FFI на Rust — и практически наверняка натыкаешься на фразу: «паника, перешедшая границу C, — это неопределённое поведение». Я сама её повторяла, в том числе в своих же текстах. И вот недавно до меня дошло: эта формулировка устарела почти два года назад. Начиная с Rust 1.81, который вышел в сентябре 2024-го, такая ситуация не является неопределённостью — процесс гарантированно и предсказуемо завершится.
Но это была только первая трещина в моей вере в авторитет старых советов. Вторая оказалась даже более показательной, потому что я поймала себя на ней буквально на днях.
Миф о стройном бинарнике
Есть такая присказка, которая кочует по статьям лет десять: добавь в конфиг panic = "abort" — и размер исполняемого файла уменьшится процентов на десять. Я встречала её в релиз-нотах, в обсуждениях, в чужих пулреквестах. И сама, недолго думая, повторила её в одном ревью. А потом вдруг поймала себя на мысли: а я вообще хоть раз проверяла это утверждение?
Стыдно признаться, но нет. Поэтому я собрала одну и ту же программу тремя разными способами и полезла смотреть, что получилось. Вместо обещанных десяти процентов экономии вышло шесть десятых. Меньше одного процента. Причём самое забавное: в сборке, где раскрутка стека невозможна в принципе, всё равно лежали одиннадцать килобайт таблиц, которые существуют исключительно ради этой самой раскрутки.
И вот что интересно: пока я копалась в этих таблицах, мне пришлось пересмотреть собственное представление о том, что вообще происходит, когда Rust-программа паникует.
Паника — это не «всё упало»
Долгое время я думала о панике как о простом обрыве: программа споткнулась, вывела сообщение в консоль и умерла. На деле это полноценный механизм исключений, построенный на той же машинерии, что и в C++. И работает он гораздо интереснее, чем кажется на первый взгляд.
Раскрутка стека происходит не в один проход, как можно было бы ожидать, а в два. Каждое исключение носит с собой восемь байт метки b"MOZ\0RUST" — историческое наследство от Mozilla, где Rust когда-то развивался. А catch_unwind ловит ошибку вовсе не через personality routine, о которой так любят писать в учебниках, а через отдельный компиляторный интринсик. Спойлер: даже здесь всё не так, как принято думать.
Но самое любопытное — это дыра, о которой почти никто не упоминает. Она всплывает только при раскрутке между двумя независимо собранными копиями стандартной библиотеки. И защищает от неё канарейка, спрятанная прямо внутри структуры исключения. Я прочитала об этом в исходниках и первое время не могла поверить, что такие нюансы вообще существуют.
Что я вынесла из этой истории
Поначалу мне казалось, что достаточно один раз разобраться в механике паники — и можно смело цитировать проверенные источники. На деле всё оказалось сложнее. Я поняла, что даже самые уважаемые статьи стареют, а утверждения, которые повторяют годами, могут перестать соответствовать реальности.
• Проверяйте то, что собираетесь советовать другим. Особенно если советуете уже десять лет.
• Следите за версиями компилятора. То, что было правдой в 2016-м, не обязано оставаться правдой в 2025-м.
• Не верьте на слово даже собственным старым статьям. Особенно им.
Если бы кто-то сказал мне год назад, что я буду перепроверять такой базовый механизм, как паника, я бы рассмеялась. Но теперь я искренне рада, что потратила вечер на разбор полётов. По крайней мере, в следующий раз, когда увижу ту самую фразу про неопределённое поведение, я смогу ответить что-то более осмысленное, чем просто кивнуть.