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встроенный разбор и рендеринг Markdown, JSON5расширенный формат JSON с комментариями и без кавычек в ключах, JSONLформат, где каждая строка — отдельный JSON-объект, Terminalвстроенный API для работы с терминалом и 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на диск, самодостаточные workspacenode_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часть компилятора Rust, которая проверяет, что ссылки на данные не конфликтуют между собой. По словам Самнера, значительная часть ошибок — это 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».