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

ueyexvk-uewnyghxhvtud7vugrq.jpeg

Большинство людей привыкли, что 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 

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) с помощью метода GetSwitches. Все остальные аргументы возвращаются в виде base::StringVector (по сути, vectоr). Этих знаний достаточно, чтобы реализовать код для задачи:

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, который уже определяет, какой из клиентов задействовать. Упрощённая схема выглядит так:

7d6b9fe45ac8c5a47d51e4522735e5eb.png

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

Для создания 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(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 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/. Там же есть кнопка «Откликнуться».

© Habrahabr.ru