Большинство людей привыкли, что 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",
# vvvvvvvv
"//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/. Там же есть кнопка «Откликнуться».
Комментарии (34)
F0iL
13.06.2019 19:49+2Так-то в Хромиуме есть еще и Views, так что вполне реально и даже нативный GUI на нем делать.
ziv2012
13.06.2019 22:21-6Тянуть Chromium на десктоп — это так себе занятие. В принципе не нужно тянуть монстра без необходимости. Авторы приложений на електроне должны приплачивать пользователям за используемые ресурсы.
F0iL
13.06.2019 22:33+3Вы статью читали? Речь идёт вообще не про Electron.
Никакой монструозности тут не будет, все ненужно вырежет линковщик при линковке.
P.S. А на Electron кстати тоже есть хорошие приложения, например отличный VS Code.
staticlab
13.06.2019 23:08+2Не обращайте внимания, у комментатора видимо душевная травма от электрона, что видно по его комментариям, а о чём на самом деле статья он так и не понял.
mapcuk
13.06.2019 23:41Многие жалуются на Electron, а хотелось бы
почитать более подробную статью с результатами профилирования или более объективного анализа. Может не так уж и страшен чёрт как его малюют.
ziv2012
14.06.2019 09:49-130 Гб кода загрузить только для того, чтобы сделать загрузку странички — это отличная работа в мире :)
F0iL
14.06.2019 11:54+2А вы посмотрите, какие оптимизации применяют современные JS- и WASM-движки для ускорения производительности (JIT там только вершина айсберга), какое количество медиа- и стриминговых технологий поддерживают современные браузеры (плюс интеграция с другими устройствами типа chromecast, DLNA и DIAL), почитайте про актуальные стандарты CSS (это вообще по сути дела тьюринг-полный язык теперь), в конце концов, загляните в современные веб-стандарты от W3C и WHATWG, где будут не только HTML и все что с ним связано, но и SVG, WebAudio, WebGL, WebUSB, Accessibility (в том числе распознавание и синтез речи), и еще очень много всего.
И браузер все это должен поддерживать. И работать на разных платформах, от десктопов до крохотных телефонов и встраиваемых систем. И давать разработчикам удобно иметь с этим дело.
Поэтому когда речь заходит о современных браузерах, простым «сделать загрузку странички» тут дело далеко не ограничивается. современные браузеры — это среды исполнения веб-приложений, по сложности они уже приблизились чуть ли не к операционным системам и IDE, и это вовсе не потому что их разработчикам просто так захотелось.
Mako_357
16.06.2019 20:16-1У него бэк на C++, иначе был таким же тупым, как все, что делается на электроне
staticlab
16.06.2019 21:39+1Да что вы говорите? https://github.com/microsoft/vscode:
- TypeScript 92.8%
- JavaScript 3.8%
- CSS 2.4%
- Inno Setup 0.7%
- HTML 0.2%
- Shell 0.1%
staticlab
13.06.2019 23:09Я правильно понял, что для своего приложения потребуется также закоммитить и файлы хромиума? Или они будут как-то отдельно?
F0iL
13.06.2019 23:17Если не хочется тащить Хромиум в кодовую базу, теоретически можно собирать его отдельно как набор shared-библиотек (это у них называется component build) и цеплять их. Правда, хедеры все равно понадобятся.
Dmitri-D
14.06.2019 07:55есть проект cef для встраивания chromium. Довольно удобная вещь и там заголовков минимум
F0iL
14.06.2019 11:45Это вообще другое, CEF — это для встраивания хромиумовского WebView в приложения.
Dmitri-D
16.06.2019 08:39Статья — про использование проекта Chromium, а CEF — это большой и хорошо работающий пример использования проекта Chromium.
staticlab
16.06.2019 13:13Статья — про использование наработок проекта Chromium в качестве основы для своего приложения, а CEF — проект для встраивания браузера в своё приложение.
Jowerd
14.06.2019 09:41Кто-нибудь, скажите пожалуйста, почему в Google 75 выпилили поддержку *.mhtml?
Быдлокод в Chromium зашёл так далеко, что поломал поддержку *.mhtml и сотрудники Google решили не париться над его исправлением?
Или *.mhtml ухудшает загрузку страниц?
Кто в теме, объясните пожалуйста.F0iL
14.06.2019 12:25Код для работы с MHTML никуда не делся.
75-ая версия нормально открывает сохраненные *.mht-файлы, а на Android, насколько я знаю, MHTML используется для сохранения offline pages.
Почему выпилили явную возможность сохранения на десктопах — вопрос хороший, на баг-трекере уже об этом спросили, причем сами же участники проекта.
Возможно это связано с тем, что при загрузке MHTML, уже давно осознанно отключено выполнение JavaScript на страницах по соображениям безопасности за неимением возможности как-либо это пофиксить в принципе (кстати, репортер получил за эту дыру награду в 1000$), в итоге, учитывая тотальную обскриптованность современного веба, эту фичу посчитали малополезной для обычных пользователей и выпилили из интерфейса. Но это только догадки.Tufed
14.06.2019 14:07Она и так была отключена по-умолчанию для всех смертных, и только некоторые, кому действительно нужно — гуглили как её включить у себя обратно. А так и это выпилили. Кому мешала отключенная функция — действительно хз. Тем более что на андройде она осталась.
F0iL
14.06.2019 14:21Тут ниже написали, что исчезла только галочка из chrome://settings, а аргумент командной строки по-прежнему на месте.
Нагуглить его можно и сейчас: peter.sh/experiments/chromium-command-line-switches (Ctrl-F «mhtml»)
Whuthering
14.06.2019 12:58Я нашел обсуждение:
groups.google.com/a/chromium.org/forum/#!msg/offline-dev/qO1B8kkWkoM/kO30ODfRBgAJ
TL;DR: разработчики решили, что эта фича нужна только для других разработчиков, и убрали флаг из интерфейса.
Опция командной строки "--save-page-as-mhtml" осталась и работает.Jowerd
14.06.2019 15:34Только что проверил, но опция --save-page-as-mhtml НЕ работает в Win 7.
Похоже, придётся в старых версиях сохранять mhtml, а в новых уже бегать по вебу.
Теперь я на своей шкуре ощутил заботу Google.Whuthering
14.06.2019 16:28Вы явно что-то делаете не так.
Всё отлично работает, более того, при запуске с этой опцией вариант «Webpage, Single file» становится активным по умолчанию.
https://habrastorage.org/webt/hk/xx/lt/hkxxltuufdbs0-1s7fjoea0p0k0.png
valis
14.06.2019 11:53Спасибо за статью.
Вроде вполне логично, но никогда не задумывался о таких кейсах использования этого движка.
worldmind
Не особо вчитывался, но есть что-то в этой идее, ибо как понимаю качество кода там высокое, все кому не лень статикчекеры прогоняют, конкурсы на уязвимости проходят.
ziv2012
Скажем так качество кода так себе!
F0iL
Скажите честно, вы действительно изучали код Chromiun?
Если да, то каких именно его частей?
Я с этой кодовой базой работаю каждый день, и могу сказать, что он написан действительно очень и очень неплохо, насколько это возможно для такого большого и сложного проекта.
Используются современные стандарты плюсов, код покрывается тестами и постоянно рефакторится, всякие ASAN'ы и подобное используются.
Да, иногда попадаются плохо документированные или страшнодревние места (типа layout'илки в Blink), но и по их улучшению работа ведётся.
Поэтому не совсем понятен ваш наезд на качество кода Chromium.
ziv2012
Изучаю webrtc. Постоянно возникает желание прибить программистов за их код. Я не помню, чтобы у меня какая-то фича завелась сразу же. Последнее AudioDeviceModule. Если создать свой указатель даже на стандартный код и передать в CreatePeerConnectionFactory, опа не работает. Если код надо постоянно рефакторить, значит он изначально делается некорректно. Этот рефакторинг бесит. Если в одной версии работает, то выкачиваем более новую версию — опа не работает. Да как же так-то :) Возвращают указатель, который может быть nullptr и его вызывают без проверки на nullptr — могут получить av. Но кого это волнует :)
До 64 версии msvc++ компилировал библиотеку FFmpeg — а потом вдруг перестал.
Приходится все заново пересобирать. И это только часть проблем! Это нормально?
staticlab
Какое отношение к Гуглу и Хромиуму имеет FFmpeg?
F0iL
Подавляюще большая часть WebRTC для Chromium — это внешняя зависимость, и хоть и он и хостится на googlesource, по факту его пилит совершенно другая команда, и в разработке так же активно участвуют, например, люди из Mozilla и многих других компании, и более того — Mozilla использует тот же самый код у себя в Firefox.
Рефакторится старый код.
Что для проекта с более чем 10-летней историей (а некоторые компоненты ведут свою родословную вообще с начала 2000-х), который активно развивается, является естественным и необходимым процессом.
wataru
Приглашаю Вас оставить гневный, но конструктивный комментарий в WebRTC баг трекере. Судя по объему претензий в Вашем комментарии, может быть даже Вам стоит составить несколько баг-репортов.
Если это действительно косяк, то его исправят. Если вы используте какие-то недокументированные, не стандартизованные вещи, то никто не гарантирует, что оно при следующем обновлении не сломается. Так везде.
Подпишитесь на mailing list, что бы быть в курсе грядущих изменений. Если API как-то меняется, то заранее бросают PSA там. Там же можно попросить о помощи и посоветоваться о вашем сценарии использования.