Немного о внутренностях WebKit

в 5:43, , рубрики: c++, qt, Qt 5, tutorial, webkit

Задался я с задачей, ознакомится с тем как работает в том или ином виде основа всех современных браузеров — 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

Источник

* - обязательные к заполнению поля