The Engine We Lost: Digital Archaeology of Opera Presto

Движок, который мы потеряли: цифровая археология Opera Presto

Оглавление


Nulla. Disclaimer

Это предуведомление появилось тут уже после того, как несколько статей стали циклом. Я вернулся к началу и оставил будущему читателю объяснение о том, что его ждёт.

  1. Здесь, с разной степенью технического погружения, рассказывается об устройстве браузера Opera Presto и его JS-движке Carakan. Начиналось всё именно с исследования, затем автору захотелось «оживить» старую программу… и, как водится, в процессе еды пришёл аппетит.
  2. Многое, что здесь написано, — юридически, так скажем, неоднозначно. Автор никоим образом не заявляет прав на чужую собственность, и старается не нарушать никаких лицензий. Осознавая парадоксальность того, что он тут делает, он не собирается далее это обсуждать, полностью отдаваясь процессу исследования.
  3. При написании этого текста использовался AI, и автор этого не собирается скрывать. Автор сам не любит AI-слоп, поэтому заранее проговаривает, что при написании этих текстов AI помогал только там, где это было необходимо. Браузерные движки — настоящий rocket science, и код Presto — не исключение. Одному человеку не под силу досконально разобраться в многолетних слоях кода (это объясняет, почему за много лет появилось весьма ограниченное количество поверхностных разборов кода), и AI-агенты — единственный инструмент, решающий задачу.
  4. 99% кода было написано AI, 99% решений было принято человеком. Автор имеет основания считать себя неплохим разработчиком, но признаёт, что SOTA-модели Anthropic и OpenAI справились быстрее и лучше.
  5. Вы можете получить код и собрать его сами. Загляните в будущее.

Ну, а теперь, собственно…

I. Эпоха до монополии: чем была Opera и почему её больше нет

Когда-то web был другим местом. Не хуже, не лучше — хотя найдутся те, кто захочет мне возразить. Но он точно был свободнее, и в нём было больше выбора. В том числе — и в том, каким браузером пользоваться.

Про первую браузерную войну и о том, как IE уничтожил Netscape, написано многое. Но вторую браузерную войну как-то не вспоминают, а ведь именно в её результате сейчас у нас остались только всепожирающий Chrome со своими клонами, эппловский Safari и старательно закапывающий себя Firefox с его жалкими тремя процентами аудитории. Всё остальное — совсем уж микробы.

В 2012 году у нас были ещё IE (кто б мог предположить, что Microsoft откажется его развивать и перейдёт на движок конкурента!) и Opera. Абсолютно независимый, уникальный и многими искренне любимый браузер, разрабатываемый в норвежской компании Opera Software ASA.

Opera была очень инновационным и, одновременно, нишевым продуктом, о точной доле рынка которого можно вести споры, хотя незначительность её присутствия на десктопах обычно не оспаривается. Создатели постоянно думали, как сделать программу удобнее и лучше — без современного корпоративного буллшита. Такие вещи, как вкладки страниц, speed dial, блокировка всплывающих окон, поиск прямо из адресной строки и многое другое было или изобретено или популяризировано именно в Opera. Отдельную любовь на постсоветском пространстве Opera заслужила за невероятно бережное обращение с трафиком. Кеширование всего и вся, отключение ненужных ресурсов, рендер по мере подгрузки, а, позже, Opera Mini/Turbo — вы могли это оценить, когда сидели на поминутном диалапе или помегабайтно тарифицируемом DSL.

Опера работала почти на всём. Все актуальные операционные системы, кнопочные телефоны, смартфоны, игровые консоли, встраиваемые устройства… На многих мобильных телефонах той эпохи это был единственный браузер, способный отображать графику, исполнять скрипты, отправлять формы.

И это был быстрый, экономный браузер. Веб тогда уже начал тяжелеть, появлялись страницы — подумайте только! — в сотни килобайт, даже мегабайты размером. Opera работала с этим невероятно эффективно. Ближе к концу жизненного цикла она боролась по скорости с Chrome, и держалась очень достойно, несмотря на несопоставимую разницу в ресурсах команд.

Кажется удивительным, что со всеми этими преимуществами, Opera не захватила мир. В правильном, справедливом мире Opera вместе с Firefox сейчас вполне могли бы держать значительные доли рынка, конкурируя с Chrome и IE, делая web лучше, свободнее и удобнее. В проклятом мире, который мы сами и создали, Opera не выдержала конкуренции. В компании решили не трепыхаться, а перейти на WebKit (а затем — на его форк Blink) и продать интеллектуальную собственность. Название «Opera» осталось, но теперь это очередной китайский рескин Chromium, говорят — даже неплохой. Бывшие сотрудники Opera развивают свой браузер Vivaldi — также рескин Chromium, и тоже, говорят, хороший.

А исходники Opera Presto, несмотря на мольбы сообщества, остались закрытыми. Почему — об этом я тоже ещё поговорю.

В ноябре 2016 года Opera Software ASA перестала существовать как единое целое, а в январе 2017 кто-то «утёк» исходники версии ~2012 года в публичный доступ. Кто это сделал — мы не знаем, но спасибо ему.

II. Цифровая археология трёх миллионов строк кода

В момент, когда я пишу это, на дворе середина 2026 года. 14 лет — это несколько сменившихся поколений развития стандартов, железа, исходников, принципов, подходов. Копание в этих исходниках сродни раскопкам Помпей; они хранят под пеплом времени срез образа мыслей разработчиков той эпохи и технологических подходов, делавших браузер уникальным.

Несколько цифр для калибровки масштаба. В дереве исходников:

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

И тут следите за руками: кодовая база едина для всех платформ.

Браузер, работавший на Windows 7, macOS, QNX и кнопочнике Sony Ericsson собирался из одного репозитория.

Как этого удалось достичь? С помощью собственной системы сборки.

II.I Как собиралась Opera?

Фактически, в репозитории нет «windows-версии» или «qnx-версии» отдельно — нужный код собирается на лету. Сборочный скрипт проходит по всем модулям, читая их метаданные (это простые текстовые файлы, содержащие описания, какие файлы компилировать, какие API модуль экспортирует и импортирует, и много чего ещё). Сравнивая метаданные с заданной конфигурацией, скрипт и собирает в итоге *_jumbo.cpp-файлы — они-то уже и компилируются.

При этом модули не ссылаются друг на друга по имени. Модуль, как сказано выше, объявляет импорты-экспорты API и условия, в которых ему эти API нужны, например:

API_CACHE_URL_STREAM
    URL_DataStream helper.

    Import if: FEATURE_EXTERNAL_SSL

Условия — это булевы выражения над фичами и твиками. Скрипт сборки разбирает их в AST, разрешает транзитивно (если A импортирует API из B, а тот API зависит от фичи C, то сборка A неявно требует C) и превращает в препроцессорные #define. Получается декларативный граф зависимостей: модули описывают, что им нужно, а система сборки вычисляет, как их связать. Для движка с тысячами компайл-тайм-переключателей это спасает от экспоненциального разрастания make-файлов.

Что такое упомянутые выше jumbo-файлы? Это местная реализация unity-сборки. Обычно каждый .cpp-файл — это отдельная единица трансляции. Компилятор запускается на него отдельно, заново разбирает все заголовки, которые этот файл подключает, и выдаёт отдельный объектный .o-файл. Когда файлов тысячи, а заголовки тяжёлые, выходит колоссальная избыточная работа: один и тот же большой заголовок разбирается тысячи раз.

Unity-сборка склеивает много .cpp-файлов в одну единицу трансляции — буквально через #include. Компилятор запускается один раз на всю пачку: общие заголовки разбираются однократно, оптимизатор видит сразу весь код пачки (а значит, может инлайнить через границы файлов), линкер получает не тысячи объектных файлов, а десятки. Сборка ускоряется в разы.

В Presto это выглядит так — генератор operasetup.py выпускает *_jumbo.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-единица целиком.

В CMake опция UNITY_BUILD, делающая ровно это, появилась в конце 2019 года. Presto применял этот приём как штатный режим ещё в 2000-х.

Второй слой — собственно сборщик, Flower, живущий в platforms/flower. Это система сборки на Python 2, заменяющая make. Её документация прямо критикует make по четырём пунктам:

Скорее всего, название этой системы сборки было выбрано по использованной архитектуре. Flower устроен на генераторах Python. Узел (node) — единица работы, поток (flow) — функция-генератор, описывающая, как этот узел построить. yield приостанавливает поток до готовности зависимостей — так получалась кооперативная многозадачность без потоков.

В MSBuild логика аналогична Flower — точкой запуска служит проект operasetup, исполняющий те же hardcore-скрипты. Там есть свои грабли, но, в целом, это работает.

Но это ещё не всё. Presto конфигурируется полностью на этапе компиляции. Никаких рантайм-флагов, никаких feature-флагов в современном смысле. Есть четыре механизма.

  1. Features (фичи) — крупные функциональные блоки, которые включаются и выключаются целиком. Они описаны в файле modules/hardcore/features/features.txt — там 416 фич. Каждая запись содержит имя, ответственного инженера, описание, список порождаемых #define и зависимости. Из этого файла генерируются профили — profile_desktop.h, profile_minimal.h, и т.д. — и минимальный встраиваемый профиль отключает порядка 70% того, что включает десктопный.
    Каждая фича в реестре — маленькая анкета: описание, владелец, что определяет, от чего зависит, и матрица «включено/выключено» по всем пяти профилям. Читать features.txt — как листать бортжурнал: вот FEATURE_OUT_OF_MEMORY_POLLING — «включите, если памяти много, но всё же ограниченно»; вот FEATURE_LIMITED_FOOTPRINT — «уменьшает размер кода ценой более медленных алгоритмов».

  2. Tweaks (твики) — константы (TWEAK_*), которых по дереву около 1 200. Каждый твик может иметь разное значение для разных профилей:

    TWEAK_HC_FREE_MESSAGE_POOL_INITIAL_SIZE   jl
        Value                   : 64
        Value for desktop       : 512
        Value for minimal       : 16
    

    Это, по сути, тюнинг производительности, вшитый в бинарник на этапе сборки.

  3. Capabilities (возможности) — макросы вида *_CAP_* (их около 1 800), которые решают задачу совместимости между версиями модулей. Вместо «если версия модуля ≥ X» пишется «если определён DPI_CAP_PLATFORM_EXECUTE». Модуль декларирует, что он умеет, а потребитель проверяет это препроцессором.

  4. Поверх всего этого лежат продуктовые переопределения в platforms/*/product/ — отдельные features.h, tweaks.h, system.h, config.h под каждую платформу.

Философия понятна: сконфигурировать всё на этапе компиляции, чтобы выключенный код просто отсутствовал в бинарнике, а не проверялся в рантайме. Нулевая стоимость отключённых фич. Для встраиваемых устройств 2000-х — абсолютно правильное решение.

Цена тоже понятна. Во-первых, комбинаторный взрыв: 416 фич, помноженные на профили, дают необозримую матрицу конфигураций, и баг в одной конфигурации может не проявиться в другой. Во-вторых, код массово исчезает за макросами. Сами имена FEATURE_* в исходниках почти не встречаются — фича при генерации сборки разворачивается в свои производные дефайны (обычно вида *_SUPPORT), и уже ими выстлан весь движок: только #ifdef *_SUPPORT в дереве почти четырнадцать тысяч, а всего препроцессорных условий — под семьдесят тысяч. Меняешь что-то — а оно «не работает», потому что весь блок вырезан препроцессором в этой конфигурации, и понять это можно только на этапе тестирования. Судя по попадающимся артефактам кода, разработчики с этим сталкивались:

// Left-over from FEATURE_SCRIPTS_IN_XML, to be removed when no longer used.
#define SCRIPTS_IN_XML_HACK

Фичу убили, а хак, который она когда-то включала, остался жить.

II.II Как писалась Opera?

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

Самый старый код датируется 1995 годом: в 2 733 файлах стоит копирайт с этой датой — это «большой взрыв» движка. А формально самая старая строчка кода в дереве — 1984 года: это скелет GNU Bison, вшитый в сгенерированный CSS-парсер. Самый же старый живой код — 1990 года, и у него отличная история, до которой мы ещё дойдём.

Часть такой документации (начинающаяся буквально с «not much here yet») объясняет принципы разработки, которых придерживалась Опера много лет. Приведу две строки, являющиеся квинтэссенцией этих принципов:

It sounds silly even to mention it, but we do require C++ to be supported. Not every interesting platform has C++.

и

Some advanced C++ concepts are not required.
RTTI, STL, namespaces, exceptions: we don’t need them.

И ещё раз вспомните, где работал Presto. В реестре фич движка — пять профилей сборки: desktop, smartphone, tv, mini, minimal. Один и тот же код компилировался в настольный браузер, в браузер для кнопочных телефонов, в прошивку телевизоров и приставок (в дереве до сих пор 71 файл упоминает ATVEF — стандарт интерактивного телевидения), в серверный рендерер Opera Mini. По окаменелостям в коде можно восстановить весь зоопарк: Symbian/EPOC (34 файла), Qualcomm BREW (60), QNX и его графическая среда Photon, Windows CE. Компиляторы у этих платформ были соответствующие: какой уж там STL, если на дворе 2002 год и твой C++ — это «C с классами», собираемый вендорским тулчейном для телефона.
Слова «interesting platform» — это гиперссылка. Ведёт она на сайт Plan 9 from Bell Labs. Инженеры Opera на полном серьёзе держали в голове, что где-то есть интересная платформа, на которой нет даже C++.

Решение «нам не нужен STL» имело следствие, определившее весь облик кода: в Opera любили всё писать сами (даже то, что они писали не сами — они частенько дописывали). В дереве есть модуль stdlib — собственная реализация стандартной библиотеки C: op_strlen, op_snprintf, op_memcpy и далее по списку. Есть свой аллокатор и менеджер памяти (modules/memory), свои строки (OpString поверх собственного типа uni_char — UTF-16, задолго до того, как char16_t появился в стандарте), свои контейнеры (OpVector, OpHashTable), свои умные указатели (OpAutoPtr).

Причём контейнеры они написали дважды. В модуле otl — Opera Template Library — лежит второе поколение, и его документация объясняет, зачем: старый OpVector<Foo> хранил указатели Foo*, новый OtlVector<Foo> хранит значения — «STL-подобные контейнеры для ядра». То есть культура, провозгласившая «STL нам не нужен», за годы пришла к тому, что написала свой STL, потом поняла, что написала его не так, и написала ещё один. Рядом — третий заход на смежную тему: модуль opdata с copy-on-write буферами. Велосипеды разных поколений, стоящие в одном гараже: OTL прижился точечно в новых подсистемах, OpData — в scope/protobuf и сетевых буферах, а ~90% кода так и живёт на OpVector/OpString.

А теперь обещанное про 1990 год. Форматированный вывод — op_sprintf и компания. Заголовок файла сохранил родословную: «This code is taken from the EMX run-time library… Copyright (c) 1990-1997 by Eberhard Mattes», и приписку: «Converted to joint ascii/unicode printf by lth@opera.com / November 2005». Код, написанный для рантайма порта GCC на OS/2 в 1990-м, в 2005-м научили Юникоду, и он до сих пор печатает каждую строчку в этом браузере.

Генератор случайных чисел тоже взят «с миру по нитке» — вихрь Мерсенна, Copyright 1997–2002 Мацумото и Нисимура. Всё это аккуратно задокументировано в реестре фич — вплоть до того, что попытка собрать движок без нужного флага упирается в стену из #error, пересказывающую историю происхождения кода.

Но это не единственное, на что пошли в Opera ради универсальности. У этого кода собственная, экзотическая даже по тем временам, OOM-дисциплина. Движок писался для устройств, где 16 мегабайт — роскошь, и каждая аллокация в нём проверяется. Никаких exception — вместо них собственный механизм LEAVE/TRAP: setjmp/longjmp с ручным стеком зачистки. Документация упоминает об этом как о травме:

A typical example of something that was outside the envelope of change is the out-of-memory handling introduced in Opera 6 — it affected virtually every line of core code and was quite costly to implement.

«Затронуло практически каждую строчку кода ядра». Это не фигура речи: в дереве около 9 700 мест используют LEAVE/TRAP, и почти любая функция возвращает OP_STATUS, который нельзя молча проигнорировать.

Сам механизм живёт в modules/util/excepts.h (в файле указан архитектор — Petter Reinholdtsen), и его происхождение читается по именам: макрос LEAVE внутри вызывает User::Leave(i) — это дословно API EPOC OS. Норвежцы строили свою обработку ошибок по образцу телефонной операционки: TRAP — это op_setjmp поверх цепочки CleanupItem-ов, классы вроде ANCHOR регистрируют объекты на автоматическую зачистку при «выпрыгивании». По сути — исключения, реализованные вручную, без поддержки компилятора, зато работающие на любом тулчейне 1999 года.

И это не было карго-культом: модуль памяти умеет запускать движок на искусственно ограниченной куче (через dlmalloc) и детерминированно доводить его до OOM на десктопе — «мощная комбинация, позволяющая точно тестировать OOM-ситуации», как пишет документация. То есть это проверялось.

Интересно, что попытка перехода на исключения была: код включает дефайны этой логики, которые никогда не были использованы. Я предполагаю, что к 2012 году поддержка настолько слабых устройств потеряла смысл, а новые компиляторы уже не добавляли оверхед размера бинарника из-за исключений. Тогда разработчики попробовали использовать идиоматичные языку средства, но так и не завершили работу, потому что переписывание трёх миллионов строк с OOM-проверками — это задача на сотни человеко-месяцев.

Кстати: писали они судя по модлайнам, в emacs/vim. Только консоль, только хардкор!

II.III Как тестировалась Опера?

После всего, что вы узнали, вы уже не удивитесь: система тестирования у Opera тоже полностью своя. Тесты пишутся в файлах .ot («Opera Test») внутри модулей. Это предметно-ориентированный язык, поддерживающий блоки setup, таблицы данных, итерации. Тесты можно писать как на C++, так и на ECMAScript, а также вставлять блоки HTML.

Pike-скрипт modules/selftest/parser/parse_tests.pike компилирует .ot в C++, который линкуется внутрь самого браузера, когда включена фича FEATURE_SELFTEST. Тесты исполняются не отдельным бинарником, а диспетчером внутри работающего браузера — на десктопе это выглядит, как запущенная Опера, открывающая внутри себя какие-то окна.

Сколько всего тестов? 1 354 файла .ot. Покрытие неравномерно, но логично — лучше всего покрыто ядро рендеринга:

Без тестов остались externalssl, webgl, applicationcache, obml_comm, xmlparser, updaters, плюс третьесторонние библиотеки. То есть критичные подсистемы команда тестировала всерьёз, а периферию и заимствованный код — нет.

Для своего времени это приличная культура тестирования, но по нынешним меркам у неё фундаментальные ограничения. Тесты по сути интеграционные — они гоняются внутри собранного браузера, нет изоляции уровня юнит-тестов. Для сборки нужен работающий интерпретатор Pike — язык ещё более экзотический, чем Python 2. И никакого CI в современном понимании: тесты прогонялись внутри сборки, а не на каждый коммит в облаке.

II.IV Кем написана Опера?

Как говорит один хороший историк: «мы не можем залезть этим людям в головы». Но по тому, какие следы они оставили, мы очень хорошо можем их представить. Возьмём, например, внутренние названия:

Сам движок — Presto, «быстро», как музыкальный темп. JavaScript-движки Opera сменялись династией, названной в честь древних письменностей: Linear A и Linear B (эгейское письмо), затем Futhark (руны), затем Carakan (яванское письмо).

Релизы браузера эпохи 9.x-10.x назывались в честь соколов (Merlin, Kestrel, Peregrine), позже пошли рыбы (Barracuda, Swordfish, Wahoo — 11.x–12.00).

Графическая библиотека называется VEGA, и это акроним: «Library for VEctor Graphics with Anti-aliasing». Декодер JPEG называется jaypeg, декодер PNG — minpng («с упором на минимальный футпринт»), менеджер паролей — wand, волшебная палочка. Модуль виджетов называется gadgets, и его module.about объясняет причину: «The module is called gadgets because, in the code, ‘widgets’ already has another meaning». А странное имя logdoc у ядра DOM-дерева — просто «logical document».

Отдельная строка в этом списке — пасхалка в CSS-парсере. В грамматике, среди прочих токенов, есть CSS_TOK_DOUBLE_RAINBOW_FUNC: Opera 11.60 ввела градиентную функцию -o-double-rainbow(). Двойная радуга, зашитая в грамматику Bison, чтобы позволить разработчикам «to sprinkle some vivid color into their web projects». Это был изящный стёб над стандартами и доказательство того, что браузер делали живые люди с отличным чувством юмора, которым было хорошо на работе.

Но самый «человеческий» слой раскопок — это оставленные в коде комментарии. 1 409 TODO, 1 859 FIXME, 1 449 XXX, ~850 ссылок на внутренний багтрекер CORE-xxxxx и ~690 на десктопный DSK-xxxxx. Но статистика не передаёт интонацию. Вот мои любимые находки:

// Warning: some cargo cult programming ahead.

if(!this)   // I know, it is silly...  :-( but if it prevent the crash, it's ok...

// Quirk crap. Try not to look.

// horrible hack, but it's probably not a good idea to font switch to ahem. ever.

// Rebuild the widget from the beginning. Yes, this sucks.

if ((p = strstr(head.longname, "?B?")) != NULL) // It's NOT Base64, it's QP damn it!

// Now we're screwed, there is no way out. Let's hope the error status...

const LayoutCoord o(0); // Avoid an insane amount of "LayoutCoord(0)" below.

// Sorry, this is ugly, but otherwise we crash

OP_ASSERT(!"What to do, what to do, with this strange content type");

Было бы нечестно свести археологию к курьёзам. Внутри Presto лежат вещи, которые и сегодня вызывают профессиональное уважение.

Carakan — JavaScript-движок — это регистровая виртуальная машина с JIT-компиляцией в нативный код под четыре архитектуры: x86, x86-64, ARM (включая Thumb) и MIPS; в утилитах рядом лежат ассемблеры ещё и для PowerPC и SuperH. Значения NaN-боксятся (8 байт на 32-битных платформах), объекты живут на «скрытых классах» — внутренняя документация называет это Compact Object Model и хвастается сокращением с 6–7 указателей на объект до одного. JIT ведёт профилирование типов прямо в байткоде и перекомпилирует горячий код по мере уточнения картины. Регулярные выражения — тоже свои, и у них собственный отдельный JIT под те же архитектуры. Работа мирового уровня, выполненная одной небольшой командой, на собственной инфраструктуре, без LLVM, в 2009 году.

VEGA — векторный растеризатор с программным бэкендом и аппаратными: Direct3D, OpenGL, DirectFB. Поверх него — Canvas 2D, WebGL (с полноценным собственным парсером и транслятором GLSL в modules/webgl — они написали свой компилятор шейдеров), SVG с SMIL-анимацией и фильтрами, умеющими считаться на GPU.

Парсер HTML5 — полная реализация алгоритма из спецификации того времени: токенизатор, tree builder со всеми insertion modes, foster parenting, adoption agency, спекулятивный предпарсер для префетча ресурсов. Внутреннее имя — Ragnarok. Presto был не только быстрым, но и очень современным движком.

Текст. Собственный OpenType-шейпер с поддержкой арабского и индийских письменностей (без HarfBuzz — его тогда толком и не было), собственная реализация двунаправленного алгоритма Юникода, таблицы Unicode 6.1, детектор кодировок и три десятка декодеров чарсетов. Всё своё.

Сеть. Опять всё своё: HTTP/1.1 с агрессивным пайплайнингом (в коде живут флаги tested_http_1_1_pipelinablity и счётчик pipeline_problem_count: движок проверял каждый сервер на совместимость и отступал при проблемах), SPDY версий 2 и 3, WebSockets уже по финальному RFC 6455, CORS с прекрасно реализованным preflight. И FTP. И заглушка Gopher — куда без неё.

И CSS-первопроходчество, за которое Opera любили (и, иногда, ненавидели) веб-разработчики: -o-tab-size, -o-object-fit (object-fit придумали в Opera), @viewport — CSS-альтернатива метатегу viewport, GCPM-флоаты для печатной вёрстки. В таблице свойств мирно сосуществуют два поколения флексбокса: финальный display: flex 2012 года и древний -webkit-box 2009-го внутри одного файла.

Если сложить слои вместе, из раскопа проступает портрет команды. Люди, которые всерьёз собирались работать на любой платформе Вселенной — и потому написали себе всё: libc, STL, аллокатор, шейпер, компилятор шейдеров, четыре JIT-бэкенда. Люди с почти военной дисциплиной метаданных — и живыми, смешными комментариями. Люди, которые репетировали нехватку памяти на телефонах — и прятали двойную радугу в грамматику CSS. Это был большой, умный, самобытный инженерный мир, жители которого любили то, что делали.

III. История одной лжи

После того, как в Opera ASA приняли решение перейти на WebKit, родилась ложь, сравнимая с «640 килобайт хватит всем». Пользователям — а ядерная их часть была людьми с IT-бекграундом — было объявлено (цитирую): «все эти перемены для обычных пользователей произойдут где-то далеко под капотом».

Люди поняли это как «вместо Presto будет WebKit, вместо Carakan будет V8, но все фичи, за которые я выбрал этот браузер, останутся». А таких фич было много, и некоторые из них так больше нигде и не реализованы.

Спойлер тринадцатилетней давности: нас обманули. На-ду-ли. Обвели вокруг пальца.

Вскоре слова «перемены под капотом» стали грустным мемом сообщества, а Опера — рескином хрома. Ну, вы знаете эту историю, не буду на ней сейчас останавливаться.

III.I Нераспиливаемый монолит UI

Можно ли было сделать всё именно так, как было объявлено вначале? Нет, и причина тому проистекает всё в том же принципе «всё своё держу в себе». Десктопный интерфейс Оперы называется Quick, и он сделан с помощью полностью самонарисованного набора виджетов. 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) даёт только:

Всё остальное — вкладки, панели, адресная строка, боковые панели, настройки — рисует Quick. Грубо говоря, интерфейс Opera примерно на 90% платформонезависим, а platforms/ — это тонкая прослойка.

В коде есть замысел создания интерфейса между движком и UI: класс OpWindowCommander содержит чистый, узкий API для встраивания. Но эта граница обходится — и модули Quick лезут во внутренности движка, используя его классы и внутренние объекты. WindowCommander в итоге стал не API встраивания, а скорее слоем уведомлений о событиях.

Но это не всё. Интерфейс и движок делят вообще всё: одну систему сборки, один общий бинарник, одни и те же идиомы обработки ошибок (OpStatus, LEAVE/TRAP), один OpString, один глобальный g_opera, один слой pi. Quick — это не приложение, которое встраивает в себя веб-движок через стабильный интерфейс. Quick компилируется внутрь движка. Это единый монолитный исполняемый файл, где UI-код вызывает движок напрямую, как соседний модуль.

Поэтому «оставить интерфейс, заменить движок» технически означает «переписать весь интерфейс заново»: вырезать все прямые обращения к Window, FramesDocument, VisualDevice, заменить модель документа, переписать связку под чужой движок, перестроить сборку. «Под капотом» такое не провернуть никак.

III.II Отговорки вместо исходников

Но почему бы не открыть бесполезные теперь исходники? Кому бы от этого стало плохо?

Вадим Макеев, тогдашний евангелист Opera (и, кстати, автор знаменитого ляпа «под капотом»), приводил три причины:

  1. Плохая задокументированность кода (которую пришлось бы писать с нуля для open source сообщества).
    Это относительный аргумент — документация есть, но неполная и не всегда актуальная. Стало бы это реальным блокером для сообщества тогда? Я не берусь судить, но код сам по себе очень самоописательный, связный; это гексагональная архитектура по духу и подходу. Плюс отличное покрытие тестами. Копаться в этом интересно.
  2. Отсутствие выгоды (на подготовку ушёл бы год работы целой команды, что для коммерческой компании бессмысленная трата денег).
    Здесь тоже нет прямой лжи. С другой стороны и убытка от раскрытия «неподготовленных» исходников как будто бы тоже не просматривается.
  3. Лицензионные ограничения (ядро Presto было сильно завязано на закрытые сторонние проприетарные библиотеки, которые нужно было бы кропотливо выпиливать).
    Самый сильный аргумент: в репозитории упоминается 45 сторонних компонентов под клубком несовместимых условий. Часть просто требует атрибуции (OpenSSL, данные 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.

    Но этот аргумент теряет свою силу, стоит нам посмотреть на слитый код. В нём только десктопные версии для Windows, Linux и macOS.
    Monotype iType использовался на устройствах без Free Type и системных шрифтов. Matrix SSL — компактный коммерческий TLS для встраиваемых устройств. Всё, остальные компоненты — под свободными лицензиями. Из однозначно нераспространяемого остались только корневые сертификаты, вырезать которые совершенно элементарно, как и прочие непубличные данные. Это однозначно не выглядит как что-то требующее месяцев работы юристов и инженеров.

Opera ASA могли открыть код десктопных версий браузера в любой момент, но предпочли этого не делать.

IV. Архитектурные бриллианты (и немного безумия)

В Опере, как вы уже поняли, предпочитали делать всё сами, когда это было возможно. Когда такой возможности не было — сторонний код интегрировался без внешних зависимостей, иногда — с собственными доработками. Такого кода достаточно много, это sqlite, zlib, libfreetype и т.п. Самое крупное и одновременно самое интересное заимствование — это форк OpenSSL, который дорабатывался самостоятельно. Модуль называется libopeay, что раскладывается как lib + op(era) + eay — инициалы Эрика Янга (Eric A. Young), автора SSLeay, той самой библиотеки, из которой в 1998 году вырос OpenSSL. Артефакты модуля показывают, что код переливался несколько раз, начиная со времени позднего SSLeay (версии 0.8.x), и до актуальной на 2012 год OpenSSL 1.0.0g. Целиком апстрим не использовался: отдельный скрипт вырезал всё лишнее — apps, demos, test, документацию, платформенные обвязки под VMS и OS/2. Потом другой скрипт оборачивал каждый оставшийся .c в свой opera_*.cpp — иначе тысячи сишных файлов OpenSSL просто не пережили бы jumbo-сборку с её едиными единицами трансляции и конфликтами имён. После этого явно шла ручная полировка напильником — то есть даже «взять готовое» у Оперы означало прокрутить чужой код через собственную мясорубку.

Но из всего OpenSSL Опера использовала только криптографию: большие числа, RSA, DH, шифры, хеши, X.509, ASN.1. А сам протокол TLS — рукопожатие, запись, сессии — написан заново, свой. OpenSSL везёт собственную реализацию SSL/TLS (каталог ssl/, файлы s3_clnt.c, t1_enc.c и прочие) — и они в дереве есть, обёрнутые в opera_s3_clnt.cpp… но помечены тегом #[external-ssl-only]. На десктопе они не компилировались вообще. Десктопный браузер шифровал через собственный стек — модуль libssl.

Зачем писать свой TLS? Затем же, зачем свой STL и свой libc: контроль и переносимость. Своя реализация укладывается в оперовскую OOM-дисциплину, в её асинхронную модель, в её менеджер сертификатов и диалоги. Граница описана декларативно — libssl просит у libopeay API криптопримитивов, и ничего сверх. Плюс к этому, в libssl есть расширения, которых в OpenSSL нет или которые Опера прикрутила по-своему:


Движок нативно (без рантайма Google Protocol Buffers) реализует protobuf. На нём реализована логика удалённой отладки, когда вы могли отлаживать страницу на телефоне прямо с десктопа. Сам отладчик — Opera Dragonfly — архитектурно был отделён от движка, и работал поверх него как веб-приложение; новые версии отладчика могли загружаться с серверов Opera без обновления самого браузера. Исходники инструмента, кстати, были открыты.


Векторная графика обрабатывается в modules/libvega, и для неё есть аж три бэкенда: программный растеризатор (vegabackend_sw), 2D-ускорение (vegabackend_hw2d) и полноценный 3D-конвейер на GPU (vegabackend_hw3d) — последний нужен для CSS-трансформаций и эффектов. Пути (VEGAPath) растеризуются алгоритмом заметающей прямой; поддерживаются градиенты, паттерны, режимы наложения.

Ниже лежит modules/libgogi — низкоуровневый графический слой с названием «Multiplatform Desktop Environment» (MDE). Это система инвалидации по регионам: отслеживаются прямоугольники экрана, требующие перерисовки, и перекрывающиеся объединяются. Решение из доGPU-эпохи, когда главным было минимизировать пересылку в фреймбуфер.


Ядро движка — однопоточное. Есть один цикл обработки сообщений, и через него проходит всё: разбор HTML, каскад CSS, раскладка, исполнение JavaScript, отрисовка. Дерево DOM и дерево боксов не защищены ни мьютексами, ни атомиками — и не нуждаются в этом, потому что к ним всегда обращается ровно один поток. Комментарии THIS IS NOT THREAD-SAFE напоминают об этом несколько раз.

Любопытная деталь: в платформенном слое есть макрос PI_CAP_SYNCOBJECT_REMOVED — вероятно в Presto когда-то были объекты синхронизации потоков, и их убрали.

Однако, в Presto существует каркас для изоляции процессов, общающихся сообщениями, что реализует не многопоточность, но многопроцессность, как в Chrome. Вернее — реализовал бы; работа так и не была никогда завершена. Однако сам подход впечатляющ.

Модуль компонента многопроцессности предоставляет сущности OpComponent/OpComponentManager/OpComponentPlatform/OpTypedMessage/OpChannel. Модель предполагает один OpComponentManager на процесс; компоненты общаются только сообщениями OpTypedMessage по каналам; адрес — тройка «менеджер.компонент.канал». Формат общения — сериализованный protobuf (component.proto: source/destination Address{componentManager, component, channel}, тип, bytes data). Транспорты вполне ожидаемые: posix_ipc под *nix, разделяемая память + события — под windows.

Каркас задумывался широким («поток или процесс», много типов воркеров), но в реальном 12.15 поточная ветка состоит из заглушек.

Единственное, ради чего этот механизм успели хоть как-то использовать — изоляция NPAPI-плагинов (Flash Player, Java Applets, etc): если плагин падал, то не тянул за собой монолит движка. Когда добавили поддержку x64, этот же механизм пригодился для запуска 32-битных плагинов через обёртку.


Pike — язык программирования, о котором вы не слышали — широко использовался в Opera. Сейчас — да и в 2012 году тоже — его использование выглядит странным, Python выглядит куда как предпочтительнее. Зачем держать Pike отдельно для сборки тестов, если у вас уже есть Python для сборки кода? Но инженеры Оперы никогда ничего не делали без причины, и мне стало интересно разобраться.

Первую зацепку дали копирайты: части компилятора тестов несут Copyright (C) 2002. Это эпоха Opera 6/7. Вторая зацепка — в реестре фич: FEATURE_3P_PIKE_UNIVERSE — «Pike… used in the Opera Mini servers». Третья — различные скрипты: генераторы таблиц интернационализации, Doxygen-раннеры, какие-то форматтеры. Pike был в Опере ещё до Python, и это был привычный команде инструмент.

Почему этот язык, а не, скажем, Perl? Формат тестов .ot — это не просто данные: внутри тестов встроены блоки C++ и JavaScript. Чтобы скомпилировать .ot в C++, парсеру нужно уметь токенизировать код C-семейства. И тут у Pike было преимущество: в его стандартной библиотеке уже лежал готовый токенизатор C/Pike — модуль Parser.Pike. Он дал разбор фигурных скобок, строк, комментариев и препроцессора C++ из коробки.

Ну а дальше действовала заповедь «работает — не трогай». Flower было удобнее написать на Python, а сборка тестов так и осталась на экзотическом интерпретаторе.

Впрочем, Perl тоже используется, сборка локализации работает на нём.


Две абсолютно легендарные фичи Presto, которые больше так нигде нормально и не появились — жесты мышью и полностью настраиваемое управление с клавиатуры. Они сделаны очень красиво с инженерной точки зрения: и жесты мышью, и клавиатурные комбинации живут в едином пространстве виртуальных клавиш OpKey::Code, описанном в modules/hardcore/module.keys:

OP_KEY_GESTURE_UP    GestureUp   gesture   (module.keys:247, + Down/Left/Right и диагонали)
OP_KEY_FLIP_BACK     FlipBack    flip      (module.keys:258, + FlipForward)

Распознаватель жестов (MouseGesture::CalculateMouseGesture) превращает штрих в направление и возвращает именно OP_KEY_GESTURE_* — а дальше скармливает его в тот же диспетчер, что и клавиатуру: InvokeKeyPressed(gesture, ...). Рокер-жесты (нажать вторую кнопку мыши, удерживая первую) синтезируют OP_KEY_FLIP_BACK/FORWARD. То есть для движка «жест влево-вниз» и «Ctrl+Z» — события одной природы, отличающиеся только полем ActionMethod { METHOD_MOUSE, METHOD_KEYBOARD, ... }.

Каждый модуль определяет список своих действий в module.actions, простые ini-конфиги описывали привязки методов к действиям в зависимости от контекста:

GestureLeft, GestureDown     = Rewind, 0
GestureRight, GestureUp      = Fast forward, 0
Platform Mac, SwipeLeft      = Back
...
z ctrl                       = Undo
1 ctrl                       = Go to speed dial, 1
Feature ExtendedShortcuts, 1 = Switch to previous page

Пользователь мог переназначить вообще всё, правя эти .ini.

Отсюда понятно, почему после миграции эти фичи «не перенесли»: это целая ядерная подсистема ввода — своё пространство виртуальных клавиш, свой распознаватель-как-часть-клавиатуры, своя компайл-тайм-таблица действий и текстовая конфигурация поверх. В модели Chromium ничего подобного нет; воспроизвести «жест — это клавиша, а привязки — редактируемый текст с наследованием по контекстам» означало бы притащить чужую архитектуру ввода целиком. Проще было не тащить — и Opera на Blink вышла без жестов, а докрутили их (частично, расширением) сильно позже и совсем иначе.


А что давала вся эта продуманная архитектура Presto? Прежде всего — переносимость.

Снова обратимся ко внутренней документации:

…there is a summary of the estimates time it takes to port Opera to a new platform, based on the ports to Kyocera and PowerTV. The figures are probably guesstimates as much as estimates, but adding them up it comes out to between 110 and 170 man-days for a port of the full browser, not including customer extras.

Перенос браузера на новую платформу команда инженеров могла уложить в месяц. Неплохо.


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

Этот доход надо было защитить, — не от пользователя, а от какого-нибудь расширения, которое могло подменять ссылки. Для этого существовал механизм Search Protection: система, защищавшая эти настройки от изменения. Файлы поисковиков подписаны RSA, выбранный движок по умолчанию охраняется контрольной суммой, и при несовпадении браузер молча откатывает поиск на подписанную копию.

В коде ещё много всего интересного и безумного, про что хочется рассказать. Набралось бы на три таких главы, но я сдерживался.

V. Разморозка и проверка работоспособности

Собрать код в Linux оказалось неожиданно просто, несмотря на устаревший тулчейн. Фактически, из экзотики требуются только Python 2.7 для Flower и Pike для тестов (Perl не считаем, любая актуальная версия в любом актуальном дистрибутиве работает), что проще всего один раз засунуть в Docker-образ. Сам движок удалось собрать на актуальном GCC 14 после одной незначительной правки и десятка флагов компилятора:

-fpermissive, -Wno-narrowing, -Wno-deprecated, -Wno-register, -Wno-class-memaccess,
-Wno-deprecated-declarations, -Wno-misleading-indentation, -Wno-address,
-Wno-stringop-overflow, -Wno-stringop-truncation, -Wno-array-bounds, -Wno-restrict,
-Wno-format-overflow, -Wno-format-truncation, -Wno-nonnull, -Wno-dangling-pointer,
-Wno-write-strings

Сборка в VS 2026 потребовала больше правок, как в языке («непонятные» современному MSVC объявления и прочие мелочи), так и в обвязке. Например, libvpx собиралась через интеграцию с ассемблером yasm. vsyasm мне завести не удалось, но враппер на питоне написать оказалось делом десяти минут. Ещё потребовалось добавить путь к 7zip (нужен для упаковки скинов) и упомянутый уже Perl.

И этого оказалось достаточно. Билды собирались и работали, тесты запускались и отрабатывали — кроме нескольких, завязанных на внутренние mock-сервера. Даже скучно.

Собирается, кстати, действительно быстро: на Intel Core Ultra 9 275HX полная сборка в один поток занимает 162 секунды, а при распараллеливании упирается в потолок около 103 секунд при 24 потоках. Такое слабое масштабирование (всего в 1,5 раза) отлично иллюстрирует специфику jumbo-сборок, где узким местом становятся не ядра процессора, а пропускная способность памяти.

Нашлись ли баги? Конечно. Расскажу один, самый интересный: Windows x64-билд падал на любой странице, в то время как на Linux x64 браузер работал без нареканий. Раскопки привели в JIT: трамплин перехода из байткода в нативный код не учитывал конвенцию вызовов Win64 — там другие регистры для передачи аргументов и обязательные 32 байта shadow space, которых unix-конвенция не знает. Плюс россыпь классики 64-битного портирования: long там, где нужен указатель, усечения HANDLE до int, и отдельный перл — современный MSVC соптимизировал в ноль таблицу обработчиков JIT-инструкций, потому что формально на неё никто «не ссылался».

Интересно, что Opera, начиная с 12 версии, выпускала x64-билды, в том числе — и для Windows, но в них, естественно, этого бага не было.

V.I Переезд на OpenSSL

Четырнадцать лет для браузера — это несколько сменившихся технологических эпох. Ещё двадцать, да даже пятнадцать лет назад новую версию браузера можно было выпускать раз в несколько месяцев, а то и полгода. Но как раз где-то с начала 2010-х процесс ускорился экспоненциально. Тот web, для которого Опера предназначалась, уже попросту не существует.

Первое, с чем мы столкнёмся — проблемы безопасности. Opera 12.15 опционально поддерживает TLS вплоть до версии 1.2, и хотя этого до сих пор достаточно для установки безопасного соединения с большинством сайтов, Opera уже не сможет проверить ни один сертификат, и на каждый чих будет сообщать о проблемах безопасности.

Как временное решение, это можно отключить в коде, заставив Оперу доверять любым сайтам. Как постоянное — нужно обновление корневых сертификатов и имплементация TLS 1.3.

SSL-стек в Opera, как вы помните, собственный, хоть и базирующийся на OpenSSL, его потолок — TLS 1.2 (код которого помечен TLS 1.2 (Jan 21, 2008: Not yet debugged)) с обменом ключами RSA/DHE и шифрами CBC/RC4/3DES. Дописывать в него TLS 1.3 с шифрованием ECDHE + AEAD технически можно, но это значит вручную повторить десять лет развития криптографии, при том что рядом лежит открытый и оттестированный OpenSSL 3. Идея отдать библиотеке протокольный слой TLS выглядит как quick win. Более того: под это даже существует инфраструктура — уже упоминавшийся Matrix SSL использовался именно в таком сценарии.

В архитектуре Presto TLS-слой — это ProtocolComm, звено в цепочке обработки соединения; рядом с родным SSLConnection встал новый OpenSSL_Record_Layer, внутри которого OpenSSL работает через memory-BIO: движок никогда не отдаёт библиотеке сокет, а перекачивает байты между её буферами и своим асинхронным транспортом. Все потребители выше и ниже по стеку не заметили подмены. Имплантация произошла фактически безболезненно, разве что пришлось заново научить Оперу считывать информацию о сертификатах и шифровании.

Проверка сертификатов при этом осталась на стороне Оперы: её хранилище, её политика доверия, её диалоги. OpenSSL совершает хендшейк, а решение «доверять или нет» принимает движок — по обновлённому набору корневых сертификатов Mozilla плюс системному хранилищу ОС (небольшая попутная доработка). Так сохранился весь родной UI безопасности, менеджер сертификатов и пользовательский опыт.

Впрочем, совсем без трудностей не обошлось. Заход на сайт с самоподписанным сертификатом «вешал» браузер. Виноват оказался не TLS: диалог предупреждения показывался невидимым — интерфейс рисовал его как оверлей поверх страницы, которой ещё нет (соединение-то приостановлено на рукопожатии). Классический баг «окно есть, но его никто не видит», существовавший в Quick UI — родной стек просто никогда не доводил дело до этого диалога.

А дальше — самое приятное. Когда протокол переехал на OpenSSL 3, вся вмороженная копия OpenSSL 1.0.0g стала не нужна. Коммит её удаления — это 2 119 файлов и без малого 470 тысяч удалённых строк. От libopeay осталось 127 файлов: заголовки да оперовские addon-расширения — но и те не выброшены, а перенесены на OpenSSL 3: асинхронная генерация ключей теперь asynch_rsa_gen_ossl3.cpp поверх EVP API, EV-данные и purposes переехали на новый X.509. То, что в Опера дописывали поверх чужой библиотеки, бережно сохранено.
Финальным штрихом добавился ключ сборки, и теперь OpenSSL можно вкомпилировать статически или подгружать системные библиотеки.

TLS 1.3 и новые сертификаты в работе

V.II Обновление стороннего кода

Кроме огромного форка OpenSSL внутри Presto вморожены сторонние библиотеки образца 2005–2012 годов: zlib, SQLite, FreeType, lcms, libwebp, Hunspell, и это не полный список. Их обновление не то, чтобы необходимо принципиально, но нам не пользоваться, нам посмотреть.

Просто обновить код получится не всегда, хотя для части библиотек всё прошло буднично или с минорными фиксами. FreeType обновился с версии 2.4.8 до 2.13.3 без регрессий, совпало всё, вплоть до того, что два «падающих» теста падают одинаково на старой и новой версии. libwebp обновился с 0.2.0 до 1.6.0, и тут уже пришлось перегенерировать тестовые ассеты, потому что новый рендер рисовал лучше. Похожая история с lcms — изменились умолчания работы с альфа-каналом.

Некоторые сторонние библиотеки содержали специфические патчи. Так в zlib Opera вживила в deflate понятие «классов» данных — свою защиту от атаки CRIME для компрессии SPDY-заголовков (куки сжимаются отдельно от остального, чтобы по размеру сжатого нельзя было угадать секрет). При обновлении до 1.3.1 этот патч пришлось перенести на современный deflate и покрыть тестами под санитайзерами. Бонус-находка: в старом форке был латентный баг конфигурации, из-за которого все уровни компрессии деградировали до самого слабого — возможно, никто в Opera так и не узнал, что их deflate-9 был deflate-1.

SQLite также был модифицирован: Opera переделала progress handler в кооперативный планировщик — виртуальная машина запросов умеет прерваться на полуслове и продолжить тот же запрос с того же опкода (стоковый SQLite после прерывания запрос убивает). Перенос патча с VDBE 3.7.9 на 3.53.3 — отдельное приключение с off-by-one на точке возобновления. Тестирование также выявило, что современный SQLite поднял размер страницы с 1К до 4К и этим взорвал квоты Web SQL, рассчитанные на килобайтные страницы.

На момент написания этого текста нетронутым остался только Hunspell: библиотека полностью изменила принцип работы и API, а интеграция с ней настолько инвазивна (редчайший случай в коде Оперы, но абсолютно обоснованный), что трогать это я пока не решаюсь.

V.III Смерть Pike и другие приключения

Попутно со всем этим приводился в соответствие со временем тулчейн. Проще всего оказалось переписать Flower с Python 2.7 на 3.13. Работа оказалась чисто механической, и большую часть взял на себя 2to3.

С заменой Pike на всё тот же Python пришлось повозиться серьёзнее. Заменить его я посчитал необходимым — слишком экзотический язык, держать который сейчас уже просто нет смысла. Старая версия Pike была запущена в Docker, через неё прогнался весь корпус тестов для получения эталонов. Дальше Python-порт сверялся с эталонами побайтово — вплоть до воспроизведения багов оригинала (Pike терял хвостовые пробелы в одном шаблоне и intern-ил мёртвые строки — порт делает так же, потому что эти байты попадают в сжатые таблицы строк). Результат: 98,3% файлов корпуса дают байт-в-байт идентичные таблицы, все 1 354 файла компилируются, и по мере включения модулей тесты пошли зелёными тысячами: util — 530/530, DOM — 2474/2479, стандартная библиотека, юникод, форматы…

Упавшие тесты оказались самыми интересными.

Тесты — скучная тема, пока они не начинают рассказывать такие истории.

Дальше настало время трогать C++. Код Оперы писался в стандартах C++98 / C++03, содержал кучу велосипедов, в том числе — макросов для эмуляции того, что позже вошло в стандарт. Сборка под современным GCC выдавала более шести тысяч предупреждений. После избавления от libopeay и обновления внутренних форков стало чуть потише.

Дальше была кропотливая работа: сначала уровень языка был поднят до C++17 без -fpermissive, затем до C++20. Ноль предупреждений на GCC/MSVC при /W4 и /permissive-, выпилены register и throw(). Код почти что «C++23-clean» — при пробной сборке новым стандартом ломаются считанные файлы, и все по одной причине, настолько красивой, что грех не рассказать.

У OpString — «крадущий» конструктор копирования: он принимает неконстантную ссылку и забирает буфер у источника. Move-семантика, написанная в 2003 году, за восемь лет до C++11. Проблема в том, что C++23 (P2266) стал агрессивнее возвращать локальные переменные как rvalue — и древний конструктор, требующий lvalue, перестал связываться.

И здесь встал главный философский вопрос реставрации: а не переписать ли всё это на «нормальный» современный C++ — std::string, std::unique_ptr, исключения? Технически — можно. Но я решил: нет. Тот самый манифест — «RTTI, STL, namespaces, exceptions: we don’t need them» — остаётся в силе. Не из ностальгии, а из соображения идентичности: Presto с std::vector внутри — это уже не Presto. Весь строй этого кода — OOM-дисциплина, свои контейнеры, свой libc — то, что хочется сохранить.

Практический компромисс, который я принял: использовать возможности языка, не использовать стандартную библиотеку. «Велосипеды» модернизируются на месте: OpAutoPtr стал move-only (с = delete у копирования), появился свой op_move — три строчки, эквивалент std::move без единого стандартного заголовка. Кстати, наивная попытка заменить OpAutoPtr на std::unique_ptr мгновенно уронила браузер на старте и тем самым отлично проиллюстрировала цену таких замен: у оперовского указателя reset(тот же указатель) — no-op, у стандартного — освобождение объекта с последующим повисшим указателем.

V.IV Музей мёртвых фич

Всяких любопытных фич в Опере было предостаточно. Экспериментировать в Осло явно любили — загибайте пальцы.

Я наверняка что-то забыл, но суть вы поняли. Ныне значительная часть этого просто не будет работать. Что-то подлежит восстановлению, что-то восстанавливать не имеет смысла.

Но это — части истории. Выкидывать их как-то неправильно; к счастью Опера сделана так, что можно просто скрыть неработающие фичи за feature-флагами. Бесполезный код остаётся там, законсервированный, но всегда готовый.

Кроме кода под macOS. Это порт под Mac OS X (Cocoa с остатками Carbon и рудиментами PowerPC-эпохи), несобираемый современным Xcode — я избавился от него без зазрения совести.

VI. Что дальше?

Сегодняшнее состояние: Presto собирается на Debian 13/GCC 14 и Visual Studio 2026, работает на Linux x64, Windows x86 и Windows x64, открывает современные HTTPS-сайты через OpenSSL 3 с TLS 1.3, прошёл через восемь обновлений зависимостей и держит зелёными тысячи собственных тестов. Для проекта, начавшегося с «а соберётся ли оно вообще», — неплохо.
Но ни о каком использовании в современном вебе речи, конечно, не идёт — в лучшем случае на сайтах мы будем видеть съехавшую разметку и неработающее содержимое. Значительная часть ресурсов не отобразится вовсе. Presto заморожен на уровне 2013 года — ES5.1 без Promise и стрелочных функций, CSS без grid и custom properties, HTTP без второй версии.

Я не хочу забегать вперёд с выводами о том, можно ли научить старую Оперу новым трюкам. Поживём — увидим.

Я не могу публиковать собранные билды или код (кроме как в виде патчей, хотя и не знаю, кто бы захотел с ними возиться). Но я открыт к предложениям и раздумываю, что можно сделать для публикации проекта.

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

Смысл в том, что Opera была браузером, который делали с любовью и пониманием. Это видно в каждой строчке, от манифеста «нам не нужен STL» до двойной радуги в грамматике CSS. Отказ в публикации исходников ощущался тогда, как какая-то несправедливость — и, судя по тому, что кто-то их всё-таки выложил, люди внутри компании сами так думали. Наверняка кто-то из этих людей хотел бы, чтобы такой разбор случился.

Ну, вот он и случился.