presto-architecture-ru.md Based on the article, Presto was the independent browser engine used by Opera from approximately 2003 to 2013, with its source code leaking online in 2017. The author analyzed the leaked code, which contains roughly 3.4 million lines of C++ across 9,800 files, describing the engine's architecture, including its core modules, JavaScript engine (Carakan), and platform-specific code. The article notes that Presto is now a "preserved slice of engineering thought" from 2013, as only three browser engines (Blink, WebKit, and Gecko) remain actively capable of rendering modern websites. Presto изнутри: как устроен движок Opera 12 — и стоит ли его воскрешать Большой технический разбор браузерного движка Presto — того самого, на котором работала Opera вплоть до версии 12.15. Исходники движка утекли в сеть в 2017 году; я взял этот архив, собрал его под современным Linux и Windows и провёл несколько недель, разбираясь, как он устроен. Ниже — подробная карта движка: архитектура и инженерные узоры, система сборки, JavaScript-движок Carakan, интерфейс браузера и его неотделимость от движка, модель потоков, поддержка 32 и 64 бит, внутренние идиомы, тесты, самые честные комментарии разработчиков — и развёрнутые ответы на вопросы, которые задаёт каждый, кто видит такой код: можно ли его причесать по канонам чистой архитектуры, не переписать ли всё на Rust, что и в каком порядке стоит улучшать — и врали ли в Opera, когда отказывались открыть исходники, ссылаясь на их «плохое состояние». --- Зачем вообще смотреть на мёртвый движок Сегодня существует ровно три живых браузерных движка, способных открыть произвольный сайт: Blink Google, он же сердце Chrome и почти всех остальных , WebKit Apple и Gecko Mozilla . Всё. Любой «новый браузер» — это, как правило, Blink в новой обуви. Presto — это четвёртый движок. Полностью независимый, написанный одной компанией, со своим HTML-парсером, своей реализацией CSS, своим JavaScript-движком, своей графической подсистемой, своим сетевым стеком и даже своей системой сборки. На нём работала Opera примерно с 2003 по 2013 год. В феврале 2013 Opera объявила о переходе на WebKit/Chromium, а Presto заморозили на версии 12.15. В 2017 году исходный код движка утёк в открытый доступ. Этот текст — результат разбора именно той утёкшей версии. Я отношусь к коду как к археологическому памятнику: это не живой проект, а законсервированный срез инженерной мысли крупной браузерной команды примерно 2013 года. И срез на удивление поучительный. Несколько цифр для калибровки масштаба. В дереве исходников: - ~3,4 миллиона строк C++ — 2,31 млн в ядре, 0,58 млн в неядерном коде, 0,53 млн в платформенном; - ~9 800 файлов .cpp / .h ; - 105 модулей ядра, 17 неядерных компонентов, 15 платформенных каталогов; - 1 354 файла тестов ; - 279 скриптов на Python 2 и 22 скрипта на Pike — это инструментарий сборки. Это не игрушка. Это полноценный браузер, и почти каждое решение в нём принято осознанно, под жёсткие ограничения своего времени. Давайте смотреть. --- Карта территории: четыре набора модулей Дерево исходников делится на четыре «набора модулей» module sets , каждый из которых — просто каталог с модулями: - modules/ — ядро движка. 105 модулей: рендеринг logdoc , layout , display , doc , dochand , JavaScript ecmascript с движком Carakan внутри , сеть url , network , cache , cookies , графика libvega , libgogi , jaypeg , minpng , img , DOM, CSS, кодировки и так далее. - adjunct/ — неядерный, но платформонезависимый код. Здесь живёт десктопный интерфейс — UI-фреймворк Quick quick , quick toolkit , почтовый клиент M2 m2 , m2 ui , автообновление, BitTorrent-клиент. - platforms/ — платформенный код: unix , mac , windows , quix интеграция с UNIX-десктопом , а также инструмент сборки Flower . - data/ — данные интернационализации, переводы, локализованные строки. Первое, что бросается в глаза при разборе, — состав модуля «по членству». В корне каждого набора лежит файл readme.txt , и модуль участвует в сборке тогда и только тогда, когда он перечислен в этом файле. Заголовок modules/readme.txt выглядит так: Emacs: please use - - tab-width: 4; indent-tabs-mode: nil - -. Thank you. vim:tabstop=4:noexpandtab: Core modules used for the Desktop product. Branch manager: current PPP integrators in Desktop CORE MILESTONE: core-integration-388 Обратите внимание на «Thank you.» в модлайне для Emacs — мелочь, но она задаёт тон: код писали живые, вежливые люди. И на строку CORE MILESTONE: core-integration-388 — версия ядра трекалась отдельными «майлстоунами интеграции». Второе наблюдение — не все «модули» написаны в Opera. Среди 105 каталогов есть сторонние библиотеки: libopeay форк OpenSSL и самый крупный модуль — 21 МБ на диске , libfreetype FreeType, 10 МБ , sqlite , zlib , libcrypto , liblcms управление цветом Little CMS , lea malloc аллокатор Дуга Ли , webp . Плюс unicode 13,5 МБ — это в основном таблицы Unicode-данных. Это важно держать в голове: «3,4 миллиона строк» — не весь этот объём написала команда Opera. Реально движкового кода Opera примерно 2,5-2,8 млн строк. Остальное — заимствованные, замороженные в 2013 году библиотеки. Крупнейшие собственно движковые модули по объёму кода предсказуемы: dom , ecmascript Carakan , layout , url , logdoc , svg , display . Именно они и есть «браузер». --- Сборка как отдельный язык программирования Самое необычное в Presto — даже не код движка, а то, как он собирается. Здесь нет ни make , ни CMake, ни autotools. Вместо этого — два собственных слоя инструментария. Hardcore: фаза генерации кода Модуль modules/hardcore в документации проекта прямо называют «центральной нервной системой» сборки. Перед тем как компилятор увидит хоть один .cpp -файл, запускается фаза setup — набор Python-скриптов во главе с modules/hardcore/scripts/operasetup.py . Эта фаза генерирует код . Среди прочего она производит: - единый список исходников всего проекта, агрегированный из всех файлов module.sources ; - предкомпилированные заголовки PCH ; - features.h — итоговую конфигурацию фич; - tweaks and apis.h — гигантский более 10 000 строк сгенерированный заголовок с твиками и API; - glue-код системы сообщений и protobuf g message , g proto ; - jumbo-файлы компиляции о них ниже ; - C++ из файлов тестов .ot . Иначе говоря, исходники Presto в том виде, в каком они лежат в git, неполны . Чтобы их собрать, нужно сперва сгенерировать недостающие куски. Эти сгенерированные файлы намеренно исключены из репозитория — их положено создавать заново при каждой настройке. Метаданные модулей Каждый модуль самоописателен. Рядом с кодом лежит набор декларативных файлов метаданных: - module.sources — какие .cpp компилировать; - module.export — какие API модуль предоставляет наружу; - module.import — какие API модуль потребляет от других; - module.tweaks — компайл-тайм-константы TWEAK ; - module.messages — определения сообщений и protobuf; - module.actions , module.keys , module.strings , module.about — UI-действия, горячие клавиши, локализованные строки, описание модуля. Самое интересное здесь — система зависимостей через API. Модуль не подключает другой модуль «по имени». Он объявляет в module.export именованный API, а потребитель в module.import пишет, при каких условиях этот API ему нужен: API CACHE URL STREAM yngve URL DataStream helper. Import if: FEATURE EXTERNAL SSL Условия — это булевы выражения над фичами и твиками. Скрипт сборки разбирает их в AST, разрешает транзитивно если A импортирует API из B, а тот API зависит от фичи C, то сборка A неявно требует C и превращает в препроцессорные define . Получается декларативный граф зависимостей: модули описывают, что им нужно, а система сборки вычисляет, как их связать. Для движка с тысячами компайл-тайм-переключателей это спасает от экспоненциального разрастания make-файлов. Jumbo-компиляция, она же unity-сборка Здесь стоит остановиться подробнее, потому что термин «unity-сборка» unity build, в Presto — jumbo многие слышали, но не все представляют его механику. Обычно каждый .cpp -файл — это отдельная единица трансляции. Компилятор запускается на него отдельно, заново разбирает все заголовки, которые этот файл подключает, и выдаёт отдельный объектный .o -файл. Когда файлов тысячи, а заголовки тяжёлые, выходит колоссальная избыточная работа: один и тот же большой заголовок разбирается тысячи раз. Unity-сборка склеивает много .cpp -файлов в одну единицу трансляции — буквально через include . Компилятор запускается один раз на всю пачку: общие заголовки разбираются однократно, оптимизатор видит сразу весь код пачки а значит, может инлайнить через границы файлов , линкер получает не тысячи объектных файлов, а десятки. Сборка ускоряется в разы. В Presto это выглядит так — генератор operasetup.py выпускает jumbo.cpp , который просто подключает исходники: cpp // This file is automatically generated by operasetup.py include "core/pch jumbo.h" include "modules/content filter/content filter.cpp" include "modules/content filter/content filter module.cpp" Платить за это приходится изоляцией. Внутри одной склеенной единицы исчезают границы между файлами: две функции static void helper в разных файлах, два символа в анонимных пространствах имён, файловый define — всё, что по отдельности было локальным, после склейки начинает конфликтовать. Поэтому unity-сборка требует дисциплины именования и аккуратности с макросами. Страдает и инкрементальность: правишь один файл — перекомпилируется вся jumbo-единица целиком. Сегодня это ровно то, что делает опция UNITY BUILD в CMake. Presto применял этот приём как штатный режим ещё в 2000-х — снова опередив своё время. Flower: сборщик на Python 2 Второй слой — собственно сборщик, Flower , живущий в platforms/flower . Это система сборки на Python 2, заменяющая make . Её документация прямо критикует make по четырём пунктам: make строит граф зависимостей заранее, а в Presto список исходников до фазы setup неизвестен; конфигурация в make — это плоские строки без списков и словарей; вывод параллельной сборки make нечитаем; цели make не самодокументируемы. Flower устроен на генераторах Python . Узел node — единица работы, поток flow — функция-генератор, описывающая, как этот узел построить. yield приостанавливает поток до готовности зависимостей — кооперативная многозадачность без потоков. Цели объявляются декоратором и потому видны в --help . Архитектурно это красиво. Практически — в 2026 году это означает, что для сборки браузера вам нужен работающий интерпретатор Python 2 , снятый с поддержки ещё в 2020-м. Об этом — в разделе про поддерживаемость. Можно ли портировать Flower на Python 3? Короткий ответ — да, и это, пожалуй, самое полезное, что вообще можно сделать с Presto. Но стоит развести два разных масштаба. Сам Flower — движок сборки в platforms/flower — это всего около 3 900 строк Python 2. Его архитектура на генераторах от версии языка не зависит. Портирование — стандартная работа Python 2 → 3: print , целочисленное деление, итерация по словарям, разделение str / bytes , синтаксис исключений, пара переименований в стандартной библиотеке. Автоматический 2to3 снимает большую часть, остальное — руками. Для четырёх тысяч строк это пара недель сфокусированной работы. Подвох в том, что сборка — это не только Flower. По всему дереву разбросано около 76 000 строк Python 2 в 279 файлах: скрипты кодогенерации hardcore operasetup.py и его окружение и пофайловые module.build/ .flow.py и .conf.py в каждом модуле. Вот это и есть настоящая поверхность портирования. Работа по-прежнему механическая и хорошо изученная, но это 76 тысяч строк, а не 4 тысячи. Плюс отдельная история — parse tests.pike : компилятор тестов написан на Pike , и это часть из примерно 11 000 строк Pike-кода в дереве. Для Pike никакого 2to3 нет; либо вы держите рядом интерпретатор Pike, либо переписываете компилятор тестов на другой язык. Итого честный масштаб задачи «перевести Flower на Python 3» — это «перевести ~76 тысяч строк Python 2 и решить, что делать с ~11 тысячами строк Pike». Несколько человеко-месяцев. Абсолютно подъёмно — и это, возможно, самая ценная отдельная модернизация: именно она превращает Presto из «собирается на специально подготовленной машине 2013 года» в «собирается у любого». Альтернатива — выбросить Flower и перейти на CMake: больше проектирования сверху, зато вы получаете живую, поддерживаемую, всем знакомую систему сборки. Я склоняюсь к тому, чтобы сперва сделать порт риск ниже, рабочая сборка сохраняется , а CMake — потом, если вообще понадобится. --- Браузер, который собирается под себя: features, tweaks, capabilities Presto конфигурируется полностью на этапе компиляции . Никаких рантайм-флагов, никаких feature-флагов в современном смысле. Есть четыре механизма. Features фичи — крупные функциональные блоки, которые включаются и выключаются целиком. Они описаны в человекочитаемом файле modules/hardcore/features/features.txt — там 416 фич . Каждая запись содержит имя, ответственного инженера, описание, список порождаемых define и зависимости. Из этого файла генерируются профили — profile desktop.h , profile minimal.h , profile tv.h , profile smartphone.h — и минимальный встраиваемый профиль отключает порядка 70% того, что включает десктопный. Tweaks твики — мелкозернистые числовые и булевы константы TWEAK , которых по дереву около 1 200 . Прелесть в том, что один твик может иметь разное значение для разных профилей: TWEAK HC FREE MESSAGE POOL INITIAL SIZE jl Value : 64 Value for desktop : 512 Value for minimal : 16 Это, по сути, тюнинг производительности, вшитый в бинарник на этапе сборки. Capabilities возможности — макросы вида CAP их около 1 800 , которые решают задачу совместимости между версиями модулей. Вместо «если версия модуля ≥ X» пишется «если определён DPI CAP PLATFORM EXECUTE ». Модуль декларирует, что он умеет, а потребитель проверяет это препроцессором. system.h — флаги возможностей платформы и компилятора порядок байт, представление float, поддержка потоков . Поверх всего этого лежат продуктовые переопределения в platforms/ /product/ — отдельные features.h , tweaks.h , system.h , config.h под каждую платформу. Философия понятна: сконфигурировать всё на этапе компиляции, чтобы выключенный код просто отсутствовал в бинарнике, а не проверялся в рантайме. Нулевая стоимость отключённых фич. Для встраиваемых устройств 2000-х — абсолютно правильное решение. Цена тоже понятна. Во-первых, комбинаторный взрыв: 416 фич, помноженные на профили, дают необозримую матрицу конфигураций, и баг в одной конфигурации может не проявиться в другой. Во-вторых — и это я ощутил на собственной шкуре, занимаясь модернизацией TLS, — код массово исчезает за макросами . По дереву разбросано примерно 2 565 директив ifdef FEATURE . Меняешь что-то — а оно «не работает», потому что весь блок вырезан препроцессором в этой сборке, и понять это можно только эмпирически. В коде попадаются и прямые следы такого «вырезания», например забытый огрызок мёртвой фичи: c // Left-over from FEATURE SCRIPTS IN XML, to be removed when no longer used. define SCRIPTS IN XML HACK Фичу убили, а хак, который она когда-то включала, остался жить. --- Архитектурные узоры, которые делают Presto переносимым Беглый обзор конвейера рендеринга не передаёт, насколько продуманной была инфраструктура вокруг него. Несколько узоров заслуживают отдельного разговора — именно они отвечают на вопрос «как один движок умудрялся работать на Windows, Mac, Linux, телефонах, телевизорах и игровых приставках». pi: слой портов и адаптеров modules/pi — это Platform Interface, и это самый важный архитектурный узор во всём Presto. Около 65 заголовочных файлов, и в каждом — чистый абстрактный интерфейс к чему-то платформозависимому: OpFont и OpPainter шрифты и рисование , OpBitmap , OpWindow , OpSocket и OpHostResolver сеть , OpLowLevelFile файлы , OpSystemInfo , OpClipboard , OpDragManager , OpInputMethod , а также интерфейсы к устройствам — OpCamera , OpAccelerometer , OpTelephony . Движок рендеринга — display , layout , logdoc — не знает ни одного системного вызова. Он работает только с абстракциями из pi . А конкретные реализации лежат в platforms/ : на Windows OpFont реализуется через GDI и DirectWrite, на macOS — через CoreText, на Linux — через FreeType. Это в чистом виде инверсия зависимостей — паттерн «порты и адаптеры», он же гексагональная архитектура. Только сделан он в Opera в середине 2000-х, задолго до того, как это словосочетание стало модным. Именно pi — причина, по которой Presto оказался настолько переносимым. Компоненты и каналы: актор-модель внутри движка В modules/hardcore/component живёт система OpComponent / OpComponentManager — и это актор-модель. Компонент — изолированная единица, которая, как прямо сказано в исходниках, «не имеет права использовать код из другого OpComponent» и общается с остальным миром исключительно типизированными сообщениями OpTypedMessage . Главное здесь — для чего это задумывалось. Не для многопоточности, а для изоляции процессов . В platforms/posix ipc есть IpcHandle и класс, который прямо назван «Opera sub-process». Сообщения умеют сериализоваться и уходить через канал в другой процесс. Иначе говоря, в Presto заложен фундамент мультипроцессной архитектуры — того, что Chrome позже сделал своей визитной карточкой. В Opera 12.15 это осталось скорее каркасом, чем готовой возможностью, но каркас — настоящий. scope: движок, который отлаживают по проводу modules/scope — это протокол удалённой отладки, фундамент инструментов разработчика Opera Opera Dragonfly . Устроен он как клиент-сервер: около 20 сервисов, описанных в .proto -файлах, и связь по сокету. Важное следствие: инструменты разработчика — это внешний клиент, говорящий с движком по сетевому протоколу. Можно было отлаживать страницу на телефоне прямо с десктопа. Девтулзы у Opera были архитектурно отделены от движка — редкая по тем временам аккуратность. protobuf своими руками Чтобы всё это работало, Opera не стала тащить в зависимости рантайм Google Protocol Buffers, а вендорила собственную реализацию protobuf вместе с кодогенератором modules/protobuf , каталог cppgen . Сообщения scope и межкомпонентные сообщения — это сгенерированные из .proto C++-классы. Самодостаточность как принцип: никаких внешних зависимостей, которые однажды сломаются. --- Как HTML превращается в пиксели Перейдём к собственно движку. Конвейер рендеринга в Presto — классический, в четыре стадии: разбор → стилизация → раскладка → отрисовка. Но дьявол, как всегда, в деталях. logdoc: логическое дерево документа Всё начинается в modules/logdoc . HTML-байты проходят через токенизатор и построитель дерева HTML5TreeBuilder , который реализует алгоритм парсинга из спецификации HTML5 с его 24 «режимами вставки» IN HEAD , IN BODY , IN TABLE и так далее . Результат — дерево узлов HTML Element modules/logdoc/htm elm.h . HTML Element — образец инженерии, экономящей каждый байт. Узлы дерева связаны через базовый класс DocTree — это двусвязное дерево m parent , m suc , m pred , m first child , m last child . Метаданные элемента упакованы по битам: 9 бит на код типа, 8 — на индекс пространства имён, 3 — на тип вставки, флаги «грязности». На странице из 10 000 элементов такая упаковка — это уже мегабайты. Особенно красив enum типа вставки. Элемент может быть невидим для JavaScript HE INSERTED BY LAYOUT , невидим для CSS, но при этом полноценно участвовать в раскладке. Это позволяет движку вставлять служебные узлы например, анонимные обёртки для таблиц , не показывая «кухню» скриптам на странице. Ещё одна важная деталь: парсинг прерываемый . Когда встречается