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, но при этом полноценно участвовать в раскладке. Это позволяет движку вставлять служебные узлы (например, анонимные обёртки для таблиц), не показывая «кухню» скриптам на странице. Ещё одна важная деталь: парсинг **прерываемый**. Когда встречается ` `, разбор останавливается (статус `EXECUTE_SCRIPT`) и может быть возобновлён позже. Это не наивный синхронный парсер, а конечный автомат, управляемый событиями. ### style: каскад CSS CSS живёт в `modules/style`. Класс `CSS` представляет таблицу стилей; правила проиндексированы по типам селекторов через хеш-таблицы (по тегу, по id, по классу) для быстрого сопоставления. Сам каскад выполняется в `modules/layout/cascade.h`: класс `LayoutProperties` оборачивает вычисленные стили элемента. Важная оптимизация — значения свойств вычисляются **лениво**, по требованию во время раскладки, а не все сразу. ### layout: дерево боксов и инкрементальная переразметка Раскладка происходит в `modules/layout`. Здесь строится **второе дерево** — дерево боксов CSS-модели. Базовый класс `Box`, от него наследуются `BlockBox`, `InlineBox`, `TableContent`, `FlexContent` и другие. У каждого `HTML_Element` есть прямой указатель `Box* layout_box`, и этот указатель равен `NULL` для нерендерящихся элементов (`display:none`). То есть дерево боксов — разреженная параллельная проекция логического дерева. Зачем разделять логическое дерево и дерево боксов? Логическое дерево с точки зрения раскладки неизменно — его меняет только парсер или DOM-скрипты. Дерево боксов эфемерно: оно перестраивается при переразметке. Такое разделение и делает возможной **инкрементальную переразметку (reflow)**. Работает она через флаги «грязности». `MarkDirty()` ставит флаг `ELM_DIRTY` — нужна «мягкая» переразметка; `MarkExtraDirty()` ставит `ELM_EXTRA_DIRTY` — «жёсткая», когда вклад элемента в размер родителя мог измениться. Сама переразметка управляется сообщениями: `PostReflowMsg()` кладёт сообщение в очередь, и когда оно обрабатывается, пересчитываются только грязные поддеревья. Каждый блочный бокс рекурсивно раскладывает своих детей, поэтому изменение одного элемента затрагивает его предков (нужно подстроить размеры) и потомков, но не трогает соседей. Отдельная элегантность — **анонимные боксы**. CSS-модель боксов и плоская структура HTML не всегда совпадают (классический пример — блочный потомок внутри строчного элемента). Presto на лету вставляет в дерево анонимные `HTML_Element` с флагом `HE_INSERTED_BY_LAYOUT`, чинит несоответствие, а при следующей переразметке удаляет и создаёт их заново. Для DOM их не существует. ### display: отрисовка обходом Отрисовка — в `modules/display`. Класс `VisualDevice` — «поверхность рисования», кеширующая состояние (шрифт, цвет, трансформация, область отсечения) и связывающая дерево боксов с платформенным `OpPainter`. После раскладки дерево боксов обходится в глубину объектами-обходчиками (`TraversalObject`): рисуются фоны и рамки, затем содержимое, затем контуры. Обход аккуратно отслеживает отсечение, трансформации и контексты наложения (stacking contexts) для корректного порядка отрисовки перекрывающихся элементов. Весь конвейер целиком: ``` сеть → HTML5-токенизатор → построитель дерева → дерево HTML_Element (logdoc) → каскад CSS (style + layout/cascade) → дерево боксов (layout) → обход и отрисовка (display) → OpPainter → пиксели ``` Архитектура честная и хорошо разделённая: logdoc разбирает, style стилизует, layout раскладывает, display рисует. Межмодульные связи минимальны — главный «мостик» между логическим деревом и раскладкой — тот самый указатель `layout_box`. Но инкрементальная переразметка тонка: взаимодействие флагов грязности, повторного использования каскада и схлопывания отступов требует очень аккуратного мышления, и хорошего обзорного документа «как работает reflow» в исходниках мне не хватило. --- ## Carakan: JavaScript-движок `modules/ecmascript` содержит **Carakan** — JavaScript-движок Opera, и это, пожалуй, самая впечатляющая часть Presto. Это не интерпретатор-наивняк, а полноценная виртуальная машина с регистровой архитектурой и JIT-компилятором. ### Конвейер исполнения Carakan — двухуровневая система. Базовый уровень — интерпретация байт-кода; для «горячих» функций и циклов включается генерация нативного кода. Конвейер такой: 1. **Парсер** превращает исходник JS в AST; 2. **Компилятор** генерирует регистровый байт-код в объекты `ES_Code`; 3. **Анализатор** (`ES_Analyzer`) делает статический вывод типов и анализ переходов; 4. **Генератор нативного кода** (опционально) производит машинный код, если функция «горячая». Ключевую идею разработчики сформулировали в собственной документации (`modules/ecmascript/carakan/documentation/native.txt`) прямо и честно: > «In essence, the bytecode instruction set is the API between the compiler and the native code generator.» Байт-код — это контракт между фронтендом и бэкендом, позволяющий им развиваться независимо. Очень здравая мысль; ровно так устроены и современные движки. ### Байт-код и инлайн-кеши Набор инструкций — порядка сотни опкодов с префиксом `ESI_` (`ESI_ADD`, `ESI_GETN_IMM`, `ESI_CALL`, `ESI_JUMP_CONDITION`...). Соглашение о вызовах регистровое: для вызова в регистре N лежит `this`, в N+1 — функция, дальше аргументы. Самый элегантный трюк — **самомодифицирующийся байт-код для кеширования доступа к свойствам**. Компилятор выпускает инструкцию `ESI_GETN_IMM`, но при первом успешном чтении свойства VM прямо в потоке байт-кода заменяет её на вариант `..._CACHED`, в который вшит результат разрешения. Следующие обращения идут уже без обхода таблицы свойств. Инлайн-кеш в чистом виде. ### JIT под три архитектуры Генератор нативного кода поддерживает **три семейства процессоров**, и масштаб говорит сам за себя: `es_native_ia32.cpp` — 397 КБ, `es_native_arm.cpp` — 276 КБ, `es_native_mips.cpp` — 254 КБ. Три отдельных бэкенда машинного кода — это серьёзная заявка на встраиваемые устройства (вспомните Opera на телевизорах и телефонах). Анализатор перед генерацией кода в цикле гоняет два прохода — анализ переходов и коррекцию вывода типов — пока они не перестанут находить новую информацию. И тут же, в документации, авторы оставили мою любимую ремарку: > «(And hopefully this loop terminates.)» «И будем надеяться, что этот цикл завершается.» Целый класс задач теории неподвижных точек уместился в одну скобку. ### Сборка мусора и объектная модель Память управляется **mark-sweep сборщиком мусора** (`es_collector.cpp`) с «квиклистами» — корзинами свободных блоков по размерам, ускоряющими аллокацию. Крупные объекты получают отдельные страницы. Объектная модель — со «скрытыми классами» (hidden classes), как у V8 и JavaScriptCore. `ES_Class` описывает «форму» объекта (какие свойства, в каком порядке, по каким смещениям), `ES_Object` — экземпляр со ссылкой на класс. Добавление свойства порождает новый класс-наследник. Значения — теговые объединения, причём есть компайл-тайм-режим **NaN-boxing** (`ES_VALUES_AS_NANS`), упаковывающий тип и значение в полезную нагрузку NaN. Интеграция с DOM — через `ES_Host_Object`: C++-объекты движка (узлы DOM, события) оборачиваются и подаются в JS как обычные объекты, с перехватом доступа к свойствам, проверками безопасности и возможностью приостановки исполнения для асинхронных операций. Для движка 2013 года Carakan вполне современен: регистровая VM, типовая обратная связь, многоархитектурный JIT, скрытые классы. Это инженерия мирового уровня. --- ## Сеть и графика ### URL: ресурс, у которого один владелец Сетевое ядро — `modules/url`. Центральная абстракция — класс-дескриптор `URL`, и у него крайне необычная модель владения. В заголовке `url2.h` есть капслок-предупреждение, которое стоит привести целиком: > «!NOTE!NOTE!NOTE! The URL object may be the ONLY handle a caller have to the identified resource! Deleting/freeing handle frequently means the resource cannot be relocated, and will in those cases also mean that the resource is removed from the cache.» То есть `URL` — это не строка и не обычный умный указатель, а почти что capability-токен. Сам ресурс живёт в подсчитываемом по ссылкам `URL_Rep`, которым может делиться много документов, и вся схема разделения держится на том, что вызывающий код дисциплинированно хранит дескрипторы. Поиск ресурса по строке URL прямо объявлен антипаттерном. Всем хозяйством — кешем, куками, аутентификацией, DNS — заведует `URL_Manager`. Загрузка ресурса асинхронна и управляется сообщениями: когда из сокета приходят данные, HTTP-соединение дёргает колбэк `HandleCallback(OpMessage, MH_PARAM_1, MH_PARAM_2)` с непрозрачными целочисленными параметрами. Соединения объединяются в пулы по парам (сервер, порт). Кеш (`Cache_Storage`) абстрагирует файловое, оперативное и потоковое хранилище. Куки хранятся в дереве доменов. ### libvega и libgogi: графика Векторная графика — в `modules/libvega`. `VEGARenderer` оборачивает `VEGARendererBackend`, и бэкендов три: программный растеризатор (`vegabackend_sw`), 2D-ускорение (`vegabackend_hw2d`) и полноценный 3D-конвейер на GPU (`vegabackend_hw3d`) — последний нужен для CSS-трансформаций и эффектов. Пути (`VEGAPath`) растеризуются алгоритмом заметающей прямой; поддерживаются градиенты, паттерны, режимы наложения. Ниже лежит `modules/libgogi` — низкоуровневый графический слой с прекрасным названием «Multiplatform Desktop Environment» (MDE). Это система инвалидации по регионам: отслеживаются прямоугольники экрана, требующие перерисовки, и перекрывающиеся объединяются. Решение из доGPU-эпохи, когда главным было минимизировать пересылку в фреймбуфер. Декодеры изображений вынесены в отдельные модули — `jaypeg` (JPEG), `minpng` (PNG) — и регистрируются в `ImageManager` как фабрики. Оба декодера потоковые: умеют обрабатывать данные по мере поступления и поддерживают прогрессивную отрисовку. --- ## Идиомы, которые пронизывают всё Вот мы и добрались до самого важного для разговора о поддерживаемости. У Presto есть несколько идиом, которые встречаются буквально в каждом файле. Не поняв их, движок невозможно ни сопровождать, ни тем более переписать. ### OOM-мания: коды возврата OpStatus Presto одержим обработкой **нехватки памяти**. Причина историческая: движок работал на телефонах и встраиваемых устройствах, где аллокация может провалиться, и упасть браузер не имеет права. Поэтому почти каждая функция, способная аллоцировать, возвращает `OP_STATUS`. `OP_STATUS` (`modules/hardcore/base/opstatus.h`) — это `typedef int` (а в отладочной сборке — целый класс, ловящий непроверенные коды). Коды простые: ```c #define OP_STATUS_ERR -1 #define OP_STATUS_ERR_NO_MEMORY -2 #define OP_STATUS_ERR_NULL_POINTER -3 #define OP_STATUS_ERR_OUT_OF_RANGE -4 ... #define OP_STATUS_ERR_NOT_SUPPORTED -9 ``` Любопытно, что «фатальной» считается ровно одна ошибка: `IsFatal()` возвращает истину только для `ERR_NO_MEMORY`. Нехватка памяти — единственное, что обязано раскрутить стек. ### LEAVE/TRAP: исключения без исключений Раз `ERR_NO_MEMORY` обязано раскрутить стек, нужен механизм раскрутки. Им стал **LEAVE/TRAP** — заимствованная из Symbian (EPOC) идиома, реализованная в `modules/util/excepts.h`. Это видно прямо в коде: там есть ветка для `EPOC`, вызовы `User::Leave()`, `CleanupStack` — буквальные API Symbian. `LEAVE(code)` нелокально выходит из текущего кода — как `throw`. `TRAP(var, block)` ловит этот выход и кладёт код в `var` — как `try/catch`. Изюминка в том, что реализаций **три**, и выбираются они препроцессором: - если доступны C++-исключения — `LEAVE` это `throw`, а `TRAP` это `try/catch`; - на Symbian — нативные `User::Leave` и `CleanupStack`; - иначе — `setjmp`/`longjmp` поверх ручного стека очистки: ```c #define TRAP(var, block) \ do { \ CleanupCatcher _catcher; \ if (0 == op_setjmp(_catcher.catcher)) { \ do { block; } while (0); \ var = OpStatus::OK; \ } else { \ var = (OpStatus::ErrorEnum)_catcher.error; \ } \ } while (0) ``` В режиме `setjmp`/`longjmp` C++-деструкторы при раскрутке *не вызываются*, поэтому объекты на стеке нужно вручную «якорить» в связном списке `CleanupItem` макросами `ANCHOR(...)`. Получается ручная, написанная руками эмуляция того, что C++ давно делает сам. И вот что особенно показательно. Поскольку в коде сосуществуют **две** модели обработки ошибок — коды возврата и LEAVE — понадобился целый DSL макросов для перехода между ними. `RETURN_IF_ERROR(expr)` проверяет код и делает `return`. `LEAVE_IF_ERROR(expr)` проверяет код и делает `LEAVE`. `RETURN_IF_LEAVE(expr)` оборачивает «лёвающий» вызов в `TRAP` и превращает в код возврата. `RAISE_AND_RETURN_IF_ERROR` дополнительно сигналит менеджеру памяти. Этих макросов в `excepts.h` около двух десятков. Каждый из них — мостик между двумя вселенными обработки ошибок, которые приходится поддерживать одновременно. ### Двухфазное конструирование Конструктор C++ не может вернуть код ошибки. Поэтому в Presto почти не аллоцируют ресурсы в конструкторах. Канонический паттерн — приватный конструктор, публичный второй этап (`Construct()` или `ConstructL()`, который может `LEAVE`) и статическая фабрика `Make()`. `Make()` делает `OP_NEW`, вызывает второй этап и при нехватке памяти возвращает `NULL`. Это предшественник современных фабричных функций, возвращающих `expected<T>`. ### Глобальное состояние и порядок инициализации Всё глобальное состояние движка собрано в объекте `g_opera`. Модули инициализируются и уничтожаются в строго заданном порядке (сначала `stdlib`, потом `pi`, `hardcore`, и так далее вверх по зависимостям), что снимает классическую проблему «фиаско порядка инициализации» — ценой жёсткости. ### Свои строки, свои контейнеры Никакого STL. Строки — `OpString` в двух вариантах, 8-битном и 16-битном (`uni_char` внутри — это UTF-16, потому что DOM и JavaScript требуют именно его). Контейнеры — собственные `OpVector`, `OpHashTable`, `OpAutoVector`. В эпоху, когда STL по разным платформам вёл себя непредсказуемо и тянул код, своя библиотека контейнеров с полным контролем над аллокацией и учётом памяти была оправданна. --- ## Интерфейс браузера: Quick и почему движок нельзя «вынуть» Отдельный вопрос: можно ли было оставить привычный, любимый интерфейс Opera 12 и подложить под него современный движок (тот же Chromium)? Ответ на него очень поучителен, и он спрятан в устройстве интерфейса. ### Что такое Quick Десктопный интерфейс Opera 12 называется **Quick** и живёт в `adjunct/quick` (слой приложения) и `adjunct/quick_toolkit` (слой виджетов). Это полностью **самонарисованный набор виджетов**. Opera не использует кнопки, списки и поля ввода операционной системы — она рисует их сама. `OpButton`, `OpTreeView`, `OpEdit`, `OpToolbar`, `OpWorkspace`, `DesktopWindow`, `BrowserDesktopWindow` — все они отрисовываются движком виджетов `modules/widgets` поверх того же `VisualDevice`, что и веб-страница, а внешний вид берётся из системы скинов `modules/skin` (`OpSkinManager`). Именно поэтому Opera 12 выглядела абсолютно одинаково на Windows, Mac и Linux и была фантастически перекрашиваемой — знаменитые скины Opera меняли весь интерьер до неузнаваемости. Каждый пиксель интерфейса рисовала сама Opera. ### Как это работает на разных ОС Раз интерфейс самонарисованный, от операционной системы нужно совсем немного. Платформенный слой (`pi` плюс `platforms/unix`, `platforms/windows`, `platforms/mac`) даёт только: нативное окно верхнего уровня, цикл обработки сообщений, системные диалоги (выбор файла), буфер обмена, drag-and-drop и нативные контекстные меню. Всё остальное — вкладки, панели, адресная строка, боковые панели, настройки — рисует Quick. Грубо говоря, интерфейс Opera примерно на 90% платформонезависим, а `platforms/` — это тонкая прослойка. ### Почему движок и интерфейс неразделимы Теперь — главное. В `modules/windowcommander` есть класс `OpWindowCommander`, и по замыслу это и должна была быть граница встраивания: чистый, узкий API между движком и интерфейсом-«вкладчиком». На практике интерфейс эту границу обходит. Файл `adjunct/quick/WindowCommanderProxy.cpp` лезет прямо во внутренности движка: подключает `modules/dochand/win.h` (класс `Window`), `modules/doc/frm_doc.h` (`FramesDocument`), `modules/display/vis_dev.h` (`VisualDevice`), достаёт внутренние объекты документа, дёргает `Window_Type`. WindowCommander в итоге стал не API встраивания, а скорее слоем уведомлений о событиях. Но даже это — не самое глубокое. Интерфейс и движок делят **вообще всё**: одну систему сборки (hardcore/Flower), один общий бинарник, одни и те же идиомы обработки ошибок (`OpStatus`, `LEAVE`/`TRAP`), один `OpString`, один глобальный `g_opera`, один слой `pi`. Quick — это не приложение, которое встраивает в себя веб-движок через стабильный интерфейс. Quick **компилируется внутрь движка**. Это единый монолитный исполняемый файл, где UI-код вызывает движок напрямую, как соседний модуль. Поэтому «оставить интерфейс, заменить движок» технически означает «переписать весь интерфейс заново»: вырезать все прямые обращения к `Window`, `FramesDocument`, `VisualDevice`, заменить модель документа, переписать связку под чужой движок, перестроить сборку. Это не операция по пересадке, это постройка нового браузера. И тут есть железное историческое доказательство. Когда Opera действительно сменила движок — в 2013-м, на Chromium, — перенести Quick она **не смогла**. Opera 15 вышла с полностью новым интерфейсом, построенным поверх Chromium, и растеряла огромную часть возможностей Opera 12. Знаменитое возмущение старых пользователей «верните мою Opera 12» — это и есть прямое следствие того, о чём идёт речь: вариант «сохранить интерфейс, заменить движок» был ровно тем, который оказался невозможен. --- ## Один поток: многопоточность, которой нет ### Presto однопоточен Ядро движка — однопоточное. Есть один цикл обработки сообщений (`MessageHandler`, глобальный `g_opera`), и через него проходит всё: разбор HTML, каскад CSS, раскладка, исполнение JavaScript, отрисовка. Дерево DOM и дерево боксов не защищены ни мьютексами, ни атомиками — и не нуждаются в этом, потому что к ним всегда обращается ровно один поток. Однопоточность здесь — не случайность, а несущая опора, и код это прямо признаёт. `OpComponentManager` снабжён комментарием «is not thread-safe». Пул памяти `OpMemoryPool` помечен капслоком «THIS IS NOT THREAD-SAFE». `OpSharedPtr` — «is not thread-safe!». Любопытная деталь: в платформенном слое есть макрос `PI_CAP_SYNCOBJECT_REMOVED` — то есть в Presto когда-то были объекты синхронизации потоков, и их **убрали**. От потоков сознательно ушли. ### Что всё-таки работает в потоках Потоки в Presto есть, но только на периферии: `pthread_create` встречается всего в 26 файлах дерева. Это фоновая индексация истории посещений (в десктопной части), часть служебных задач интерфейса — и `oppseudothread` в Carakan. Последнее — красивый трюк: pthread там используется не ради параллелизма, а чтобы реализовать **сопрограммы**, то есть приостанавливать и возобновлять исполнение JavaScript поперёк нативного стека C++. Сами задачи JavaScript планируются кооперативно (`ES_Thread`), без вытеснения. ### Можно ли добавить многопоточность Честный ответ: в ядро — нельзя, по крайней мере не малой кровью. Параллельная раскладка или параллельный каскад (то, ради чего вообще появился Servo) требуют, чтобы структуры данных были безопасны для одновременного доступа. В Presto всё наоборот: деревья DOM и боксов изменяемы и не блокируются, `g_opera` — общее изменяемое глобальное состояние, доставка сообщений синхронна, инициализация модулей строго последовательна. Ретрофит параллелизма — это перестройка позвоночника движка: оценка в 6-12 месяцев архитектурной работы выглядит оптимистично, и ещё годы уйдут на отлов гонок. Урок Servo именно в этом: параллельная раскладка должна закладываться в архитектуру с первого дня. Но у Presto был свой ответ на вызов параллелизма — просто другой. Система компонентов и каналов плюс `posix_ipc` указывают не на многопоточность, а на **мультипроцессность**: изолировать работу в отдельных процессах, общающихся сообщениями, — путь, которым пошёл Chrome. В 12.15 это каркас, а не готовая функция, но для проекта-возрождения мультипроцессность через уже существующий механизм `OpComponent` — куда более реалистичная история про параллелизм, чем попытка сделать ядро потокобезопасным. А вот что реально и недорого — вынести в фоновые потоки **листовую** работу: декодирование изображений, сетевой ввод-вывод, дисковый кеш. Для этого даже есть готовый мостик `OpThreadTools`, умеющий безопасно перекидывать результат обратно в главный поток. Рендеринг это не распараллелит, но отзывчивость интерфейса заметно поднимет. --- ## 32 бита, 64 бита и современные компиляторы ### x86 и x64 Вопреки тому, что Opera 12.15 на десктопе **поставлялась 32-битной**, сам код собирается и в 64 бита. Разрядность задаётся обязательным системным флагом `SYSTEM_64BIT` (он определяет макрос `SIXTY_FOUR_BIT`); на POSIX она определяется по `__LP64__`/`_LP64`, на Windows — по `_WIN64`. Причём код действительно 64-бит-чистый: указатели приводятся к целым через типы `INTPTR`/`UINTPTR`, а не через сырой `int`, а в Windows-сборке предупреждения об усечении указателя (C4311/C4312) превращены в жёсткие ошибки компиляции. То есть собрать можно и x86, и x64; для проекта-возрождения очевидная цель — x64. То, что десктопная Opera 12 была 32-битной, — продуктовое решение своего времени (меньше размер, а NPAPI-плагины тогда были 32-битными), а не ограничение кода. ### Россыпь архитектур Сборка декларирует поддержку IA32 (x86 и x86-64), ARM, MIPS, PowerPC и SuperH. JIT Carakan покрывает IA32/ARM/MIPS, остальные архитектуры довольствуются интерпретатором. Порядок байт и требования к выравниванию у RISC-процессоров разведены системными флагами. Эта широта — наследие «встраиваемой» Opera на телевизорах, телефонах и приставках. ### Современный компилятор Вы уже собрали Presto под GCC 4.9 и Visual Studio 2010 — то есть худшее позади. Двинуться дальше, к GCC 13+, Clang 17+ или MSVC 2022, можно, но это требует работы. Код написан под C++98/03, и вот конкретные препятствия: - **Динамические спецификации исключений.** Макрос `THROWCLAUSE` разворачивается в `throw()` — конструкцию, объявленную устаревшей в C++11 и **удалённую в C++17**. Её придётся нейтрализовать (свести к `noexcept` или к пустому месту). - **Нет ключевого слова `override`** — строгие современные сборки будут сыпать предупреждениями `-Wsuggest-override`. - **Ключевое слово `register`** (в сгенерированных лексерах) — удалено в C++17. - **Компиляторо-специфичные атрибуты** (`__attribute__`, `__declspec`), `restrict`, `__thread` — потребуют аккуратных макросов совместимости. - Отсутствие move-семантики, `nullptr`, `auto`, лямбд — это не препятствия, а просто «старость»: собираться не мешает. Отдельная важная деталь: десктопная GCC-сборка идёт с флагами `-fno-exceptions -fno-rtti` (это прямо прописано в `platforms/flower/module.build/00-gcc.conf.py`). То есть C++-исключения в Presto **выключены** — и именно поэтому существует весь механизм `LEAVE`/`TRAP` на `setjmp`/`longjmp`. Современная сборка может либо сохранить `-fno-exceptions` (путь на `setjmp` продолжит работать), либо, наоборот, наконец включить исключения и позволить `LEAVE` стать настоящим `throw`. Вывод: реалистичный путь — остаться на `-std=c++03`/`gnu++03` (или перейти на `c++14` с разбором предупреждений), залатать горстку удалённых конструкций, обновить макросы атрибутов. Это ограниченная, механическая работа. Компилятор — не самое трудное. Самое трудное — инструментарий сборки (Python 2, Pike). --- ## Тесты: браузер, который тестирует себя сам У Presto есть тесты — и тестовая инфраструктура у него тоже своя, необычная. Тесты пишутся в файлах `.ot` («Opera Test») внутри модулей. Это предметно-ориентированный язык. Pike-скрипт `modules/selftest/parser/parse_tests.pike` (4 232 строки) компилирует `.ot` в C++, который линкуется **внутрь самого браузера**, когда включена фича `FEATURE_SELFTEST`. Тесты исполняются не отдельным бинарником, а диспетчером внутри работающего браузера. Вот фрагмент реального теста — `modules/util/excepts.ot`, который проверяет ровно ту машинерию LEAVE/TRAP, о которой шла речь выше: ``` group "util.excepts"; include "modules/util/excepts.h"; global { int e(int i, Container c) { ANCHOR(Container, c); OpStackAutoPtr<Container> A(OP_NEW(Container, (1,&used))); if (0 != i) LEAVE(OpStatus::ERR); return i; } ... } ``` Тест аллоцирует объекты, провоцирует `LEAVE` и проверяет по счётчику `used`, что все «заякоренные» объекты корректно освободились при раскрутке. DSL поддерживает блоки `setup`, таблицы данных, итерацию, и тесты можно писать как на C++, так и на ECMAScript, а также вставлять блоки HTML. Сколько всего тестов? **1 354 файла `.ot`.** Покрытие неравномерно, но логично — лучше всего покрыто ядро рендеринга: - `layout` — 55 тестов; - `svg` — 52; - `dom` (core + html) — около 80; - `style` — 32; - `logdoc` — 29; - `url` — 28; - `forms`, `util`, `cache`, `pi` — по 23-26. А вот модули вообще без тестов: `externalssl`, `webgl`, `applicationcache`, `obml_comm`, `coredoc`, `xmlparser`, `updaters`, плюс вендоренные библиотеки. То есть критичные подсистемы команда тестировала всерьёз, а периферию и заимствованный код — нет. Честная оценка: для своего времени это приличная культура тестирования. Но по нынешним меркам у неё фундаментальные ограничения. Тесты по сути интеграционные — они гоняются внутри собранного браузера, нет изоляции уровня юнит-тестов. Для запуска нужен работающий интерпретатор **Pike** — язык ещё более экзотический, чем Python 2. И никакого CI в современном понимании: тесты прогонялись внутри сборки, а не на каждый коммит в облаке. ### Как сделать тестирование лучше Каркас `.ot` хорош, но чтобы он стал современной страховочной сеткой, ему не хватает четырёх вещей. Первое — **убрать зависимость от Pike**: переписать `parse_tests.pike` на Python (это около 4 тысяч строк из примерно 11 тысяч всего Pike-кода в дереве), и тогда для сборки тестов не нужен будет экзотический интерпретатор. Второе — **завести CI**: тесты уже есть и уже умеют гоняться внутри браузера, не хватает только автоматизации «собрать — прогнать — собрать отчёт» на каждое изменение, что с готовым Docker-окружением делается за вечер. Третье — **дописать тесты там, где их нет**: модули с нулевым покрытием (`externalssl`, `webgl`, `applicationcache`, чувствительные к безопасности места) — это ровно те модули, которые проект-возрождение и будет править, так что новые тесты окупятся именно там. Четвёртое — для листовых модулей (декодеры изображений, парсеры, криптопримитивы) имеет смысл добавить обычные автономные юнит-тесты: им не нужен весь браузер, и обратную связь они дают мгновенно. Скелет хороший — не хватает автоматизации и покрытия на краях. --- ## Комментарии: голоса инженеров Код пишут люди, и в комментариях Presto эти люди очень хорошо слышны. Я специально прочесал дерево в поисках честных, усталых и смешных комментариев. Все цитаты ниже проверены — это реальные строки в реальных файлах. Самый человечный — в модуле миниатюр, `modules/thumbnails/src/thumbnail.cpp:346`: > `// I'm sorry for inflicting this upon you, dear reader, but it seems unavoidable.` > («Простите, дорогой читатель, что навлекаю это на вас, но, кажется, иначе никак.») Археология в чистом виде — `modules/cache/url_dd.cpp:549`: > `// Black magic from 2005. If there are 13 or more free bytes` > («Чёрная магия из 2005-го.») Отношения с компилятором — `modules/ecmascript/carakan/src/builtins/es_math_builtins.cpp:585`: > `# error "Man, your compiler sucks rocks."` Совместимость с конкурентами — `modules/ecmascript/carakan/src/compiler/es_parser.cpp:1525`: > `an evil test in the mozilla regression test suite. :-)` > («злой тест в наборе регрессионных тестов Mozilla».) Усталая честность — `modules/forms/src/formvaluelist.cpp:383`: > `// Rebuild the widget from the beginning. Yes, this sucks.` > («Перестраиваем виджет с нуля. Да, это паршиво.») Капитуляция перед спецификацией — `modules/webfeeds/src/webfeedentry.cpp:291`: > `OP_ASSERT(!"What to do, what to do, with this strange content type");` > («Что же делать, что же делать с этим странным content type».) Обещание, которое мы все давали, — `adjunct/quick/WindowCommanderProxy.cpp:2220`: > `//this one is _really_ ugly, let's find a better solution soon!` > («Вот это _реально_ уродливо, давайте скоро найдём решение получше!») Реализм насчёт отладки — `modules/ecmascript/carakan/src/vm/es_bytecode.h:94`, про лимит длины стек-трейса: > `... for internal debugging of crap code, 50 might be more useful` > («для внутренней отладки дрянного кода 50 может оказаться полезнее».) И ещё несколько коротких: «it's patently stupid to have an advance cache» (`mdefont/mdf_cache.cpp:429`); «Oops, our OpMemoryStateStatic is not large enough!» (`memory/src/memory_state.cpp:97`); «The amount of black magic to apply here is up to the implementation» (`pi/OpFont.h:625`); «Band-aid to make forms with radius not look too crappy» (`forms/src/piforms.cpp:793`). Эти комментарии — не повод посмеяться над авторами. Наоборот: они показывают зрелую инженерную культуру, где честность в комментарии ценится выше показного лоска. И заодно это бесплатная карта технического долга — каждый такой комментарий помечает место, которое стоит пересмотреть. --- ## Что делает Presto тяжёлым для поддержки Теперь соберём всё вместе и ответим на главный вопрос. Presto тяжело сопровождать — но важно понять, *почему именно*, потому что от этого зависит, что с ним вообще имеет смысл делать. 1. **Мёртвый инструментарий сборки.** Чтобы собрать браузер, нужны Python 2 (снят с поддержки в 2020) и Pike (нишевый язык, который мало кто видел). Это первое, обо что спотыкается каждый, кто хочет собрать движок сегодня. 2. **Комбинаторика конфигурации.** 416 фич, ~1 200 твиков, ~1 800 capabilities, ~2 565 директив `#ifdef FEATURE_*`. Код массово исчезает за макросами, и «не работает» часто означает «вырезано препроцессором в этой конфигурации». Это не теория — я ловил это руками. 3. **Ручная дисциплина вместо языковых средств.** LEAVE/TRAP, двухфазное конструирование, ручное «якорение» объектов, ~20 макросов-мостиков между двумя моделями ошибок, ручной учёт памяти. Всё это — параллельная инфраструктура, которую язык C++17 сегодня даёт из коробки (исключения, RAII, умные указатели, `std::expected`), а здесь она написана и сопровождается руками. 4. **Глобальное изменяемое состояние.** `g_opera` и жёсткий порядок инициализации модулей делают движок по сути однопоточным синглтоном. Распараллелить раскладку или стилизацию, как это делает Servo, в такой архитектуре практически невозможно без глубокой перестройки. 5. **Код эпохи C++03.** Свои строки, свои контейнеры, никакого STL, никаких move-семантик. Это было оправдано в 2005-м и стало балластом к 2020-м. 6. **Замороженный веб.** И это, возможно, главное. Presto застрял в вебе 2013 года. Нет CSS Grid. Нет HTTP/2 и HTTP/3. TLS заморожен на наборе шифров без ECDHE и AES-GCM (из-за чего, собственно, и началась моя возня с модернизацией — примерно пятая часть современных сайтов просто не открывается). Вендорные форки OpenSSL, FreeType, SQLite заморожены на версиях 2013 года. Движок не «сломан» — мир ушёл вперёд. Обратите внимание: пункты 1-5 — это устранимый инженерный долг. Пункт 6 — фундаментальный, и никаким рефакторингом он не лечится. --- ## Можно ли причесать Presto по канонам чистой архитектуры Соблазнительная мысль: взять Presto и переразложить по принципам Clean Architecture — концентрические слои, зависимости направлены внутрь, бизнес-логика независима от фреймворков и ввода-вывода. Давайте честно разберём, что это дало бы. Во-первых, **Presto уже не наивен архитектурно**. Система API export/import — это, по сути, явная декларация зависимостей между модулями. А модуль `pi` (platform interface) — это готовый слой портов и адаптеров: `OpFont`, `OpPainter`, `OpSocket` и десятки других — абстрактные интерфейсы, которые реализует платформенный код в `platforms/`. Инверсия зависимостей на границе с платформой здесь уже сделана. Во-вторых, и это важнее, — **чистая архитектура решает не ту проблему**. Её цель — управлять зависимостями ради тестируемости и заменяемости. Но боль движка рендеринга не в форме графа зависимостей. Боль — в самих алгоритмах (инкрементальная переразметка, схлопывание отступов, вывод типов в JIT) и в десятках тысяч граничных случаев веб-совместимости (тот самый «evil test in the mozilla regression suite»). Это сложность *неустранимая*, продиктованная спецификациями веб-платформы. Никакое расслоение её не уменьшит. В-третьих, **у слоёв есть цена**. Дополнительная косвенность, виртуальные вызовы, аллокации — на горячем пути рендерера (reflow, paint, диспетчеризация байт-кода) это измеримые потери кадрового бюджета. Браузеры сознательно нарушают чистоту слоёв ради скорости: прямой указатель `layout_box` внутри `HTML_Element` — это намеренный межмодульный «срез угла», и это правильное решение. Вывод трезвый: применять Clean Architecture к Presto целиком — не нужно и не достаточно. Структура движка для своей предметной области и так разумна. А вот «причёсывание» меньшего масштаба окупится: вырезать мёртвые модули и фичи (начиная с тех, у кого ноль тестов), сократить комбинаторику конфигурации, заменить ручную OOM-машинерию на исключения и `std::expected`, ввести умные указатели, поднять стандарт до C++17/20, прикрутить нормальный CI. Это не «чистая архитектура», это просто модернизация — и она реально помогает. --- ## Переписать на Rust или Go: честный разбор И, наконец, вопрос, который задаёт каждый. Раз код старый — может, переписать? ### Go: сразу нет Go не годится для ядра браузерного движка, и причин несколько. Движку нужен детерминированный контроль над памятью; пауза сборщика мусора на горячем пути отрисовки — это пропущенный кадр. Структуры данных движка (деревья DOM и боксов) — это огромные графы указателей, недружелюбные к GC. Нужен плотный C-ABI для интеграции с платформой и графикой. Go отлично подойдёт для инструментария или сетевых сервисов вокруг браузера, но не для самого движка. ### Rust: правильный язык — но не для «переписывания Presto» Rust — другое дело. Безопасность памяти без сборщика мусора, производительность уровня C++, хороший C-interop, бесстрашная многопоточность (которой у однопоточного, завязанного на цикл сообщений Presto не было никогда). Если бы движок писали с нуля сегодня, Rust был бы естественным выбором. Но фраза «переписать Presto на Rust» смешивает две совершенно разные задачи: **(а) Портировать существующий код.** Это почти бессмысленно. Вы потратите годы и получите безопасную по памяти версию браузера 2013 года, который по-прежнему не открывает половину современного веба. Логику Presto нельзя механически перевести — а даже если бы было можно, переводить заморозку 2013 года не нужно. **(б) Написать современный движок с нуля.** А это уже не «возрождение Presto». Это ровно то, чем заняты Servo и Ladybird. И вот их история — лучший из существующих ответов на вопрос о стоимости. **Servo.** Стартовал в Mozilla в 2012 году как исследовательский движок на Rust. Прошло 14 лет. На начало 2026 года Servo — это версия **0.0.5**, и в обзорах прямо пишут, что ему «ещё очень далеко до прямой конкуренции с большой тройкой движков». Реальная отдача Servo была не в том, что он стал браузером, — а в том, что из него извлекли два компонента, **Stylo** (параллельный движок CSS) и **WebRender** (GPU-композитор), и встроили их в Firefox Quantum в 2017 году. Сегодня Servo развивается под управлением Linux Foundation Europe и позиционируется как встраиваемый движок, а не как браузер. **Ladybird.** Вырос из браузера операционной системы SerenityOS, стал самостоятельным проектом, набрал спонсоров (среди них Cloudflare) и **8 штатных инженеров на фуллтайме** плюс сообщество волонтёров. Дорожная карта: альфа в 2026-м, бета в 2027-м, стабильный релиз для широкой публики — в 2028-м. То есть около десятилетия от первого кода до стабильной версии, с оплачиваемой командой. И — внимание — даже Ladybird, проект на C++, внедряет Rust **инкрементально**: в феврале 2026 года команда начала по частям портировать на Rust парсер JavaScript и генератор байт-кода. Не «большой взрыв», а кусочек за кусочком. Оба проекта учат одному и тому же: браузерный движок — это работа масштаба десятилетия и большой команды, а грамотный способ внедрять Rust — инкрементальный, за стабильными интерфейсами, начиная с листьев графа зависимостей. И есть ещё один аргумент, который часто упускают. **Сам Presto — это уже готовый ответ.** Opera — компания с сотнями инженеров — в 2013 году посчитала, что поддерживать независимый движок дальше экономически нецелесообразно, и перешла на Chromium. Если содержание движка не потянула Opera на пике, оценивать его «переписывание» силами энтузиастов нужно с очень холодной головой. ### Что тогда реально делать Главный драйвер стоимости — не язык. Это **поверхность веб-платформы**. Те ~2,5-2,8 млн строк собственно движкового кода кодируют двадцать лет HTML, CSS, DOM, ECMAScript, SVG плюс гору граничных случаев совместимости. Это нельзя ни перевести механически, ни выбросить. Для проекта-возрождения Presto вывод такой: - **Переписывание с нуля на Rust — нереалистично.** Это перестаёт быть возрождением Presto и становится «конкуренцией с Servo и Ladybird меньшими силами». - **Инкрементальное внедрение Rust — теоретически возможно** и, что приятно, система модульных API в Presto делает это более осуществимым, чем в типичном монолите. Можно брать листовой модуль с узким интерфейсом — декодер изображений (`minpng`, `jaypeg`), криптопримитив, со временем TLS-слой — и заменять его Rust-крейтом или Rust-реализацией за существующим API модуля. Но даже это — работа на годы, и покупает она в основном безопасность, а не современный веб. - **Самое ценное и достижимое — модернизация на месте.** Починить сборку (Python 2 → Python 3, убрать Pike или перейти на CMake), перейти на современный компилятор и стандарт C++, обновить стек безопасности (TLS, корневые сертификаты — то, чем я и занимаюсь), при возможности подменить вендоренные библиотеки на актуальные версии. Это не «чистая архитектура» и не «переписать на Rust». Это «сохранить и подлатать края». Звучит скромнее, чем «перепишем всё на Rust». Зато это выполнимо — и сохраняет именно то, ради чего Presto и интересен: живой, цельный, независимый браузерный движок, четвёртый в мире, который иначе останется только архивным `.zip`. --- ## Что можно улучшить: дорожная карта от простого к сложному Если свести всё вышесказанное к практическому плану, получается список — от самого дешёвого и полезного к самому дорогому и сомнительному. 1. **Самое простое — оживить инструментарий сборки.** Перевести ~76 тысяч строк Python 2 на Python 3, переписать `parse_tests.pike` на Python и убрать зависимость от Pike. Лучшее соотношение пользы к усилиям: именно это превращает Presto в проект, который может собрать кто угодно. Недели — несколько месяцев. 2. **Обновить стек безопасности.** Свежий список корневых сертификатов (уже сделано), современный TLS с нормальными шифрами, актуальные протоколы. Это разница между «открывает 80% веба» и «открывает веб». Ровно то, чем я занят сейчас. 3. **Перейти на современный компилятор.** GCC 13+/Clang/MSVC 2022, залатать удалённые конструкции C++, обновить макросы атрибутов. Ограниченная механическая работа. Недели. 4. **Срезать мёртвый груз.** Удалить модули и фичи, которые десктопному возрождению не нужны никогда (профили для ТВ, мобильных, встраиваемых; мёртвые фичи; модули без тестов и без смысла). Меньше кода — меньше поверхность сопровождения. Умеренно. 5. **Автоматизировать тесты.** CI вокруг существующего набора `.ot`, новые тесты на краях. Умеренно. 6. **Вынести в фоновые потоки листовую работу.** Декодирование изображений, сетевой ввод-вывод, дисковый кеш — через готовый `OpThreadTools`. Рендеринг это не распараллелит, но отзывчивость поднимет. Умеренно-сложно. 7. **Постепенно модернизировать идиомы.** По мере того как трогаете код: двухфазное конструирование → обычные конструкторы, ручная очистка → RAII, `OpString` → что-то вменяемое. Модуль за модулем, никогда не «большим взрывом». Сложно, длинный хвост. 8. **Обновить вендоренные библиотеки.** Заменить замороженные в 2013-м форки (OpenSSL, FreeType, SQLite, zlib) на современные версии. Тонко, потому что Opera их *форкала* и правила под себя. Сложно. 9. **Новые возможности веб-платформы.** HTTP/2, CSS Grid, современный JavaScript — каждый пункт сам по себе проект. Догонять веб 2026 года — задача, по сути, бесконечная. Очень сложно. 10. **Самое тяжёлое — многопоточность ядра или переписывание компонентов на Rust.** Хирургия на архитектуре; см. разделы про потоки и про переписывание. Годы. Форма этого списка и есть главный вывод. Дешёвые пункты (1-3) — это ровно то, что **сохраняет Presto живым и собираемым**. Дорогие пункты (9-10) — ровно то, что для проекта-сохранения не окупается. Возрождение Presto должно жить в верхней части списка. --- ## Что мне понравилось — и что показалось странным После глубокого изучения кода у меня сложилось вполне личное к нему отношение. **Что понравилось.** - **Carakan.** Регистровая виртуальная машина с JIT под три архитектуры, скрытыми классами и самомодифицирующимися инлайн-кешами — и при этом *задокументированная* (те самые `documentation/*.txt`). Для 2013 года — работа мирового уровня. - **Слой `pi`.** Серьёзная, дисциплинированная граница «портов и адаптеров», сделанная в середине 2000-х — задолго до того, как «гексагональная архитектура» стала модным словом. Именно она дала Presto переносимость. - **Система модульных API.** Декларативные `export`/`import` именованных API с булевыми условиями — настоящий граф зависимостей вместо `#include`-спагетти. - **Jumbo-сборка** — за десять лет до того, как unity build вошёл в моду. - **Честность кода.** Те самые комментарии. `// Yes, this sucks.` рядом с костылём лучше, чем вылизанная ложь. - **Региональная инвалидация в `libgogi`** — элегантно и ровно по размеру задачи своей эпохи. - **Битовая упаковка в `HTML_Element`** — за каждый байт боролись осознанно. **Что показалось странным или спорным** (с поправкой на эпоху — тогда всё это было оправданно): - **Два языка сборки, оба сегодня экзотические: Python 2 и Pike.** Выбрать Pike для компилятора тестов было необычным решением даже в 2008-м. - **Тройка features/tweaks/capabilities.** 416 + ~1200 + ~1800 переключателей — это реальная мощь, но матрица их комбинаций непроверяема, а исчезающий за макросами код тяжело держать в голове. Одной оси хватило бы. - **Две параллельные модели обработки ошибок** — коды возврата *и* `LEAVE`/`TRAP` — и ~20 макросов только ради того, чтобы наводить мосты между ними. Стоило выбрать одну. - **Ручная реализация стека очистки** (`CleanupItem`, `ANCHOR`), эмулирующая то, что деструкторы C++ делают сами, — потому что сборка идёт с `-fno-exceptions`. На телефоне 2003 года оправданно; везде остальное — налог. - **Модель «дескриптор `URL` — единственная ссылка на ресурс»** с паникующим `!NOTE!NOTE!NOTE!` прямо в заголовке. Мощно, хрупко, и грабли, в которых сам комментарий и признаётся. - **`OpString` в 8- и 16-битном вариантах** — вечная заноза. - **Конфигурация браузера *целиком* на этапе компиляции.** Нулевая стоимость в рантайме — да, но это значит, что единой «Opera» не существует: есть тысячи разных бинарников, и баг воспроизводится только в одном из них. Ничто из этого не глупость. Это инженерия 2003-2013 годов под реальными ограничениями — телефоны с килобайтами памяти, компиляторы, которым нельзя доверять, отсутствие пригодного STL, — которая закрепилась привычкой и пережила причины, её породившие. В этом, собственно, и состоит вся история любого legacy-кода. --- ## Врали ли в Opera про «плохое состояние» исходников? Когда сообщество просило Opera открыть исходники Presto, компания отказалась, сославшись на то, что код «в плохом состоянии» и что нет ресурсов привести его в пригодный для публикации вид. Утёкшее дерево позволяет это проверить. **Часть первая — «плохое состояние»** — как утверждение о *качестве кода* не выдерживает проверки. Утёкшее дерево — это и есть улика: код связный, модульный, самоописательный, задокументированный, в целом покрыт тестами (1 354 селфтеста). Я *собрал* его — под Linux и под Windows — из снимка 2013 года. Так не выглядит сгнивший код. Если «плохое состояние» означало «грязный код», то это было, мягко говоря, упрощение. **Часть вторая — «нет ресурсов привести его в пригодный вид»** — почти наверняка правда. И утёкшее дерево показывает, *почему*: дело в **лицензиях**. В репозитории — около **45 сторонних компонентов** (они так и перечислены в `modules/hardcore/features/features-thirdparty.txt`) под клубком несовместимых условий. Часть просто требует атрибуции (OpenSSL/SSLeay, данные Unicode, кодеки Xiph). Часть — копилефт, который «заражает» распространение (вариант GPLv2 у FreeType, тройная лицензия GPL/LGPL/MPL у Hunspell, LGPL у GStreamer). А часть — и это решающее — **коммерческий, проприетарный чужой код, который Opera не имела права публиковать в принципе**: шрифтовой движок Monotype iType (`FEATURE_3P_ITYPE_ENGINE`) и библиотека Matrix SSL (`FEATURE_3P_MATRIX_SSL`). Сверху — нераспространяемые данные: хранилище корневых сертификатов с доверенными якорями, данные поисковых систем, ~197 МБ баз переводов, завязанных на внутреннюю инфраструктуру Opera. Нельзя открыть дерево, в котором лежит коммерчески лицензированный исходный код другой компании. Чтобы сделать Presto публикуемым, Opera пришлось бы найти каждый сторонний компонент, вырезать или заменить проприетарные, проверить каждую лицензию, собрать всю атрибуцию, распутать зависимости от данных и задокументировать сборку, которой нужны Python 2 и Pike. Это месяцы работы юристов плюс инженеров — без какой-либо выручки, ради продукта, который только что публично закрыли. **Мой вердикт: не врали — но формулировали неточно.** «Код в плохом состоянии» было удобным, понятным неспециалисту пересказом настоящей, негламурной правды: «код нормальный, но он юридически переплетён со сторонним и проприетарным материалом, а распутать это для публикации — реальная работа, за которую никто не заплатит». Ирония в том, что именно утечка дала возможность это проверить, — и проверка показывает хорошо сделанный движок, завязанный в лицензионный узел. Оба факта истинны одновременно. И небольшое следствие лично для тех, кто берётся за возрождение. Тот же лицензионный узел — это и *наша* проблема. Любой, кто поднимает Presto из утёкшего архива, стоит на коде, в который входят компоненты, никогда для этого не лицензированные. Это надо трезво держать в голове: возрождение Presto — занятие хобби-археологическое, а не основа для коммерческого продукта. --- ## Заключение Presto оказался не «старым плохим кодом». Это связный, продуманный, добротно сделанный артефакт своей эпохи: полноценный браузер, написанный одной командой, со своим JavaScript-движком уровня мировых аналогов, своей системой сборки, своей графикой, своими тестами — и со своими, очень человеческими, комментариями. «Legacy» он не потому, что плох, а потому, что изменился мир вокруг: веб-платформа разрослась на порядок, языки и инструментарий сменили эпоху, а рынок схлопнулся в монокультуру Chromium. Инженерные решения Presto — компайл-тайм-конфигурация, OOM-дисциплина, многоархитектурный JIT, региональная инвалидация графики — были правильными ответами на правильные вопросы 2005-2013 годов. Стоит ли его воскрешать? Если под воскрешением понимать «переписать начисто на модном языке» — нет, это путь Servo и Ladybird, путь длиной в десятилетие и в десятки человеко-лет. Если под воскрешением понимать «не дать умереть, собрать на современных системах, подлатать самые острые углы и сохранить как живой, работающий, независимый движок» — то да, безусловно стоит. Именно этим я и занимаюсь. А «(And hopefully this loop terminates.)» — пусть остаётся как есть. Этот цикл завершается. Я проверял. --- *Статья основана на разборе утёкших исходников Presto (Opera 12.15). Все приведённые цитаты комментариев, пути к файлам и количественные показатели проверены непосредственно по дереву исходного кода.*
open-source, developer-tools, research, web3