company_banner

Chromium — это не только браузер, но и хороший фреймворк



    Большинство людей привыкли, что Chromium — это и браузер, и основа для других браузеров. До недавнего времени я тоже так думал, но, изучая эту тему уже пару месяцев, я начал открывать другой дивный мир. Chromium — это огромная экосистема, в которой есть всё: и система зависимостей, и система кроссплатформенной сборки, и компоненты почти на все случаи жизни. Так почему же не попробовать создавать свои приложения, используя всю эту мощь?

    Под катом небольшое руководство, как начать это делать.

    Подготовка окружения


    В статье я буду использовать Ubuntu 18.04, порядок действий для других ОС можно посмотреть в документации:


    Для выполнения последующих шагов необходимы Git и Python. Если они не установлены, то их необходимо поставить с помощью команды:

    sudo apt install git python

    Установка depot_tools


    depot_tools — это набор инструментов для разработки Chromium. Для его установки необходимо выполнить:

    git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

    И добавить путь в переменную окружения PATH:

    export PATH="$PATH:/path/to/depot_tools"

    Важно: если depot_tools были скачаны в домашнюю папку, то не используйте ~ в переменной PATH, иначе могут возникнуть проблемы. Необходимо использовать переменную $HOME:

    export PATH="$PATH:${HOME}/depot_tools"

    Получение кода


    Для начала надо создать папку для исходников. Например, в домашней директории (необходимо около 30 Гб свободного места):

    mkdir ~/chromium && cd ~/chromium

    После этого можно скачать исходники с помощью утилиты fetch из depot_tools:

    fetch --nohooks --no-history chromium

    Теперь можно пойти попить чай/кофе, так как процедура небыстрая. Для экспериментов история не нужна, поэтому используется флаг --no-history. С историей будет ещё дольше.

    Установка зависимостей


    Все исходники лежат в папке src, идём в неё:

    cd src

    Теперь нужно поставить все зависимости с помощью скрипта:

    ./build/install-build-deps.sh

    И запустить хуки:

    gclient runhooks

    На этом подготовка окружения завершена.

    Система сборки


    В качестве основной системы сборки Chromium используется Ninja, а утилита GN применяется для генерирования .ninja-файлов.

    Чтобы понять, как пользоваться этими инструментами, предлагаю создать тестовую утилиту example. Для этого в папке src надо создать подпапку example:

    mkdir example

    Затем в папке src/example надо создать файл BUILD.gn, который содержит:

    executable("example") {
     sources = [
       "example.cc",
     ]
    }

    BUILD.gn состоит из цели (исполняемого файла example) и списка файлов, которые необходимы для сборки цели.

    Следующим шагом надо создать сам файл example.cc. Для начала предлагаю сделать классическое приложение «Hello world»:

    #include <iostream>
    
    int main(int argc, char **argv) {
       std::cout << "Hello world" << std::endl;
    
       return 0;
    }

    Исходный код можно найти на GitHub.

    Чтобы GN узнала о новом проекте, нужно в файле BUILD.gn, который находится в src, в разделе deps добавить строку "//example":

    ...
    group("gn_all") {
     testonly = true
    
     deps = [
       ":gn_visibility",
       "//base:base_perftests",
       "//base:base_unittests",
       "//base/util:base_util_unittests",
       "//chrome/installer",
       "//chrome/updater",
       "//net:net_unittests",
       "//services:services_unittests",
       "//services/service_manager/public/cpp",
       "//skia:skia_unittests",
       "//sql:sql_unittests",
       "//third_party/flatbuffers:flatbuffers_unittests",
       "//tools/binary_size:binary_size_trybot_py",
       "//tools/ipc_fuzzer:ipc_fuzzer_all",
       "//tools/metrics:metrics_metadata",
       "//ui/base:ui_base_unittests",
       "//ui/gfx:gfx_unittests",
       "//url:url_unittests",
    
       # ↓↓↓↓↓↓↓↓
       "//example",
     ]
     ...

    Теперь необходимо вернуться в папку src и сгенерировать проект с помощью команды:

    gn gen out/Default

    GN также позволяет подготовить проект для одной из поддерживаемых IDE:

    • eclipse
    • vs
    • vs2013
    • vs2015
    • vs2017
    • vs2019
    • xcode
    • qtcreator
    • json

    Более подробную информацию можно получить с помощью команды:

    gn help gen

    Например, для работы с проектом example в QtCreator надо выполнить команду:

    gn gen --ide=qtcreator --root-target=example out/Default

    После этого можно открыть проект в QtCreator:

    qtcreator out/Default/qtcreator_project/all.creator

    Финальный шаг — сборка проекта с помощью Ninja:

    autoninja -C out/Default example

    На этом краткое ознакомление с системой сборки можно завершить.

    Приложение можно запустить с помощью команды:

    ./out/Default/example

    И увидеть Hello world. На самом деле, про систему сборки в Chromium можно написать отдельную статью. Возможно, и не одну.

    Работа с командной строкой


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

    Задача: вывести на экран все аргументы, переданные приложению в стиле Chromium.
    Для работы с командной строкой необходимо в example.cc подключить заголовочный файл:

    #include "base/command_line.h"

    А также надо не забыть в BUILD.gn добавить зависимость от проекта base. BUILD.gn должен выглядеть так:

    executable("example") {
     sources = [
       "example.cc",
     ]
    
     deps = [
       "//base",
     ]
    }

    Теперь всё необходимое будет подключено к example.

    Для работы с командной строкой Chromium предоставляет синглтон base::CommandLine. Чтобы получить ссылку на него, надо использовать статический метод base::CommandLine::ForCurrentProcess, но сначала надо его инициализировать с помощью метода base::CommandLine::Init:

    base::CommandLine::Init(argc, argv);
    
    auto *cmd_line = base::CommandLine::ForCurrentProcess();

    Все аргументы, переданные приложению в командной строке и начинающиеся с символа - возвращаются в виде base::SwitchMap (по сути, map<string, string>) с помощью метода GetSwitches. Все остальные аргументы возвращаются в виде base::StringVector (по сути, vectоr<striпg>). Этих знаний достаточно, чтобы реализовать код для задачи:

    for (const auto &sw : cmd_line->GetSwitches()) {
       std::cout << "Switch " << sw.first << ": " << sw.second << std::endl;
    }
    
    for (const auto &arg: cmd_line->GetArgs()) {
       std::cout << "Arg " << arg << std::endl;
    }

    Полную версию можно найти на GitHub.

    Чтобы собрать и запустить приложение надо выполнить:

    autoninja -C out/Default example
    ./out/Default/example arg1 --sw1=val1 --sw2 arg2

    На экран будет выведено:

    Switch sw1: val1
    Switch sw2:
    Arg arg1
    Arg arg2

    Работа с сетью


    В качестве второго и последнего на сегодня примера предлагаю поработать с сетевой частью Chromium.

    Задача: вывести на экран содержимое URL'а, переданного в качестве аргумента.

    Сетевая подсистема Chromium


    Сетевая подсистема довольно большая и сложная. Входной точкой для запросов к HTTP, HTTPS, FTP и другим data-ресурсам является URLRequest, который уже определяет, какой из клиентов задействовать. Упрощённая схема выглядит так:



    Полную версию можно посмотреть в документации.

    Для создания URLRequest'а необходимо использовать URLRequestContext. Создание контекста — довольно сложная операция, поэтому рекомендуется использовать URLRequestContextBuilder. Он проинициализирует все необходимые переменные значениями по умолчанию, но, при желании, их можно поменять на свои, например:

    net::URLRequestContextBuilder context_builder;
    
    context_builder.DisableHttpCache();
    context_builder.SetSpdyAndQuicEnabled(true /* http2 */, false /* quic */);
    context_builder.SetCookieStore(nullptr);

    Многопоточность


    Сетевой стек Chromium расчитан на работу в многопоточной среде, поэтому пропустить эту тему нельзя. Базовыми объектами для работы с многопоточностью в Chromium являются:

    • Task — задача для выполнения, в Chromium это функция с типом base::Callback, которую можно создать с помощью base::Bind.
    • Task queue — очередь задач для выполнения.
    • Physical thread — кроссплатформенная обёртка над потоком операционной системы (pthread в POSIX или CreateThread() в Windows). Реализовано в классе base::PlatformThread, не используйте напрямую.
    • base::Thread — реальный поток, который бесконечно обрабатывает сообщения из выделенной очереди задач; не рекомендуется создавать их напрямую.
    • Thread pool — пул потоков с общей очередью задач. Реализован в классе base::ThreadPool. Как правило, создают один экземпляр. Задачи в него отправляются с помощью функций из base/task/post_task.h.
    • Sequence or Virtual thread — виртуальный поток, который использует реальные потоки и может переключаться между ними.
    • Task runner — интерфейс для постановки задач, реализован в классе base::TaskRunner.
    • Sequenced task runner — интерфейс для постановки задач, который гарантирует, что задачи будут исполнены в том же порядке, в каком пришли. Реализовано в классе base::SequencedTaskRunner.
    • Single-thread task runner — аналогичен предыдущему, но гарантирует, что все задачи будут выполнены в одном потоке ОС. Реализовано в классе base::SingleThreadTaskRunner.

    Реализация


    Некоторые компоненты Chromium требуют наличия base::AtExitManager — это класс, позволяющий зарегистрировать операции, которые надо выполнить при завершении приложения. Использовать его очень просто, необходимо в стеке создать объект:

    base::AtExitManager exit_manager;

    Когда exit_manager выйдет из области видимости, все зарегистрированные callback'и будут выполнены.

    Теперь нужно позаботиться о наличии всех необходимых компонентов многопоточности для сетевой подсистемы. Для этого нужно создать Thread pool, Message loop с типом TYPE_IO для обработки сетевых сообщений, и Run loop — основной цикл программы:

    base::ThreadPool::CreateAndStartWithDefaultParams("downloader");
      
    base::MessageLoop msg_loop(base::MessageLoop::TYPE_IO);
    base::RunLoop run_loop;

    Дальше нужно с помощью Context builder'а создать Context:

    auto ctx = net::URLRequestContextBuilder().Build();

    Чтобы послать запрос, необходимо с помощью метода CreateRequest объекта ctx создать объект URLRequest. В качестве параметров передаются:

    • URL, строка с типом GURL;
    • приоритет;
    • делегат, который обрабатывает события.

    Делегат представляет собой класс, реализующий интерфейс net::URLRequest::Delegate. Для данной задачи он может выглядеть так:

    class MyDelegate : public net::URLRequest::Delegate {
    public:
       explicit MyDelegate(base::Closure quit_closure) : quit_closure_(std::move(quit_closure)),
                                                         buf_(base::MakeRefCounted<net::IOBuffer>(BUF_SZ)) {}
    
       void OnReceivedRedirect(net::URLRequest *request, const net::RedirectInfo &redirect_info,
                               bool *defer_redirect) override {
           std::cerr << "redirect to " << redirect_info.new_url << std::endl;
       }
    
      void OnAuthRequired(net::URLRequest* request, const net::AuthChallengeInfo& auth_info) override {
           std::cerr << "auth req" << std::endl;
       }
    
       void OnCertificateRequested(net::URLRequest *request, net::SSLCertRequestInfo *cert_request_info) override {
           std::cerr << "cert req" << std::endl;
       }
    
       void OnSSLCertificateError(net::URLRequest* request, int net_error, const net::SSLInfo& ssl_info, bool fatal) override {
           std::cerr << "cert err" << std::endl;
       }
    
       void OnResponseStarted(net::URLRequest *request, int net_error) override {
           std::cerr << "resp started" << std::endl;
           while (true) {
               auto n = request->Read(buf_.get(), BUF_SZ);
               std::cerr << "resp read " << n << std::endl;
    
               if (n == net::ERR_IO_PENDING)
                   return;
    
               if (n <= 0) {
                   OnReadCompleted(request, n);
                   return;
               }
    
               std::cout << std::string(buf_->data(), n) << std::endl;
           }
       }
       void OnReadCompleted(net::URLRequest *request, int bytes_read) override {
           std::cerr << "completed" << std::endl;
           quit_closure_.Run();
       }
    
    private:
       base::Closure quit_closure_;
       scoped_refptr<net::IOBuffer> buf_;
    };

    Вся основная логика находится в обработчике события OnResponseStarted: содержимое ответа вычитывается, пока не произойдёт ошибка или будет нечего читать. Так как после чтения ответа нужно завершить приложение, то делегат должен иметь доступ к функции, которая прервёт основной Run loop, в данном случае используется callback типа base::Closure.

    Теперь всё готово для отправки запроса:

    MyDelegate delegate(run_loop.QuitClosure());
    
    auto req = ctx->CreateRequest(GURL(args[0]), net::RequestPriority::DEFAULT_PRIORITY, &delegate);
    req->Start();

    Чтобы запрос начал обрабатываться, надо запустить Run loop:

    run_loop.Run();

    Полную версию можно найти на GitHub.

    Чтобы собрать и запустить приложение нужно выполнить:

    autoninja -C out/Default example
    out/Default/example "https://example.com/"

    Финал


    На самом деле, в Chromium можно найти много полезных кубиков и кирпичиков, из которых можно строить приложения. Он постоянно развивается, что, с одной стороны, является плюсом, а с другой стороны, регулярные изменения API не дают расслабиться. Например, в последнем релизе base::TaskScheduler превратился в base::ThreadPool, к счастью, без изменения API.

    P.S. Мы ищем ведущего программиста на C++ в свою команду! Если чувствуете в себе силы, то наши пожелания описаны тут: team.mail.ru/vacancy/4641/. Там же есть кнопка «Откликнуться».
    Mail.ru Group
    1 331,34
    Строим Интернет
    Поддержать автора
    Поделиться публикацией

    Комментарии 34

      +5
      Не особо вчитывался, но есть что-то в этой идее, ибо как понимаю качество кода там высокое, все кому не лень статикчекеры прогоняют, конкурсы на уязвимости проходят.
        –12
        Скажем так качество кода так себе!
          +10
          Скажите честно, вы действительно изучали код Chromiun?
          Если да, то каких именно его частей?
          Я с этой кодовой базой работаю каждый день, и могу сказать, что он написан действительно очень и очень неплохо, насколько это возможно для такого большого и сложного проекта.
          Используются современные стандарты плюсов, код покрывается тестами и постоянно рефакторится, всякие ASAN'ы и подобное используются.
          Да, иногда попадаются плохо документированные или страшнодревние места (типа layout'илки в Blink), но и по их улучшению работа ведётся.
          Поэтому не совсем понятен ваш наезд на качество кода Chromium.
            –5
            Изучаю webrtc. Постоянно возникает желание прибить программистов за их код. Я не помню, чтобы у меня какая-то фича завелась сразу же. Последнее AudioDeviceModule. Если создать свой указатель даже на стандартный код и передать в CreatePeerConnectionFactory, опа не работает. Если код надо постоянно рефакторить, значит он изначально делается некорректно. Этот рефакторинг бесит. Если в одной версии работает, то выкачиваем более новую версию — опа не работает. Да как же так-то :) Возвращают указатель, который может быть nullptr и его вызывают без проверки на nullptr — могут получить av. Но кого это волнует :)
            До 64 версии msvc++ компилировал библиотеку FFmpeg — а потом вдруг перестал.
            Приходится все заново пересобирать. И это только часть проблем! Это нормально?
              +6

              Какое отношение к Гуглу и Хромиуму имеет FFmpeg?

                +5
                Изучаю webrtc
                WebRTC — это все-таки не Chromium, обсуждаемый в статье,
                Подавляюще большая часть WebRTC для Chromium — это внешняя зависимость, и хоть и он и хостится на googlesource, по факту его пилит совершенно другая команда, и в разработке так же активно участвуют, например, люди из Mozilla и многих других компании, и более того — Mozilla использует тот же самый код у себя в Firefox.
                Если код надо постоянно рефакторить, значит он изначально делается некорректно.
                Рефакторится старый код.
                Что для проекта с более чем 10-летней историей (а некоторые компоненты ведут свою родословную вообще с начала 2000-х), который активно развивается, является естественным и необходимым процессом.
                  +5

                  Приглашаю Вас оставить гневный, но конструктивный комментарий в WebRTC баг трекере. Судя по объему претензий в Вашем комментарии, может быть даже Вам стоит составить несколько баг-репортов.


                  Если это действительно косяк, то его исправят. Если вы используте какие-то недокументированные, не стандартизованные вещи, то никто не гарантирует, что оно при следующем обновлении не сломается. Так везде.


                  Подпишитесь на mailing list, что бы быть в курсе грядущих изменений. Если API как-то меняется, то заранее бросают PSA там. Там же можно попросить о помощи и посоветоваться о вашем сценарии использования.

            +3

            Так-то в Хромиуме есть еще и Views, так что вполне реально и даже нативный GUI на нем делать.

              –13
              Тянуть Chromium на десктоп — это так себе занятие. В принципе не нужно тянуть монстра без необходимости. Авторы приложений на електроне должны приплачивать пользователям за используемые ресурсы.
                +5

                Вы статью читали? Речь идёт вообще не про Electron.
                Никакой монструозности тут не будет, все ненужно вырежет линковщик при линковке.


                P.S. А на Electron кстати тоже есть хорошие приложения, например отличный VS Code.

                  +8

                  Не обращайте внимания, у комментатора видимо душевная травма от электрона, что видно по его комментариям, а о чём на самом деле статья он так и не понял.

                    +2

                    Многие жалуются на Electron, а хотелось бы
                    почитать более подробную статью с результатами профилирования или более объективного анализа. Может не так уж и страшен чёрт как его малюют.

                      –5
                      30 Гб кода загрузить только для того, чтобы сделать загрузку странички — это отличная работа в мире :)
                        +4
                        А вы посмотрите, какие оптимизации применяют современные JS- и WASM-движки для ускорения производительности (JIT там только вершина айсберга), какое количество медиа- и стриминговых технологий поддерживают современные браузеры (плюс интеграция с другими устройствами типа chromecast, DLNA и DIAL), почитайте про актуальные стандарты CSS (это вообще по сути дела тьюринг-полный язык теперь), в конце концов, загляните в современные веб-стандарты от W3C и WHATWG, где будут не только HTML и все что с ним связано, но и SVG, WebAudio, WebGL, WebUSB, Accessibility (в том числе распознавание и синтез речи), и еще очень много всего.
                        И браузер все это должен поддерживать. И работать на разных платформах, от десктопов до крохотных телефонов и встраиваемых систем. И давать разработчикам удобно иметь с этим дело.
                        Поэтому когда речь заходит о современных браузерах, простым «сделать загрузку странички» тут дело далеко не ограничивается. современные браузеры — это среды исполнения веб-приложений, по сложности они уже приблизились чуть ли не к операционным системам и IDE, и это вовсе не потому что их разработчикам просто так захотелось.
                        –2
                        У него бэк на C++, иначе был таким же тупым, как все, что делается на электроне
                  –10
                  Это не фреймворк — это чудище, жрущее память и ресурсы.
                    0

                    Я правильно понял, что для своего приложения потребуется также закоммитить и файлы хромиума? Или они будут как-то отдельно?

                      0
                      Если не хочется тащить Хромиум в кодовую базу, теоретически можно собирать его отдельно как набор shared-библиотек (это у них называется component build) и цеплять их. Правда, хедеры все равно понадобятся.
                        0
                        есть проект cef для встраивания chromium. Довольно удобная вещь и там заголовков минимум
                          0

                          Это же совсем другое.

                            0
                            Это вообще другое, CEF — это для встраивания хромиумовского WebView в приложения.
                              0
                              Статья — про использование проекта Chromium, а CEF — это большой и хорошо работающий пример использования проекта Chromium.
                                0

                                Статья — про использование наработок проекта Chromium в качестве основы для своего приложения, а CEF — проект для встраивания браузера в своё приложение.

                        –2
                        Кто-нибудь, скажите пожалуйста, почему в Google 75 выпилили поддержку *.mhtml?
                        Быдлокод в Chromium зашёл так далеко, что поломал поддержку *.mhtml и сотрудники Google решили не париться над его исправлением?
                        Или *.mhtml ухудшает загрузку страниц?

                        Кто в теме, объясните пожалуйста.
                          +1
                          Код для работы с MHTML никуда не делся.
                          75-ая версия нормально открывает сохраненные *.mht-файлы, а на Android, насколько я знаю, MHTML используется для сохранения offline pages.
                          Почему выпилили явную возможность сохранения на десктопах — вопрос хороший, на баг-трекере уже об этом спросили, причем сами же участники проекта.
                          Возможно это связано с тем, что при загрузке MHTML, уже давно осознанно отключено выполнение JavaScript на страницах по соображениям безопасности за неимением возможности как-либо это пофиксить в принципе (кстати, репортер получил за эту дыру награду в 1000$), в итоге, учитывая тотальную обскриптованность современного веба, эту фичу посчитали малополезной для обычных пользователей и выпилили из интерфейса. Но это только догадки.
                            +1
                            Она и так была отключена по-умолчанию для всех смертных, и только некоторые, кому действительно нужно — гуглили как её включить у себя обратно. А так и это выпилили. Кому мешала отключенная функция — действительно хз. Тем более что на андройде она осталась.
                          +1
                          Я нашел обсуждение:
                          groups.google.com/a/chromium.org/forum/#!msg/offline-dev/qO1B8kkWkoM/kO30ODfRBgAJ

                          TL;DR: разработчики решили, что эта фича нужна только для других разработчиков, и убрали флаг из интерфейса.

                          Опция командной строки "--save-page-as-mhtml" осталась и работает.
                            0
                            Только что проверил, но опция --save-page-as-mhtml НЕ работает в Win 7.
                            Похоже, придётся в старых версиях сохранять mhtml, а в новых уже бегать по вебу.

                            Теперь я на своей шкуре ощутил заботу Google.
                              0
                              Вы явно что-то делаете не так.
                              Всё отлично работает, более того, при запуске с этой опцией вариант «Webpage, Single file» становится активным по умолчанию.
                              https://habrastorage.org/webt/hk/xx/lt/hkxxltuufdbs0-1s7fjoea0p0k0.png
                              image
                          +2
                          Спасибо за статью.
                          Вроде вполне логично, но никогда не задумывался о таких кейсах использования этого движка.
                            +1
                            Опишите, пожалуйста, кейс когда это реально нужно.
                            • НЛО прилетело и опубликовало эту надпись здесь

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое