Задался я с задачей, ознакомится с тем как работает в том или ином виде основа всех современных браузеров — WebKit, как происходит процесс загрузки ресурсовю И что с этим всем, собственно, можно сделать. Документации по вопросу, в принципе, достаточно:
* структурированная, но не покрывающая и 10 й части от Apple;
* разбросаные статьи в вики, разные по степени детализации и степени покрытия.
Целью данной статьи является не общий взгляд на систему сверху, а как раз точечный и детальный разбор одного из процессов, происходящих в системе. Который, по моему мнению, иногда дает лучшее представление о системе в целом, нежели абстрактный взгляд. А может быть просто будет маленьким кирпичиком, который понадобится разработчику для составления из разрозненной мозайки информации своего представления о системе.
Структура
- WebKit1 — внешнее апи, дефакто для каждого порта свое;
- WebKit2 — новая его версия, с ним я не работал;
- JavascriptCore — JavaScript движок;
- WTF — Web Template Framework;
- ThirdParty — набор сторонних библиотек, вчасности leveldb;
- WebCore — базисный функционал WebKit. Как раз про него и большая часть текста. Весь функционал загружен в namespace WebCore. Состоит из подпроектов:
- loader — WebCore::FrameLoader, WebCore::DocumentLoader,WebCore::DocumentWriter — это оттуда, в общем, все что связано с коммуникацией Document <-> внешний мир — это там;
- dom — Document Object Model. WebCore::Document;
- html — WebCore::HTMLDocument — наследник WebCore::Document,WebCore::PluginDocument,WebCore::HTMLElement, его наследники — WebCore::HTMLAnchorElement и так далее. Имплементация докмументной модели HTML.
- page — WebCore::Frame,WebCore::History,WebCore::ContextMenu — все, что связано с UI или просто с высокоуровневой коммуникацией (History браузера, например);
- platform — Специфические реализации функционала для разных портов;
- rendering, css, svg, storage, plugins, inspector — названия говорят сами за себя. Детально не рассматриваются.
Процесс загрузки документа, loader
programm1.c
mainWidget = new QWebView(parent);
// mainWidget->setHtml("<html><body>HEIL</body></html>");
mainWidget->page()->mainFrame()->setHtml( "<html><body>HEIL</body></html>",QUrl() );
QWebFrame::setHtml() → QWebFrameAdapter::setHtml // qt/qtwebkit/Source/WebKit/qt/WebCoreSupport/QWebFrameAdapter.cpp:284
...
→ WebCore::FrameLoader::load // frame->loader()->load(WebCore::FrameLoadRequest(frame, request, substituteData));
// "request" description
WebCore::FrameLoadRequest(
WebCore::Frame
WebCore::ResourceRequest // Request Description - пустой урл
WebCore::SubstituteData // Data description - qt/qtwebkit/Source/WebCore/loader/SubstituteData.h
// data - WTF::RefPtr<WebCore::SharedBuffer>
// mime-type - "text/html"
// encoding - "utf-8"
// failingURL
)
) → FrameLoaderClientQt::createDocumentLoader() // RefPtr<DocumentLoader> loader = m_client->createDocumentLoader(request,substitute_data) // WebCore::FrameLoaderClient()
→ FrameLoader::load(DocumentLoader* newDocumentLoader) // FrameLoader::load(loader.get())
newDocumentLoader.m_frame = 0 ;
→ FrameLoader::loadWithDocumentLoader(newDocumentLoader, type, 0) → FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType type, PassRefPtr<FormState> prpFormState)
...
setPolicyDocumentLoader(loader); // Не только проверка полиси но устанавливает m_frame в loader.
→ loader→setFrame(m_frame);
→ m_writer.setFrame(frame);
...
// Check policies and with callback jump to callContinueLoadAfterNavigationPolicy via static jumper
FrameLoader::callContinueLoadAfterNavigationPolicy(const ResourceRequest&, PassRefPtr<FormState> formState, bool shouldContinue )
→
// Контент загружаемой страницы: m_policyDocumentLoader.get()->substituteData().content()->data();
setProvisionalDocumentLoader(m_policyDocumentLoader.get()); // m_provisionalDocumentLoader = m_policyDocumentLoader.get();
FrameLoader::continueLoadAfterWillSubmitForm
// RefPtr<DocumentLoader> m_provisionalDocumentLoader;
m_provisionalDocumentLoader→startLoadingMainResource
// DocumentLoader::m_mainResource - CachedResourceHandle<CachedRawResource>
// DocumentLoader::ResourceRequest m_request;
// Контент загружаемой страницы m_substituteData.content()->data()
→ handleSubstituteDataLoadSoon
→ handleSubstituteDataLoadNow
WebCore::ResourceResponse response(url, m_substituteData.mimeType(), m_substituteData.content()→size(), m_substituteData.textEncoding(), "");
→ DocumentLoader::responseReceived(0, response) // responseReceived(CachedResource* resource, const ResourceResponse& response)
m_response = response;
// m_m_identifierForLoadWithoutResourceLoader=1
// notifier() через него осуществлается посылка евентов в View классы.
→ frameLoader()→notifier()→dispatchDidReceiveResponse(this, m_identifierForLoadWithoutResourceLoader, m_response, 0);
→ DocumentLoader::continueAfterContentPolicy(PolicyUse); // PolicyUse is enum val enum PolicyAction {PolicyUse,PolicyDownload,PolicyIgnore};
// m_response.isHTTP()=0 isLoadingMainResource()=1 isStopping()=0
...
→ DocumentLoader::dataReceived(0, m_substituteData.content()→data(), m_substituteData.content()→size());
→ frameLoader()→notifier()→dispatchDidReceiveData(this, m_identifierForLoadWithoutResourceLoader, data, length, -1);
→ DocumentLoader::commitLoad(const char* data, int length) // data – our html data
→ frameLoader→client()→committedLoad(this, data, length); // FrameLoaderClient::committedLoad
→ FrameLoaderClientQt::committedLoad
→ void DocumentLoader::commitData(const char* bytes, size_t length) // loader->commitData(data, length) – Святой грааль работы с DocumentWriter, в documentLoader уже сконструирован m_frame.
m_writer.begin(documentURL(), false);
m_writer.setDocumentWasLoadedAsPartOfNavigation();
→ void m_writer.addData(bytes, length); // DocumentWriter::addData
...
→ finishLoading(0)
«Коллстек» выше — это цепочка вызовов от setHtml до коммуникации с Document. Если интересует процесс расстановки полиси и разнообразные виды загрузки данных, то копать надо там. Я выкинул часть переходов, оставив на мой взгляд только те, которые отражают какую-либо смысловую операцию.
Процесс, который нас интересует в «коллстеке» выше — начало записи в документ и что для этого нужно. Посмотрев, как именно готовится DocumentWriter в DocumentLoader::commitData, можно programm1.cpp «упростить» (не в смысле кода, а в смысле приближения ее «к земле»).
programm2.c
QWebPage *page = mainWidget->page();
QWebFrame *qtWebFrame = mainWidget->page()->mainFrame();
QWebFramePrivate *qtWebFramePrivate = qtWebFrame->d;
WebCore::Frame *frame = qtWebFramePrivate->frame;
WebCore::DocumentWriter m_writer(frame);
m_writer.setFrame(frame);
m_writer.begin(url, false);
m_writer.setDocumentWasLoadedAsPartOfNavigation();
m_writer.setEncoding("utf-8", true);
m_writer.addData(html ,strlen(html) );
m_writer.end();
Таким же образом надо опуститься ниже — до парсинга (семейство классов WebCore::DocumentParser). Полностью от прослойки Writer избавится не получится: вызов appendBytes содержит в себе writer, плюс Writer отвечает за создание интерфейса декодирования и коммуникацию с View: DocumentWriter::reportDataReceived
— вызвает m_frame->document()->recalcStyle(Node::Force).
void DecodedDataDocumentParser::appendBytes(DocumentWriter* writer, const char* data, size_t length) {
...
String decoded = writer->createDecoderIfNeeded()->decode(data, length);
...
writer->reportDataReceived();
...
}
Вчленив шаги инициализации из DocumentWriter::begin получим programm3.cpp:
programm3.cpp
const char *html = "<html><body>HEIL<b>IGO</b><script>document.write(1234);</script></body></html>";
size_t htmlen = strlen(html);
RefPtr<WebCore::Document> document = WebCore::DOMImplementation::createDocument("text/html", frame, url, false);
document->createDOMWindow();
frame->setDocument(document);
document->implicitOpen();
frame->script()->updatePlatformScriptObjects();
RefPtr<WebCore::DocumentParser> parser = document->parser();
WebCore::DocumentWriter writer(frame);
m_parser->appendBytes(&writer,html ,htmlen);
m_parser->finish();
PS
На этом мое путешествие в WebKit приостанавливается. Надеюсь, что для кого-либо этот текст понизит точку вхождения в проект.
В последнем разделе собраны рецепты быстрого развертывания окружения для экспериментов.
Компиляция
За основу был взят пакет qt-everywhere-opensource-src-5.3.1. Оттуда брать все не обязательно — достаточно qtbase i qtwebkit. При сборке я столкнулся со следующей проблемой: статическая сборка с включенным -debug (./configure -static -debug) флагом создает неподемные библиотеки, которые слинковать мне в рабочий пример так и не удалось на 8ГБ машине. Ну и даже если дождаться, линковки — вариант, ждать по несколько минут перекомпиляции простейших примеров не очень подходит. Без symbol info линковка занимает пару секунд, сам libWebkit.a ~ 54Mb.
С shared бибилиотекой есть другая проблема: qt — не экспортирует API webkita, а прячет его за своим. Лечится это "-fvisibility=default" вместо hidden. Для хака этого достаточно — в библиотеке нет перекрывающихся имен, для нормального же проекта надо долго и нудно перелопачивать определения экспортированных функций, используя директивы WTF_EXPORT. Про экспортинг есть информация тут, но мне не помогло.
Необходимо также раскрыть доступ к WebCore миру у QWebFrame (qtwebkit/Source/WebKit/qt/WidgetApi/qwebframe.h) — имплементация спрятана за приватной переменной QWebFramePrivate* QWebFrame::d
— ее надо сделать паблик. Тогда к WebCore::Frame достучатся можно будет через
WebCore::Frame *frame = [ QWebView ]->page()->mainFrame()->frame;
Для тестов, я, все-таки, рекомендую динамическую сборку, иначе ни gdb ни просто линковка удовольствия вам не доставят. Вот мои приблизительные настройки:
cd ./qtbase
./configure -opensource -confirm-license -release -nomake tools -nomake examples -no-compile-examples -no-opengl -no-openvg -no-egl -no-eglfs -no-sql-sqlite2 -D QT_NO_GRAPHICSVIEW -D QT_NO_GRAPHICSEFFECT -D QT_NO_STYLESHEET -D QT_NO_STYLE_CDE -D QT_NO_STYLE_CLEANLOOKS -D QT_NO_STYLE_MOTIF -D QT_NO_STYLE_PLASTIQUE -no-qml-debug -no-alsa -no-cups -no-dbus -no-directfb -no-evdev -no-glib -no-gtkstyle -no-kms -no-libudev -no-linuxfb -no-mtdev -no-nis -no-pulseaudio -no-sm -no-xinerama -no-xinput2 -no-xkb -no-xrender -openssl -openssl-linked -icu -fontconfig -system-freetype -system-libpng -system-libjpeg -system-zlib -qt-pcre -qt-harfbuzz -qt-sql-sqlite -qt-xcb -debug
make -j 8
cd ../qtwebkit
../qtbase/bin/qmake WEBKIT_CONFIG-=use_glib use_gstreamer use_gstreamer010 use_native_fullscreen_video legacy_web_audio web_audio video gamepad -o Makefile.WebCore.Target WebKit.pro
# удаляем -fvisibility=default из полученных make, может ето можно сделать и опцией.
make -f Makefile.WebCore.Target -j 8
Осталось забрать все библиотеки из ../lib/
и можно линковать проект. Минимально необходимо было:
libQt5Core.so libQt5Gui.so libQt5PrintSupport.so libQt5WebKit.so libQt5WebKitWidgets.so libQt5Widgets.so libQt5Newtork.so
libQt5Core.so.5 libQt5Gui.so.5 libQt5PrintSupport.so.5 libQt5WebKit.so.5 libQt5WebKitWidgets.so.5 libQt5Widgets.so.5 libQt5Network.so.5
Проблемы сборки
Eсли вы увидели:
This application failed to start because it could not find or load the Qt platform plugin "xcb".
Значит, для статической сборки вы забыли:
Q_IMPORT_PLUGIN(QXcbIntegrationPlugin)
А для динамической потерялся либо libxcb.so, либо если вы собрали qtbase с ключем -qt-xcb надо создать в проекте папку ./platforms с содежимым qt/qtbase/plugins/platforms (достаточно одного файла libqxcb.so).
При компиляции тестового модуля нужно отключить инлайн функции (-fno-inline-small-functions) либо взять весь список дефайнов, с которыми компилировалась библиотека, иначе, поскольку вы используете те же includes, что и скомпилированный WebKit, придется следить за всеми definами, которые попадают в заголовок. В WebCore::Document мой клиентский код из-за данной ошибки для переменной m_parser потерял 8 байт в смещении, и появлялись они — ошибки — в самых разнообразных местах.
Автор: marten_de