Большинство людей привыкли, что 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/. Там же есть кнопка «Откликнуться».
Автор: svistunov