Трохи про нутрощах 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


Проблеми складання
Якщо ви побачили:
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 байт в зміщенні, і з'являлися вони — помилки — в найрізноманітніших місцях.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.