Visual StudioでChromeの動作を見る: Blinkの初期化処理やイベントリスナーのバックトレース取得

はじめに

Visual Studio 2010 ExpressでChromeがビルドできるようになったので、次はデバッグを試してみました。

今回は、こちらのドキュメントを参考にしました。
http://www.chromium.org/developers/how-tos/debugging

手順

1. chrome/chrome.slnを開きます。

2. Debug版のChrome.exeをVisual Studioでビルドします。

3. ソリューションエクスプローラから「chrome」を右クリックし、「スタートアッププロジェクトに設定」を選択します。

4. 「chrome」プロジェクト -> プロパティ -> デバッグ -> コマンド引数に「--single-process」を指定します。

5. 「webkit」プロジェクト -> 「src」->「WebKit.cpp」(third_party/WebKit/Source/WebKit/chromium/src/WebKit.cpp)を開きます。

6. 「void initialize(Platform* webKitPlatformSupport)」の関数の先頭にブレークポイントを設定します。

7. デバッグを開始します。

バックトレース

以下の呼び出し履歴(バックトレース)が得られます。

 	webkit.dll!WebKit::initialize(WebKit::Platform * webKitPlatformSupport)  行 114	C++
>	content.dll!content::RenderThreadImpl::EnsureWebKitInitialized()  行 628 + 0x17 バイト	C++
 	content.dll!content::RenderThreadImpl::OnCreateNewView(const ViewMsg_New_Params & params)  行 1152 + 0xf バイト	C++
 	content.dll!DispatchToMethod<content::RenderThreadImpl,void (__thiscall content::RenderThreadImpl::*)(ViewMsg_New_Params const &),ViewMsg_New_Params>(content::RenderThreadImpl * obj, void (const ViewMsg_New_Params &)* method, const Tuple1<ViewMsg_New_Params> & arg)  行 546 + 0xf バイト	C++
 	content.dll!ViewMsg_New::Dispatch<content::RenderThreadImpl,content::RenderThreadImpl,void (__thiscall content::RenderThreadImpl::*)(ViewMsg_New_Params const &)>(const IPC::Message * msg, content::RenderThreadImpl * obj, content::RenderThreadImpl * sender, void (const ViewMsg_New_Params &)* func)  行 790 + 0x82 バイト	C++
 	content.dll!content::RenderThreadImpl::OnControlMessageReceived(const IPC::Message & msg)  行 1140 + 0x84 バイト	C++
 	content.dll!content::ChildThread::OnMessageReceived(const IPC::Message & msg)  行 272 + 0x13 バイト	C++
 	ipc.dll!IPC::ChannelProxy::Context::OnDispatchMessage(const IPC::Message & message)  行 261 + 0x18 バイト	C++
 	ipc.dll!base::internal::RunnableAdapter<void (__thiscall IPC::ChannelProxy::Context::*)(IPC::Message const &)>::Run(IPC::ChannelProxy::Context * object, const IPC::Message & a1)  行 190 + 0x21 バイト	C++
 	ipc.dll!base::internal::InvokeHelper<0,void,base::internal::RunnableAdapter<void (__thiscall IPC::ChannelProxy::Context::*)(IPC::Message const &)>,void __cdecl(IPC::ChannelProxy::Context * const &,IPC::Message const &)>::MakeItSo(base::internal::RunnableAdapter<void (__thiscall IPC::ChannelProxy::Context::*)(IPC::Message const &)> runnable, IPC::ChannelProxy::Context * const & a1, const IPC::Message & a2)  行 900	C++
 	ipc.dll!base::internal::Invoker<2,base::internal::BindState<base::internal::RunnableAdapter<void (__thiscall IPC::ChannelProxy::Context::*)(IPC::Message const &)>,void __cdecl(IPC::ChannelProxy::Context *,IPC::Message const &),void __cdecl(IPC::ChannelProxy::Context *,IPC::Message)>,void __cdecl(IPC::ChannelProxy::Context *,IPC::Message const &)>::Run(base::internal::BindStateBase * base)  行 1257 + 0x2a バイト	C++
 	base.dll!base::Callback<void __cdecl(void)>::Run()  行 396 + 0xe バイト	C++
 	base.dll!base::MessageLoop::RunTask(const base::PendingTask & pending_task)  行 476	C++
 	base.dll!base::MessageLoop::DeferOrRunPendingTask(const base::PendingTask & pending_task)  行 489	C++
 	base.dll!base::MessageLoop::DoWork()  行 669 + 0xc バイト	C++
 	base.dll!base::MessagePumpForUI::DoRunLoop()  行 241 + 0x1d バイト	C++
 	base.dll!base::MessagePumpWin::RunWithDispatcher(base::MessagePump::Delegate * delegate, base::MessagePumpDispatcher * dispatcher)  行 64 + 0xf バイト	C++
 	base.dll!base::MessagePumpWin::Run(base::MessagePump::Delegate * delegate)  行 48 + 0x1c バイト	C++
 	base.dll!base::MessageLoop::RunInternal()  行 431 + 0x29 バイト	C++
 	base.dll!base::MessageLoop::RunHandler()  行 405	C++
 	base.dll!base::RunLoop::Run()  行 46	C++
 	base.dll!base::MessageLoop::Run()  行 312	C++
 	base.dll!base::Thread::Run(base::MessageLoop * message_loop)  行 153	C++
 	base.dll!base::Thread::ThreadMain()  行 197 + 0x16 バイト	C++
 	base.dll!base::`anonymous namespace'::ThreadFunc(void * params)  行 57 + 0xe バイト	C++
 	kernel32.dll!75cf33aa() 	

このバックトレースだけでも、chromeのレイヤー構造が見えてきました。
ChromeレンダリングエンジンはWebKitからフォークしたBlinkに切り替わっていますが、名前はまだWebKitのままのようです。

base.dll -> ipc.dll -> content.dll -> webkit.dll


あと、試しに、イベントリスナーっぽいクラス(EventListenerWrapper)のコンストラクタにブレークポイントを置いて、chromeで新しいタブを開くと、以下のバックトレースが得られました。先ほどとは違い、HTMLDocumentParserやWebDOMEventListenerが登場しました。

このようにバックトレースに出てくる関数を中心に攻めていくと、chromeの内部の仕組みを効率的に理解できそうです。

>	webkit.dll!WebKit::EventListenerWrapper::EventListenerWrapper(WebKit::WebDOMEventListener * webDOMEventListener)  行 48	C++
 	webkit.dll!WebKit::WebDOMEventListenerPrivate::createEventListenerWrapper(const WebKit::WebString & eventType, bool useCapture, WebCore::EventTarget * target)  行 52 + 0x28 バイト	C++
 	webkit.dll!WebKit::WebDOMEventListener::createEventListenerWrapper(const WebKit::WebString & eventType, bool useCapture, WebCore::EventTarget * target)  行 59	C++
 	webkit.dll!WebKit::WebNode::addEventListener(const WebKit::WebString & eventType, WebKit::WebDOMEventListener * listener, bool useCapture)  行 186 + 0x1d バイト	C++
 	chrome.dll!autofill::PageClickTracker::DidFinishDocumentLoad(WebKit::WebFrame * frame)  行 95 + 0x65 バイト	C++
 	content.dll!content::RenderViewImpl::didFinishDocumentLoad(WebKit::WebFrame * frame)  行 3681 + 0x52 バイト	C++
 	webkit.dll!WebKit::FrameLoaderClientImpl::dispatchDidFinishDocumentLoad()  行 408 + 0x27 バイト	C++
 	webkit.dll!WebCore::FrameLoader::finishedParsing()  行 688 + 0x15 バイト	C++
 	webkit.dll!WebCore::Document::finishedParsing()  行 4345	C++
 	webkit.dll!WebCore::HTMLConstructionSite::finishedParsing()  行 343 + 0x18 バイト	C++
 	webkit.dll!WebCore::HTMLTreeBuilder::finished()  行 2831	C++
 	webkit.dll!WebCore::HTMLDocumentParser::end()  行 766	C++
 	webkit.dll!WebCore::HTMLDocumentParser::attemptToRunDeferredScriptsAndEnd()  行 777	C++
 	webkit.dll!WebCore::HTMLDocumentParser::prepareToStopParsing()  行 214	C++
 	webkit.dll!WebCore::HTMLDocumentParser::processParsedChunkFromBackgroundParser(WTF::PassOwnPtr<WebCore::HTMLDocumentParser::ParsedChunk> popChunk)  行 453 + 0xf バイト	C++
 	webkit.dll!WebCore::HTMLDocumentParser::pumpPendingSpeculations()  行 485	C++
 	webkit.dll!WebCore::HTMLDocumentParser::didReceiveParsedChunkFromBackgroundParser(WTF::PassOwnPtr<WebCore::HTMLDocumentParser::ParsedChunk> chunk)  行 334	C++
 	webkit.dll!WTF::FunctionWrapper<void (__thiscall WebCore::HTMLDocumentParser::*)(WTF::PassOwnPtr<WebCore::HTMLDocumentParser::ParsedChunk>)>::operator()(const WTF::WeakPtr<WebCore::HTMLDocumentParser> & c, WTF::PassOwnPtr<WebCore::HTMLDocumentParser::ParsedChunk> p1)  行 254 + 0x24 バイト	C++
 	webkit.dll!WTF::BoundFunctionImpl<WTF::FunctionWrapper<void (__thiscall WebCore::HTMLDocumentParser::*)(WTF::PassOwnPtr<WebCore::HTMLDocumentParser::ParsedChunk>)>,void __cdecl(WTF::WeakPtr<WebCore::HTMLDocumentParser>,WTF::PassOwnPtr<WebCore::HTMLDocumentParser::ParsedChunk>)>::operator()()  行 523	C++
 	webkit.dll!WTF::Function<void __cdecl(void)>::operator()()  行 704 + 0x1a バイト	C++
 	webkit.dll!WTF::callFunctionObject(void * context)  行 62	C++
 	glue.dll!base::internal::RunnableAdapter<void (__cdecl*)(void *)>::Run(void * const & a1)  行 171 + 0x18 バイト	C++
 	glue.dll!base::internal::InvokeHelper<0,void,base::internal::RunnableAdapter<void (__cdecl*)(void *)>,void __cdecl(void * const &)>::MakeItSo(base::internal::RunnableAdapter<void (__cdecl*)(void *)> runnable, void * const & a1)  行 872	C++
 	glue.dll!base::internal::Invoker<1,base::internal::BindState<base::internal::RunnableAdapter<void (__cdecl*)(void *)>,void __cdecl(void *),void __cdecl(void *)>,void __cdecl(void *)>::Run(base::internal::BindStateBase * base)  行 1173 + 0x19 バイト	C++
 	base.dll!base::Callback<void __cdecl(void)>::Run()  行 396 + 0xe バイト	C++
 	base.dll!base::MessageLoop::RunTask(const base::PendingTask & pending_task)  行 476	C++
 	base.dll!base::MessageLoop::DeferOrRunPendingTask(const base::PendingTask & pending_task)  行 489	C++
 	base.dll!base::MessageLoop::DoWork()  行 669 + 0xc バイト	C++
 	base.dll!base::MessagePumpForUI::DoRunLoop()  行 241 + 0x1d バイト	C++
 	base.dll!base::MessagePumpWin::RunWithDispatcher(base::MessagePump::Delegate * delegate, base::MessagePumpDispatcher * dispatcher)  行 64 + 0xf バイト	C++
 	base.dll!base::MessagePumpWin::Run(base::MessagePump::Delegate * delegate)  行 48 + 0x1c バイト	C++
 	base.dll!base::MessageLoop::RunInternal()  行 431 + 0x29 バイト	C++
 	base.dll!base::MessageLoop::RunHandler()  行 405	C++
 	base.dll!base::RunLoop::Run()  行 46	C++
 	base.dll!base::MessageLoop::Run()  行 312	C++
 	base.dll!base::Thread::Run(base::MessageLoop * message_loop)  行 153	C++
 	base.dll!base::Thread::ThreadMain()  行 197 + 0x16 バイト	C++
 	base.dll!base::`anonymous namespace'::ThreadFunc(void * params)  行 57 + 0xe バイト	C++
 	kernel32.dll!75cf33aa() 	

chromeにパッチを投げてレビューを受けるまでのワークフロー

はじめに

chromeのWebSocketのコードを読んでいたら、コメントにtypoを発見しました。
せっかくなので、パッチを作成してレビューを受けられる状態までもっていきました。

一連のワークフローは手順の通りでできました。
http://www.chromium.org/developers/contributing-code

実際にやってみて、パッチの送信からレビュー依頼までのワークフロー(ツールも含む)がよく整っている印象でした。

ワークフロー

ブランチを作成します。

$ git checkout -b fix-typo origin/master
M       net/websockets/websocket_throttle.h
Branch fix-typo set up to track remote branch master from origin.
Switched to a new branch 'fix-typo'

コードを修正します。

$ git diff
diff --git a/net/websockets/websocket_throttle.h b/net/websockets/websocket_throttle.h
index 713e8f8..aff7305 100644
--- a/net/websockets/websocket_throttle.h
+++ b/net/websockets/websocket_throttle.h
@@ -43,7 +43,7 @@ class NET_EXPORT_PRIVATE WebSocketThrottle {

   // Checks sockets waiting in |queue_| and check the socket is the front of
   // every queue for the destination addresses of |socket|.
-  // If so, the socket can resume estabilshing connection, so wake up
+  // If so, the socket can resume establishing connection, so wake up
   // the socket.
   void WakeupSocketIfNecessary();

修正ファイルをコミット対象に加えます。

$ git add net/websockets/websocket_throttle.h

コミットします。

$ git commit

エディタが起動するので、コメントを書きます。

Fix typo in net/websockets/websocket_throttle.h

BUG=231019

「BUG=」に修正対象のバグIDを書きます。
今回はあらかじめ登録しておいたバグIDを指定しました。
typoなのでバグ登録は不要だったかもしれません。
https://code.google.com/p/chromium/issues/detail?id=231019

コメントを保存すると、コミットが完了します。

[fix-typo 6e50384] Fix typo in net/websockets/websocket_throttle.h
 1 files changed, 1 insertions(+), 1 deletions(-)

パッチを送信します。

$ git cl upload

コメントの入力を求められるので、不要なコメント行(#)を削除します。

# Enter a description of the change.
# This will displayed on the codereview site.
# The first line will also be used as the subject of the review.
Fix typo in net/websockets/websocket_throttle.h

BUG=231019

コメントを保存すると、メールとパスワードが聞かれるので入力します。
アップロードが完了すると、レビュー用のURLが発行されます。

 net/websockets/websocket_throttle.h |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
Upload server: https://codereview.chromium.org (change with -s/--server)
Email (login for uploading to https://codereview.chromium.org): xxxxxxx@gmail.com
Password for xxxxxxx@gmail.com:
Saving authentication cookies to /home/ysano/.codereview_upload_cookies
Issue created. URL: https://codereview.chromium.org/14145003
Uploading base file for net/websockets/websocket_throttle.h

発行されたレビュー用URLにアクセスします。
https://codereview.chromium.org/14145003

「Edit Issue」をクリックし、レビュアを割り当てます。
src/net/websockets/OWNERS を見ると、鵜飼さんがあったので、鵜飼さんを割り当てました。

「Publish+Mail Comments」をクリックし、メッセージを書きます。

以上でレビューの依頼が完了しました。

Visual Studio 2010 ExpressとninjaでChromiumをビルドする

はじめに

Windows 7環境が汚れてきたので、Windows 7の再インストールのついでにChromiumをビルドできるようにしました。

環境

以下の環境でChromiumをビルドできるようにします。

インストール手順の概要

大まかな手順は以下の通りです。

基本的には以下に従いました。
http://www.chromium.org/developers/how-tos/build-instructions-windows

今回は「Automatic simplified toolchain setup」の手順を踏むことにしました。
これに従うことで、以下が自動ダウンロード&デプロイされます。

ちなみにこのやり方は、2013年に入ってから整備されたようです。
http://src.chromium.org/viewvc/chrome?revision=175004&view=revision

Windows 7のインストール

再インストールしました。
余計なソフトは入っていないクリーンな状態です。
Windows 7インストール直後にWindows Updateを実施しました。

Visual Studio 2010 Expressのインストール

ここから入手してインストールしました。
http://www.microsoft.com/visualstudio/jpn/downloads

depot_toolsのインストール

Cygwinがあると便利なので、Cygwinを前提にインストールすることにしました。
Wikiの「Cygwin:」の説明に従います。
http://www.chromium.org/developers/how-tos/install-depot-tools

Cygwinをインストールします。
Cygwinのインストール時には依存パッケージ(required_packages)をインストールするように注意します。
あと、ca-certificatesパッケージも必要です。
インストールし忘れると、git cloneできません。

$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
Cloning into 'depot_tools'...
error: error setting certificate verify locations:
  CAfile: /usr/ssl/certs/ca-bundle.crt
  CApath: none while accessing https://chromium.googlesource.com/chromium/tools/depot_tools.git/info/refs
fatal: HTTP request failed

PuTTYCygもついでにインストールしておきます。
http://www.chromium.org/developers/how-tos/cygwin

コードの取得

Chromiumのビルドに必要なコードを取得します。

Gitの方がなにかと俊敏に動くのでGitを使います。
https://code.google.com/p/chromium/wiki/UsingGit

$ git config --global user.name "My Name"
$ git config --global user.email "my@email"
$ git config --global core.autocrlf false
$ git config --global core.filemode false

$ fetch blink --nosvn=True

説明によると、gclient syncしないように、とあったので、--nohooksを指定してフックだけ走らせないようにしました。

$ gclient sync --nohooks

ツールチェーンのインストール(自動)

cygwinからは実行できないとあったので、コマンドプロンプトを立ち上げて以下を実行しました。

Cygwinpythonが使えるか不安だったので、Python 2.7を別途インストールしておきました。

C:\Python27\python src\tools\win\toolchain\toolchain.py

しばらくすると、ネットからダウンロードしてきたツールチェーンがデプロイされます。

環境変数の設定

ツールチェーンのデプロイが完了すると、「win_toolchain」フォルダ以下に「env.bat」が生成されます。

@echo off
:: Generated by tools\win\toolchain\toolchain.py.
:: Targeting VS2010.
set GYP_DEFINES=windows_sdk_path="c:\cygwin\home\fixme\win_toolchain\win8sdk" component=shared_library
set GYP_MSVS_VERSION=2010e
set GYP_MSVS_OVERRIDE_PATH=c:\cygwin\home\fixme\win_toolchain
set GYP_GENERATORS=ninja
set GYP_PARALLEL=1
set WDK_DIR=c:\cygwin\home\fixme\win_toolchain\WDK
set DXSDK_DIR=c:\cygwin\home\fixme\win_toolchain\DXSDK
set WindowsSDKDir=c:\cygwin\home\fixme\win_toolchain\win8sdk
echo Environment set for toolchain in c:\cygwin\home\fixme\win_toolchain.
cd /d c:\cygwin\home\fixme\win_toolchain\..

このバッチファイルは、コマンドプロンプトでしか使えないので(Cygwinを使うと決めたので)、バッチファイルは実行はせず、Windows環境変数にそれぞれの値を設定することにしました。

ただし1点変更を加えました。
「Using both Ninja and MSBuild」にあるように、「GYP_GENERATORS」が「ninja,msvs」となるようにしました。
Visual Studioでコードを見たいためです。こうすることで、Visual Studioのソリューションファイル(*.sln)も生成されます。
http://www.chromium.org/developers/how-tos/build-instructions-windows

gclient syncの実行

環境変数を設定したので、反映させるためCygwinシェルを新たに起動します。
その上で以下のコマンドを実行します。

$ cd src
$ gclient sync

ninjaでのビルドに必要なファイルとVisual Studioのソリューションファイル等が生成されます。

ninjaによるビルド

Cygwinシェルで続けて以下を実行します。ビルド時間を計測するため、timeコマンドを先頭に追加します。

time ninja -C out/Debug chrome

これでビルドが始まります。

Windowsのタスクマネージャでリソースを監視していると、ビルド中は全コアのCPUが100%稼動状態になりました。

今回試した環境では、これくらいの時間でビルドできました。

[...]
[13835/13836] CXX obj\chrome\common\chrome.crash_keys.obj
[13836/13836] LINK chrome.exe

real    96m51.894s
user    0m0.841s
sys     0m2.166s

Chromiumの起動確認

ビルドが成功すると、out/Debug/chrome.exeが生成されます。
上がビルドしたChromiumで、下が現時点で最新のChromeです。
バージョン番号からも最新のChromiumがビルドできたことが確認できます。


Visual Studioでのソースコードブラウズ

src/chrome/chrome.slnを開くとVisual Studioが起動します。
Intellisenseのデータベース構築がはじまります。
Intellisenseの準備が完了すると、クラスや関数の定義が簡単に参照できるようになります。

WebKit: QtWebKitのビルドとはじめてのWebKit Hack

はじめに

今回は、Google ChromeiPhoneのMobile Safariなどで採用されているレンダリングエンジンWebKitをより身近な存在に感じられるようにします。スタンダードなパソコンとLinuxC/C++の知識が多少あれば、WebKitのHackの入り口まで一気に到達できます。と言っても、僕も今その入り口に着いたばかりです・・

これまでの経緯とビルド戦略

ソフトウェアのビルドの失敗を解決する作業は、大抵骨の折れる作業と経験的にわかっています。特に大規模ソフトウェアでは、はまり出すと解決に多大な時間がかかります。放棄したくなってしまいます。

WebKitMacWindows(Visual Studio)、Gtk、Qtなど様々なターゲットでビルドできます。今までVisual StudioGtkでビルドを試みたのですが、よごれた環境(これしか無く)の上に環境構築しようとして既存のソフトとバッティングしてうまくいかなかったり、ドキュメントの記述が古そうだったりと、何度やってもうまくいきませんでした。

それでしぶとく次にQtを試しました。QtWebKitのビルドドキュメントを読んでいると、Build Botの構築手順が示されてました。

それで閃きました。これならうまくいくんじゃないかと。

Build Botの環境構築は誰がやっても作れるようなreproducible(再現可能)な手順になっているべきで、多分そうなっているんじゃないかと。

それと今回はしょうもない失敗を避けるため、クリーンな環境を用意することにしました。

結局これで正解でした。はまることはほとんど無く、すんなりWebKitをビルドできました。

環境

  • Ubuntu 12.04 (on Virtual Box)

http://www.ubuntulinux.jp/download/ja-remix-vhd にある仮想ハードディスクイメージをtorrentで取得しました。インストール直後で何も汚れていません。

QtWebKitのビルド

手順は http://trac.webkit.org/wiki/QtWebKitGardening の通りで、1点を除いて問題ありませんでした。

Wikiに「Qt is installed into /usr/local/Trolltech (normal user doesn't have write permission for it by default」と注意書きがあるのですが、これを見逃すとうまくインストールできません。予め/usr/local/Trolltechを作成してオーナーを自分にしておく必要があります。ここにQtがインストールされるためです。ディレクトリの作成とオーナー変更をしていないと、Qtのインストールで失敗してしまいます。

% sudo mkdir /usr/local/Trolltech
% sudo chown psan:psan /usr/local/Trolltech

あとはドキュメントの通りです。

% sudo apt-get install python-software-properties
% sudo add-apt-repository ppa:u-szeged/sedkit
% sudo apt-get update
% sudo apt-get install sedkit-env-qtwebkit

% sudo add-apt-repository ppa:gstreamer-developers/ppa
% sudo apt-get update
% sudo apt-get install gstreamer1.0 libgstreamer-plugins-base1.0-dev

% git clone git://github.com/ossy-szeged/qt5-tools.git
% ./qt5-tools/build-qt5.sh

WebKitのcloneは時間がかかるので、先にcloneしておきます。

% git clone git://gitorious.org/qtwebkit/testfonts.git
% git clone git://git.webkit.org/WebKit.git WebKit

環境変数をビルドの度に指定するのは面倒なので、ビルドスクリプトのラッパーを作成します。スクリプト名はbuild_webkit.shとしました。ビルドの過程はteeでログファイルに残すようにしています。

#!/bin/sh

. /home/psan/qt5-tools/build-qt5-env

export WEBKIT_TESTFONTS=/home/psan/testfonts
export TZ=America/Los_Angeles
export QTDIR=/usr/local/Trolltech/Qt5/$QT_WEEKLY_REV
export PATH=/usr/local/Trolltech/Qt5/$QT_WEEKLY_REV/bin:$PATH

time /home/psan/WebKit/Tools/Scripts/build-webkit --qt 2>&1 | tee build-webkit.log

WebKitのcloneが終わったら、ビルドします。WebKitのビルドはかなりのCPUリソースを使うため、Virtual Boxを設定して、8個あるCPUのうち、6個割り当てておきました。これで並列コンパイルの恩恵が受けられます。

% chmod +x build_webkit.sh
% ./build_webkit.sh
[...]
====================================================================
 WebKit is now built (35m:55s). 
 To run QtTestBrowser with this newly-built code, use the
 "WebKit/Tools/Scripts/run-launcher" script.
====================================================================
10656.65user 1070.15system 35:56.36elapsed 543%CPU (0avgtext+0avgdata 1647024maxresident)k
291984inputs+789912outputs (348major+123511928minor)pagefaults 0swaps

36分でビルドできました。

さっそくQtTestBrowserを起動してみます。

% ./WebKitBuild/Release/bin/QtTestBrowser

ビルドしたばかりの最新のWebKitGoogleを表示できました。

はじめてのWebKit Hack: CSSを完全無視して色を強制指定

すぐに視覚的に結果がわかる修正を考えました。CSSの色指定を本来とは違うように解釈させることができれば、レンダリング結果を見れば、自分のコードが反映されたことが一目瞭然だろうと。これはWebKitCSS実装を読みはじめた時に思いついたアイデアです。はやく試したくてたまりませんでした。

さっそくCSSの色指定のパースするコードを見てみます。色をパースすると言っても様々なパターンがあるようです。今回は#RRGGBBな色指定だけとりあえず対象にできれば良しとして、コードをたどっていくと、Color::parseHexColor()にたどり着きました。

Source/WebCore/css/CSSParser.cpp

bool CSSParser::parseColor(RGBA32& color, const String& string, bool strict)
{
    // First try creating a color specified by name, rgba(), rgb() or "#" syntax.
    if (fastParseColor(color, string, strict))
        return true;
[...]

bool CSSParser::fastParseColor(RGBA32& rgb, const String& name, bool strict)
{
    unsigned length = name.length();
    bool parseResult;

    if (!length)
        return false;

    if (name.is8Bit())
        parseResult = fastParseColorInternal(rgb, name.characters8(), length, strict);
    else
        parseResult = fastParseColorInternal(rgb, name.characters(), length, strict);
[...]

template <typename CharacterType>
static inline bool fastParseColorInternal(RGBA32& rgb, const CharacterType* characters, unsigned length , bool strict)
{
    CSSPrimitiveValue::UnitTypes expect = CSSPrimitiveValue::CSS_UNKNOWN;

    if (!strict && length >= 3) {
        if (characters[0] == '#') {
            if (Color::parseHexColor(characters + 1, length - 1, rgb))
                return true;
        } else {
            if (Color::parseHexColor(characters, length, rgb))
                return true;
        }
    }
[...]

ここでCSSの色指定の16進数表現を内部表現(int)に変換しているようです。rgbに色をつっこめば所望の色に変更できそうです。

WebCore/platform/graphics/Color.cpp

// originally moved here from the CSS parser
template <typename CharacterType>
static inline bool parseHexColorInternal(const CharacterType* name, unsigned length, RGBA32& rgb)
{
    if (length != 3 && length != 6)
        return false;
    unsigned value = 0;
    for (unsigned i = 0; i < length; ++i) {
        if (!isASCIIHexDigit(name[i]))
            return false;
        value <<= 4;
        value |= toASCIIHexValue(name[i]);
    }
    if (length == 6) {
        rgb = 0xFF000000 | value;
        return true;
    }
    // #abc converts to #aabbcc
    rgb = 0xFF000000
        | (value & 0xF00) << 12 | (value & 0xF00) << 8
        | (value & 0xF0) << 8 | (value & 0xF0) << 4
        | (value & 0xF) << 4 | (value & 0xF);
    return true;
}

差分は次の通りです。CSSのパース中に得られた色情報を完全に無視して、常に#00FF00(緑)にしてみることにしました。makeRGB()は同じくColor.cppにあったので使うことにしました。

% git diff
diff --git a/Source/WebCore/platform/graphics/Color.cpp b/Source/WebCore/platform/graphic
index 04dc73e..0c13f22 100644
--- a/Source/WebCore/platform/graphics/Color.cpp
+++ b/Source/WebCore/platform/graphics/Color.cpp
@@ -128,6 +128,8 @@ RGBA32 makeRGBAFromCMYKA(float c, float m, float y, float k, float a)
 template <typename CharacterType>
 static inline bool parseHexColorInternal(const CharacterType* name, unsigned length, RGB
 {
+    rgb = makeRGB(0, 255, 0);
+    return true;
     if (length != 3 && length != 6)
         return false;
     unsigned value = 0;

再びビルドします。フルビルドでは36分かかりましたが、今回は24秒でした。局所的な修正のビルドなのでかなりはやいです。

% ./build_webkit.sh
[...]
====================================================================
 WebKit is now built (00m:24s). 
 To run QtTestBrowser with this newly-built code, use the
 "WebKit/Tools/Scripts/run-launcher" script.
====================================================================
20.41user 3.08system 0:24.02elapsed 97%CPU (0avgtext+0avgdata 1647104maxresident)k
0inputs+106880outputs (0major+446479minor)pagefaults 0swaps

試しにこんなHTMLをレンダリングさせてみました。違いが分かるようにFirefoxの結果も並べてみます。

<html>
<body>
<style type="text/css">
.message
{
    color: #00FFFF;
}
</style>
</body>
<p class="message">This text is actually sky blue. <br>But what color you can see with the modifed WebKit?</p>
</html>

おー!FirefoxCSSの通り解釈して、Hack版WebKitCSSを完全に無視して#00FF00(緑)になりました。

WebKit: fontショートハンドプロパティのパース

今回はショートハンドプロパティであるfontプロパティのパースについて見ていきます。
ここだけ読んでも、CSS2.1の仕様とWebKitの実装の対応関係がとれているのがなんとなく見えてきます。

15.8 Shorthand font property: the 'font' property

'font'
Value: [ [ <'font-style'> || <'font-variant'> || <'font-weight'> ]? <'font-size'> [ / <'line-height'> ]? <'font-family'> ] | caption | icon | menu | message-box | small-caption | status-bar | inherit
Initial: see individual properties
Applies to: all elements
Inherited: yes
Percentages: see individual properties
Media: visual
Computed value: see individual properties

http://www.w3.org/TR/CSS21/fonts.html#font-shorthand

前回見た通り、プロパティ値のパースはCSSParser::parseValue()が入り口になっている。
fontプロパティに対応するcaseが見つかる。

Source/WebCore/css/CSSParser.cpp

bool CSSParser::parseValue(CSSPropertyID propId, bool important)
{
[...]
    case CSSPropertyFont:
        // [ [ 'font-style' || 'font-variant' || 'font-weight' ]? 'font-size' [ / 'line-height' ]?
        // 'font-family' ] | caption | icon | menu | message-box | small-caption | status-bar | inherit
        if (id >= CSSValueCaption && id <= CSSValueStatusBar)
            validPrimitive = true;
        else
            return parseFont(important);
        break;
[...]

CSSParser::parseFont()を見る。

Source/WebCore/css/CSSParser.cpp

// [ 'font-style' || 'font-variant' || 'font-weight' ]? 'font-size' [ / 'line-height' ]? 'font-family'
bool CSSParser::parseFont(bool important)
{
    // Let's check if there is an inherit or initial somewhere in the shorthand.
    for (unsigned i = 0; i < m_valueList->size(); ++i) {
        if (m_valueList->valueAt(i)->id == CSSValueInherit || m_valueList->valueAt(i)->id == CSSValueInitial)
            return false;
    }

    ShorthandScope scope(this, CSSPropertyFont);
    // Optional font-style, font-variant and font-weight.
    bool fontStyleParsed = false;
    bool fontVariantParsed = false;
    bool fontWeightParsed = false;
    CSSParserValue* value;
    while ((value = m_valueList->current())) {
        if (!fontStyleParsed && isValidKeywordPropertyAndValue(CSSPropertyFontStyle, value->id, m_context)) {
            addProperty(CSSPropertyFontStyle, cssValuePool().createIdentifierValue(value->id), important);
            fontStyleParsed = true;
        } else if (!fontVariantParsed && (value->id == CSSValueNormal || value->id == CSSValueSmallCaps)) {
            // Font variant in the shorthand is particular, it only accepts normal or small-caps.
            addProperty(CSSPropertyFontVariant, cssValuePool().createIdentifierValue(value->id), important);
            fontVariantParsed = true;
        } else if (!fontWeightParsed && parseFontWeight(important))
            fontWeightParsed = true;
        else
            break;
        m_valueList->next();
    }

    if (!value)
        return false;

    if (!fontStyleParsed)
        addProperty(CSSPropertyFontStyle, cssValuePool().createIdentifierValue(CSSValueNormal), important, true);
    if (!fontVariantParsed)
        addProperty(CSSPropertyFontVariant, cssValuePool().createIdentifierValue(CSSValueNormal), important, true);
    if (!fontWeightParsed)
        addProperty(CSSPropertyFontWeight, cssValuePool().createIdentifierValue(CSSValueNormal), important, true);

    // Now a font size _must_ come.
    // <absolute-size> | <relative-size> | <length> | <percentage> | inherit
    if (!parseFontSize(important))
        return false;

    value = m_valueList->current();
    if (!value)
        return false;

    if (isForwardSlashOperator(value)) {
        // The line-height property.
        value = m_valueList->next();
        if (!value)
            return false;
        if (!parseLineHeight(important))
            return false;
    } else
        addProperty(CSSPropertyLineHeight, cssValuePool().createIdentifierValue(CSSValueNormal), important, true);

    // Font family must come now.
    RefPtr<CSSValue> parsedFamilyValue = parseFontFamily();
    if (!parsedFamilyValue)
        return false;

    addProperty(CSSPropertyFontFamily, parsedFamilyValue.release(), important);

    // FIXME: http://www.w3.org/TR/2011/WD-css3-fonts-20110324/#font-prop requires that
    // "font-stretch", "font-size-adjust", and "font-kerning" be reset to their initial values
    // but we don't seem to support them at the moment. They should also be added here once implemented.
    if (m_valueList->current())
        return false;

    return true;
}

CSS2.1の仕様書にもあるBNF記法が、関数の先頭行にコメントとして転記されている。この関数は、このコメントのBNFにそって処理が構成されている。

大きくは3つ。

  • 1. Optional font-style, font-variant and font-weight.
  • 2. Now a font size _must_ come.
  • 3. Font family must come now.

最初のコメントにある「Let's check if there is an inherit or initial somewhere in the shorthand.」ここはあまりよく理解できていない。inheritやinitialがプロパティ値に含まれている場合は、さっさと処理を切り上げているようだ。パースしても無駄になるからだろうか。

ShorthandScopeが目を引いた。ShorthandScopeはスコープをうまく使って、CSSParserでショートハンドプロパティのパースの状態管理を実現しているようだ。ShorthandScopeのローカル変数が作られたタイミングでコンストラクタが走り、スコープを抜けるタイミングでデストラクタが走る。CSSParser::parseFont()はreturnでスコープを抜けるケースが多くあり、手動で状態遷移するよりも、スコープにまかせたほうが漏れなく確実に所望の処理を行える。m_currentShorthandは初期状態がCSSPropertyInvalidなので、ショートハンドプロパティのパース中は、そのプロパティのIDを保持して、パースが終われば、元の状態のCSSPropertyInvalidに戻す、という動作をするのだろう。

class ShorthandScope {
    WTF_MAKE_FAST_ALLOCATED;
public:
    ShorthandScope(CSSParser* parser, CSSPropertyID propId) : m_parser(parser)
    {
        if (!(m_parser->m_inParseShorthand++))
            m_parser->m_currentShorthand = propId;
    }
    ~ShorthandScope()
    {
        if (!(--m_parser->m_inParseShorthand))
            m_parser->m_currentShorthand = CSSPropertyInvalid;
    }

private:
    CSSParser* m_parser;
};

ここからはそれほど面白くはなく、CSSの仕様とWebKitの実装がうまく対応がとれているなあ、といったくらい。

// Optional font-style, font-variant and font-weight. を見る。
font-style、font-variant、font-weightは省略可能なので、あればプロパティとして認識する。
もしも無ければ、「 cssValuePool().createIdentifierValue(CSSValueNormal)」で「normal」(デフォルト値)として認識するわけだろう。

次に // Now a font size _must_ come. でfont sizeをパースする。
「/」(Forward Slash Operator)が見つかったら、line heightをパースする。

最後に、Font family must come now. でfont familyをパースする。

WebKit: CSSプロパティと値の組(宣言)のパース

今回はCSSのプロパティと値の組(宣言)についてWebKitの実装を読んでみる。

つまり、CSSの以下の仕様。

4.1.8 Declarations and properties

A declaration is either empty or consists of a property name, followed by a colon (:), followed by a property value. Around each of these there may be white space.

http://www.w3.org/TR/CSS21/syndata.html#declaration

WebKitではCSSのパースはbisonで実現されている。
実装を見てみる。

Source/WebCore/css/CSSGrammar.y.in

declaration:
[...]
    |
    property ':' maybe_space expr prio {
        $$ = false;
        bool isPropertyParsed = false;
        if ($1 && $4) {
            parser->m_valueList = parser->sinkFloatingValueList($4);
            int oldParsedProperties = parser->m_parsedProperties.size();
            $$ = parser->parseValue(static_cast<CSSPropertyID>($1), $5);
            if (!$$)
                parser->rollbackLastProperties(parser->m_parsedProperties.size() - oldParsedProperties);
            else
                isPropertyParsed = true;
            parser->m_valueList = nullptr;
        }
        parser->markPropertyEnd($5, isPropertyParsed);
    }
[...]
property:
    IDENT maybe_space {
        $$ = cssPropertyID($1);
    }
  ;

prio:
    IMPORTANT_SYM maybe_space { $$ = true; }
    | /* empty */ { $$ = false; }
  ;

expr:
    term {
        $$ = parser->createFloatingValueList();
        $$->addValue(parser->sinkFloatingValue($1));
    }
    | expr operator term {
        $$ = $1;
        if ($$) {
            if ($2) {
                CSSParserValue v;
                v.id = 0;
                v.unit = CSSParserValue::Operator;
                v.iValue = $2;
                $$->addValue(v);
            }
            $$->addValue(parser->sinkFloatingValue($3));
        }
    }
    | expr invalid_block_list {
        $$ = 0;
    }
    | expr invalid_block_list error {
        $$ = 0;
    }
    | expr error {
        $$ = 0;
    }
  ;
[...]
operator:
    '/' maybe_space {
        $$ = '/';
    }
  | ',' maybe_space {
        $$ = ',';
    }
  | /* empty */ {
        $$ = 0;
  }
  ;

「property ':' maybe_space expr prio」がポイントで、プロパティと値の組(宣言)を表している。
例えば、

h1 { font-family: sans-serif !important }

があった場合、propertyは「font-family」、maybe_spaceは「半角スペース」、exprは「sans-serif」、prioは「true」になる。

表にまとめると、

規則 説明 バインド先
property プロパティ名 $1
: コロン $2
maybe_space 省略可能なスペース $3
expr プロパティ値(式) $4
prio !important宣言の有無 $5

sinkFloatingValueList($4)でパース中のプロパティ値リスト($4)をCSSParserのm_valueListに一時保存する。
一時保存されたプロパティ値リストは、プロパティ値のパース中に参照される。
パースが済んだら、一時保存した値リストを白紙に戻す。

ちなみに、プロパティ値リストの要素数は大抵の場合、1つになりそうだが、font-familyプロパティのようなもので活躍するのだろう。

15.3 Font family: the 'font-family' property

The property value is a prioritized list of font family names and/or generic family names. Unlike most other CSS properties, component values are separated by a comma to indicate that they are alternatives:

body { font-family: Gill, Helvetica, sans-serif }
http://www.w3.org/TR/CSS21/fonts.html#font-family-prop

CSSParserは、パース済みのプロパティーリストを保持している。
もしプロパティ値のパースに失敗したら、パース中に追加されたプロパティーを無かったことに(ロールバック)する。

rollbackLastProperties()は巻き戻すプロパティ数を引数に取る。これから、パース済のプロパティリストは順序付のデータ構造であることが読み取れる。実際に実装を確認すると、Vectorで実現されていた。

Source/WebCore/css/CSSParser.h

class CSSParser {
[...]
    ParsedPropertyVector m_parsedProperties;

少し戻ってproperty規則を見てみる。

property規則のアクションでcssPropertyID()が呼ばれている。これは、プロパティ値(文字列)からハッシュ表をもとにID(整数)に変換しているだけの処理。目的は高速化で、CSSのパース中やその後の処理で文字列よりも軽量な整数IDに変換しておいたほうが、速度面で有利だから。
cssPropertyID()で少し面白かったのは、

Source/WebCore/css/CSSParser.cpp

template <typename CharacterType>
static CSSPropertyID cssPropertyID(const CharacterType* propertyName, unsigned length)
{
    char buffer[maxCSSPropertyNameLength + 1 + 1]; // 1 to turn "apple"/"khtml" into "webkit", 1 for null character

    for (unsigned i = 0; i != length; ++i) {
        CharacterType c = propertyName[i];
        if (c == 0 || c >= 0x7F)
            return CSSPropertyInvalid; // illegal character
        buffer[i] = toASCIILower(c);
    }
    buffer[length] = '\0';

    const char* name = buffer;
    if (buffer[0] == '-') {
#if ENABLE(LEGACY_CSS_VENDOR_PREFIXES)
        // If the prefix is -apple- or -khtml-, change it to -webkit-.
        // This makes the string one character longer.
        if (hasPrefix(buffer, length, "-apple-") || hasPrefix(buffer, length, "-khtml-")) {
            memmove(buffer + 7, buffer + 6, length + 1 - 6);
            memcpy(buffer, "-webkit", 7);
            ++length;
        }
#endif
#if PLATFORM(IOS)
        cssPropertyNameIOSAliasing(buffer, name, length);
#endif
    }

    const Property* hashTableEntry = findProperty(name, length);
    const CSSPropertyID propertyID = hashTableEntry ? static_cast<CSSPropertyID>(hashTableEntry->id) : CSSPropertyInvalid;

    static const int cssPropertyHistogramSize = numCSSProperties;
    if (hasPrefix(buffer, length, "-webkit-") && propertyID != CSSPropertyInvalid) {
        int histogramValue = propertyID - firstCSSProperty;
        ASSERT(0 <= histogramValue && histogramValue < cssPropertyHistogramSize);
        HistogramSupport::histogramEnumeration("CSS.PrefixUsage", histogramValue, cssPropertyHistogramSize);
    }

    return propertyID;
}

ちなみに、ハッシュテーブルは、Source/WebCore/css/makeprop.plのperlスクリプトからCSSPropertyNames.inを読み込んで生成される。
Source/WebCore/css/CSSPropertyNames.in

//
// CSS property names
//
// Some properties are used internally, but are not part of CSS. They are used to get
// HTML4 compatibility in the rendering engine.
//
// Microsoft extensions are documented here:
// http://msdn.microsoft.com/workshop/author/css/reference/attributes.asp
// 

// high-priority property names have to be listed first, to simplify the check
// for applying them first.
color
direction
display
font
font-family
font-size
[...]

CSSParser::parseValue()にもぐってみる。CSSPropertyIDに対応するswitch文が大量に存在することがわかる。ここではfont-familyのパースを追いかけてみる。
先ほど見たプロパティ値リストであるm_valueListが早速登場した。
m_valueList->current()で先頭を取得して、値が無くなるまでwhile (value)でループしてるのがわかる。
実装詳細はまだ読み解けないが、コンマ区切りのリスト構造を構築していることはわかる。

Source/WebCore/css/CSSParser.cpp

bool CSSParser::parseValue(CSSPropertyID propId, bool important)
{
[...]
    case CSSPropertyFontFamily:
        // [[ <family-name> | <generic-family> ],]* [<family-name> | <generic-family>] | inherit
    {
        parsedValue = parseFontFamily();
        break;
    }
[...]
PassRefPtr<CSSValueList> CSSParser::parseFontFamily()
{
    RefPtr<CSSValueList> list = CSSValueList::createCommaSeparated();
    CSSParserValue* value = m_valueList->current();

    FontFamilyValueBuilder familyBuilder(list.get());
    bool inFamily = false;

    while (value) {
        if (value->id == CSSValueInitial || value->id == CSSValueInherit || value->id == CSSValueDefault)
            return 0;
        CSSParserValue* nextValue = m_valueList->next();
        bool nextValBreaksFont = !nextValue ||
                                 (nextValue->unit == CSSParserValue::Operator && nextValue->iValue == ',');
        bool nextValIsFontName = nextValue &&
            ((nextValue->id >= CSSValueSerif && nextValue->id <= CSSValueWebkitBody) ||
            (nextValue->unit == CSSPrimitiveValue::CSS_STRING || nextValue->unit == CSSPrimitiveValue::CSS_IDENT));

        if (value->id >= CSSValueSerif && value->id <= CSSValueWebkitBody) {
            if (inFamily)
                familyBuilder.add(value->string);
            else if (nextValBreaksFont || !nextValIsFontName)
                list->append(cssValuePool().createIdentifierValue(value->id));
            else {
                familyBuilder.commit();
                familyBuilder.add(value->string);
                inFamily = true;
            }
        } else if (value->unit == CSSPrimitiveValue::CSS_STRING) {
            // Strings never share in a family name.
            inFamily = false;
            familyBuilder.commit();
            list->append(cssValuePool().createFontFamilyValue(value->string));
        } else if (value->unit == CSSPrimitiveValue::CSS_IDENT) {
            if (inFamily)
                familyBuilder.add(value->string);
            else if (nextValBreaksFont || !nextValIsFontName)
                list->append(cssValuePool().createFontFamilyValue(value->string));
            else {
                familyBuilder.commit();
                familyBuilder.add(value->string);
                inFamily = true;
            }
        } else {
            break;
        }

        if (!nextValue)
            break;

        if (nextValBreaksFont) {
            value = m_valueList->next();
            familyBuilder.commit();
            inFamily = false;
        }
        else if (nextValIsFontName)
            value = nextValue;
        else
            break;
    }
    familyBuilder.commit();

    if (!list->length())
        list = 0;
    return list.release();
}

WebKit: コード探索

はじめに

久しぶりにブログを書いてみたくなりました。WebKitに興味が少し出てきているので、学んだことなどをメモとして書いていこうと思います。

コンパイル時assert

コンパイル時assertの面白い例。コメントで型のサイズを肥大化させないこと、と書くよりも効果的。WebKitのような大規模開発で設計者の意図/複雑さを統制する有効な手段の1つと思う。

WebKit/Source/WebCore/css/CSSValue.cpp

struct SameSizeAsCSSValue : public RefCounted<SameSizeAsCSSValue> {
    uint32_t bitfields;
};

COMPILE_ASSERT(sizeof(CSSValue) == sizeof(SameSizeAsCSSValue), CSS_value_should_stay_small);