Назад к блогу

Bun переписали с Zig на Rust: как устроен новый код и что это дало

Bun переписали с Zig на Rust: как устроен новый код и что это дало

Bun переписали с Zig на Rust: полностью автоматизированный перенос 535 тысяч строк занял четыре месяца и обошёлся примерно в 165 тысяч долларов токенов. В статье разбирается, что именно изменилось в рантайме и как теперь устроен код — от устройства бандлера до работы с сырыми указателями и обратными ссылками.

Bun — JavaScript/TypeScript-рантайм, бандлер и пакетный менеджер — переписан с Zig на Rust. О переносе объявил создатель проекта Джарред Самнер, статья об этом датирована 20 сентября 2026 года, а сама Rust-реализация вошла в релиз Bun v1.4.0 в августе 2026 года. Исходный объём составлял 535 496 строк Zig; после переноса тестовый набор проходил уже более миллиона строк кода.

Работа заняла 4 месяца вместо предполагавшегося года и обошлась примерно в $165 000 токенов. Перенос выполнялся автоматизированно и целиком: Zig транспилировался в Rust (в том числе с использованием unsafe-кода, который предполагалось отрефакторить позже) и проверялся существующим набором тестов из более чем миллиона утверждений.

Что именно изменилось

Рантайм теперь написан на Rust и работает на JavaScriptCore под капотом. В проекте присутствуют файлы конфигурации Rust-инструментария (Cargo.toml, Cargo.lock, rust-toolchain.toml, rustfmt.toml). Известно, что код транспилировался в Rust.

После переписывания вышли три версии:

  • Bun 1.4 — релиз, в котором Bun переписан на Rust. В него вошли встроенная автоматизация headless-браузера (Bun.WebView), Bun.Image, Bun.markdown, JSON5, JSONL, Terminal и cron API, совместимость с Node.js 26.3.0 с 1 517 новыми проходящими тестами, параллельные test и run, Windows ARM64 и opt-in глобальное виртуальное хранилище для установок до 7× быстрее.
  • Bun v1.4.1 — исправляет 202 проблемы (236 👍) и добавляет HTTP/2 в Bun.serve, потоковую запись тел Response на диск, самодостаточные workspace node_modules, bun install --offline и --prefer-offline, паузу и возобновление WebSocket-соединений, crypto.argon2, tree-shaking через динамический импорт, улучшенную конвертацию CommonJS в ESM для default-импортов, --compile --bytecode при кросс-компиляции, меньшие и быстрее стартующие скомпилированные исполняемые файлы, до 9× быстрее чтение и запись Buffer, 2× быстрее асинхронный контекст выполнения, более быстрый запуск и множество исправлений совместимости с Node.js.
  • Bun v1.4.2 — исправляет две регрессии, влияющие на Elysia и асинхронный контекст выполнения, зависание в @discordjs/ws, декодирование CMYK JPEG в Bun.Image, редкий сбой JIT и сбой GC на musl.

Как устроен код бандлера

Основной объект сборки владеет транспайлером, графом парсинга и LinkerContext, и именно его поля читаются при линковке. Единица работы на пуле потоков — по одной на исходный файл: она несёт путь, содержимое или файловые дескрипторы, загрузчик, JSX-прагму и обратную ссылку на основной объект сборки.

Компонент, связывающий модули, хранит указатель на граф парсинга и собственный граф линковки, обратные ссылки на лог и Resolver, а также счётчик незавершённых задач pending_task_count: AtomicU32 — он нужен, чтобы понимать, когда объект можно безопасно освободить. Транспайлер — конфигурационный объект с состоянием логирования и разрешения модулей.

Взаимодействие устроено так: основной объект сборки вызывает линковку у своего линкера, передавая себя как сырой указатель, а после линковки запускает параллельную генерацию чанков из линкера и чанков. Транспайлер связывает логи через set_log, который синхронизирует лог самого транспайлера, линкера и резолвера.

Обратные ссылки и сырые указатели

Связи между компонентами реализованы как типизированные обёртки над сырыми указателями. Обратная ссылка на объект-владелец хранится как BackRef. Ссылка на конфигурацию разрешения модулей помечена как обратная ссылка в конфигурацию транспилятора; вместо сырого указателя выбрана обёртка, чтобы обращаться к ней через безопасное разыменование, а не разыменовывать указатель вручную. Ссылка на фреймворк хранится как обёртка над сырым указателем, а граф парсинга — как сырой указатель, доступ к которому идёт через метод с проверкой, что указатель не нулевой.

Связь между контекстом линковки и объектом сборки реализована через container_of: функция вычисляет адрес объекта сборки по полю, указывающему на контекст линковки, а вариант для чтения возвращает тот же адрес без права записи. В задаче парсинга обратный указатель на объект сборки хранится как обёртка с безопасным разыменованием, а в результате — как обёртка, записанная с правом записи при инициализации задачи. Право записи означает, что указатель получен из изменяемой ссылки и потому позволяет менять объект сборки, а не только читать его.

Безопасность разделяемого доступа к контексту линковки обоснована тем, что сырые указатели — это обратные ссылки в объект сборки и транспилятор, чьи времена жизни строго переживают все параллельные секции, а записи из разных потоков идут в непересекающиеся слоты структуры массивов.

Арены и времена жизни

Парсинг и линковка опираются на арену, живущую дольше прохода сборки. Сборщик хранит её как ссылку на арену потока, а доступ к ней отдаёт через аксессор, возвращающий ссылку на арену графа. Задачи парсинга создаются при инициализации и размещаются в арене специальным размещающим вызовом, который возвращает изменяемую ссылку с временем жизни, отвязанным от заимствования самого сборщика, — чтобы вызывающий мог сразу перезаимствовать его изменяемо. Контракт зафиксирован в комментарии: слот арены закреплён и переживает все выданные изменяемые ссылки, освобождаясь только при полном сбросе арены.

Пути интернируются в арене до записи в результат и карту соответствия путей индексам источников — иначе ключ мог бы повиснуть после освобождения списка точек входа. В линкере задачи для source map создаются с обратной ссылкой на контекст линковки, у которой время жизни стёрто до 'static, потому что задачи джойнятся до дропа самого контекста. Транспайлер предоставляет аксессоры для файловой системы, лога и загрузчика переменных окружения, возвращающие ссылки с отвязанным временем жизни, чтобы их можно было держать рядом с другими изменяемыми заимствованиями полей.

Виртуальная машина, модули и event loop

Общее глобальное состояние для одного исполнения JS-инстанса, по одному на поток. Оно владеет двумя встроенными event loop — regular и macro: regular обслуживает обычную работу, а macro используется, пока выполняется макрос. Текущий цикл различается сравнением указателя с regular- и macro-циклом. Доступ к циклу возможен как к сырому указателю, как к изменяемой ссылке и как к разделяемой ссылке; есть также вход в цикл сейчас с выходом при drop — с контрольной точкой и без неё.

Управление модулями построено на контекстах: для Bun.ModuleGraph создаётся контекст выполнения скрипта, контексты хранятся по идентификатору, а вход в контекст делает его текущим, причём для отсутствующего или удалённого контекста подставляется контекст-заглушка. Освобождение очереди работы освобождает всё, что стоит в обоих циклах, не выполняя это.

Почему это сделали

Цель переписывания — устранить повторяющиеся уязвимости безопасности памяти с помощью borrow checker. По словам Самнера, значительная часть ошибок — это use-after-free, double-free и «забыли освободить» на пути обработки ошибок. В безопасном Rust такие ошибки становятся ошибками компилятора, а очистка выполняется автоматически по принципу RAII через Drop: «ошибки компилятора — лучшая обратная связь, чем стайлгайд».

Ручное переписывание оценивалось как слишком дорогое: год работы небольшой команды инженеров с заморозкой исправлений, security-фиксов и разработки фич на это время. Вместо этого Самнер потратил неделю на проверку гипотезы о том, что новая модель Anthropic справится с переписыванием Bun на Rust. Постепенная миграция как выбранный путь не рассматривалась — был выполнен автоматизированный одномоментный перенос.

Как шла работа

Перенос выполнялся с использованием предрелизной версии Claude Fable 5, оркестрированной примерно через 50 динамических рабочих процессов. Работа была распределена по четырём шардам рабочих пространств, каждый запускал 16 агентов — всего 64 экземпляра Claude параллельно. На пиковой скорости система генерировала примерно 1 300 строк кода в минуту и регистрировала до 695 коммитов в час.

До слияния переписывание потребовало 5,9 млрд некешированных входных токенов, 690 млн выходных токенов и 72 млрд чтений кешированных входных токенов — около $165 000 по ценам API. Для сравнения: вручную это заняло бы около года у 3 инженеров с полным контекстом по кодовой базе. Через несколько дней после начала переписывания высокая доля тестового набора начала проходить.

Ключевыми факторами успеха названы планирование и два файла: PORTING.md с отображением шаблонов и типов Zig в Rust и LIFETIMES.tsv с временами жизни каждого поля структур. Процесс постоянно улучшался: после каждой ошибки улучшали сам процесс реализации, а не правили вручную артефакты кода. Роли разделены жёстко — реализатор не ревьюит, ревьюер не реализует, а найденные замечания обрабатывает отдельный агент-исправлятель. Для отлова проблем до финального переноса применялись состязательные агентные ревью кода.

Что это дало на практике

Rust-реализация в Bun v1.4.0 устранила 128 давних ошибок, присутствовавших в v1.3.14, и утечки нативной памяти. Тесты внутрипроцессной сборки, выполняющие 2 000 последовательных операций сборки, стабилизировались на 609 МБ вместо роста выше 6,7 ГБ. Пропускная способность HTTP выросла на 2–5%.

Совместимость с Node.js улучшилась: в Bun 1.4 заявлена совместимость с Node.js 26.3.0 и 1 517 новых проходящих тестов, в v1.4.1 — дальнейшие улучшения совместимости. Ранее, до переписывания, ситуацию с памятью пытались улучшать без смены языка: в релизе Bun v1.1.34 было уменьшено потребление памяти для долго работающих процессов, а в Bun v1.1.10 загрузка по сети стала использовать до 2,8 раз меньше памяти.

Ограничения и открытые вопросы

Часть кода остаётся на unsafe с сырыми указателями, и корректность там держится на договорённостях, зафиксированных в комментариях. В обработчике отложенной загрузки плагина сырой указатель-обратная ссылка разыменовывается через unsafe — риск в том, что корректность зависит от того, что указатель действительно жив. Обращение к контексту линковки из обратной ссылки централизовано в одном методе, который полагается на то, что задачи трогают непересекающиеся чанки. Функция получения AST принимает сырые указатели на транспайлер и резолвер, чтобы избежать UB от алиасинга &mut, и реборроуит только непересекающееся поле. При линковке берётся сырой указатель на объект сборки с оговоркой, что получатель &mut this.linker и *bundle_ptr не создают перекрывающихся &mut. Под --watch Box намеренно утекает, так как watcher-поток держит сырой указатель на объект сборки и разыменовывает его после возврата функции; утечка ограничена тем, что следующее изменение файла перезапускает процесс.

В коде остаются незакрытые TODO. Генерация файла манифеста серверных компонентов происходит слишком рано, до tree-shaking, поэтому неизвестно, какие экспорты живые, а какие мёртвые; план — сначала генерировать заглушку с export const serverManifest = undefined;, а позже перегенерировать файл с окончательным манифестом. Режим без отдельного SSR-графа не реализован и приводит к панике через todo_panic!. В обработке шаблонов имён ассетов есть незакрытый TODO про outbase. При отсутствии named import стоит комментарий «TODO: investigate if this is a bug», поскольку это означает импорты, добавляемые без разрешения. Поле log в options.rs помечено к удалению в пользу доступа через bundler.log.

Механический перенос внёс 19 тонких семантических регрессий из-за синтаксического сходства Zig и Rust. 11 раундов проверки безопасности от Claude Code Security исправили несколько проблем безопасности, а круглосуточный покрытийный фаззинг каждого парсера дал 15 PR.

О реакции сообщества известно немного: пользователь vitaminCPP утверждал, что слияние более миллиона строк машинно-транспилированного кода делает Bun важной отраслевой «канарейкой» для проверки того, смогут ли массивные LLM-сгенерированные кодовые базы оставаться поддерживаемыми на протяжении жизненного цикла разработки ПО. Создатель Zig Эндрю Келли опубликовал резко критический отзыв под названием «My Thoughts on the Bun Rust Rewrite».

Источники

Похожее