Gecko hacking tutorial

現在、このページは Gyuque により執筆途中です。内容が頻繁に変わりますのでご注意ください。

はじめに

この文書は、Mozilla の心臓部である Gecko を Hack するための手順を紹介します。

準備

環境

この文書は、以下の環境を想定しています。

OS Microsoft Windows
コンパイラ Microsoft Visual Studio.NET 2003
対象となるMozilla Mozilla Firefox 2.0.0.2 (Gecko 1.8.1.2)

ソースコードの取得

何よりもまず、Mozilla のソースコードが必要です。この記事では、(この記事の執筆時点で)Firefoxの最新リリースである Firefox 2.0.0.2 を使用します。Firefox 2.0.0.2 のソースコードは、以下から入手可能です。

http://ftp.mozilla.org/pub/mozilla.o....0.0.2/source/

ソースコードはbzipアーカイブです。後で展開作業を行いますので、適当な場所に保存しておいてください。

コンパイラの準備

Windows Build Prerequisitesによると、この記事が対象としている Firefox 2.0.x をビルドするためには、Visual Studio 6 か .NET 2002 (7.0)、もしくは .NET 2003 (7.1) が必要です。これらはすべて旧バージョンです。最新版である Visual Studio 2005 (8.0) は無償で配布されていますが、これでビルドを試みても、どこかで失敗するでしょう。

旧バージョンの Visual Studio を入手する手順は少々面倒です。最新版 (2005) のライセンスを購入した上で、Microsoft からインストール用のディスクを購入する必要があります。 もしあなたが、学生や研究者ならば、ソフトウェアの管理者に相談してみてください。あなたの研究室(等)が MSDNAA を契約していれば、旧バージョンの Visual Studio のインストールディスクがあるかもしれません。

無事ライセンスとインストールディスクを入手したら、通常通りの手順でインストールしてください。もちろん、Visual C++ をインストールするのを忘れずに!

MASM32の準備

Windows Build Prerequisitesにはなぜか記述がありませんが、Mozilla のコードにはアセンブリも含まれているので、ビルドの過程でアセンブラが必要となります。この記事では、 MASM32 というアセンブラを使用します。MASM32 は、以下から入手可能です。

http://www.masm32.com/

MASM32 をどこにインストールしても構いませんが、環境変数の設定の際に PATH を通すことを忘れないでください。

その他の準備

引き続き、Windows Build Prerequisitesを参照しながら Cygwin と moztools のインストールと環境変数の設定を行ってください。Cygwin のインストールに際しては、make のバージョンに気をつけてください。

設定例

以下は、筆者の Cygwin.bat です。

set HOME=C:\cygwin\home\satoshi_ueyama
set VCVARS=C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\bin\vcvars32.bat
set MSSDK=C:\Program Files\Microsoft Platform SDK
set MOZ_TOOLS=C:\moztools
set CYGWINBASE=C:\cygwin
set CYGWIN=nodosfilewarning
set PATH=%PATH%;C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\bin

call "%VCVARS%"
set PATH=%PATH%;%MOZ_TOOLS%\bin;C:\masm32\bin

@echo off

C:
chdir C:\cygwin\bin

bash --login -i

ここまで準備したもの—Visual Studio、MASM32、moztools—が到達可能になっていますね? もしビルドの途中で何かが見つからないと文句を言われたら、ここを見直してください。

最初のビルド

アーカイブの展開

まず、前章で取得した firefox-2.0.0.2-source.tar.bz2 をどこかに配置しましょう。この記事では、C:\ 直下に mozhackというディレクトリを作成し、そこに firefox-2.0.0.2-source.tar.bz2 をコピーしました。Explorer で見ると以下のような状態です:

画像:Mh_001.png

さて、Cygwinのコンソールを起動し、このディレクトリに移動します。C: ドライブは、 /cygdrive/c 以下にマウントされていますので、mozhack ディレクトリに移動するためには:

cd /cygdrive/c/mozhack/

と入力します。移動したらlsしてみてください。firefox-2.0.0.2-source.tar.bz2 がありましたか? では、アーカイブを展開しましょう。展開のためのコマンドは以下のとおりです:

tar xjfv firefox-2.0.0.2-source.tar.bz2

これは少し時間がかかります。この間にちょっと別の作業をしましょう。

(オプション)cygwinの追加設定

毎回、Cygwin を起動するたびにディレクトリを移動するのは煩わしいことです。そこで、自動的に移動を行うように設定しましょう。C:\cygwin\home\_YOUR_NAME_ に .bashrc というファイルがありますね? その末尾に以下の行を追加してください:

cd /cygdrive/c/mozhack/mozilla

mozilla というディレクトリは、前節での展開作業で生成される(筈の)ディレクトリです。ここが make を行う場所になります。

あとついでに、cygwin.batへのショートカットをスタートメニューに追加して、Cygwin の冴えないアイコンを Pop なものに変えておきましょう。

画像:Mh_002.png

展開ファイルの確認

さて、そろそろ展開作業が終わりましたか? 正しく展開されていれば、mozilla というディレクトリが作成され、その中にいろいろなファイルやディレクトリがある筈です。Explorer のフォルダツリーで見ると以下のような状態です:

画像:Mh_003.png

元のアーカイブ firefox-2.0.0.2-source.tar.bz2 は必要ないので削除しましょう。

ビルドの設定

いよいよビルド! —の前に、少しばかり設定をする必要があります。mozilla ディレクトリ内に、.mozconfig というファイルを作成してください。 Explorer は、ピリオドで始まるファイルを作成しようとすると失敗しますので、Cygwin のコンソールから touch コマンドを利用するのがよいでしょう。.mozconfig には、何をどのようにビルドするかを記述します。

我々が普段使用している Firefox と同じものをビルドするための設定は以下の通りです:

mk_add_options MOZ_CO_PROJECT=browser
mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/obj-@CONFIG_GUESS@
ac_add_options --enable-application=browser
ac_add_options --enable-optimize
ac_add_options --disable-tests
ac_add_options --disable-debug
ac_add_options --enable-svg
ac_add_options --enable-svg-renderer-gdiplus
ac_add_options --disable-activex
ac_add_options --disable-activex-scripting
ac_add_options --disable-shared
ac_add_options --enable-static

適当なエディタで .mozconfig に以上の内容を記述し、保存してください。

ビルド

今度こそビルドです。まずは configure をしましょう:

./configure --disable-installer

ここで重要なのは --disable-installer オプションです。読んで字の如く、インストーラの生成を無効にするオプションです。インストーラを生成するためには、さらにいろいろと必要なものがあります。手順を簡略化するために、インストーラの生成を行わないことにします。

さあ、configure は無事に終わりましたか? 初挑戦でこれが一発で終われば大したものです。

続いて make です。ここでは単に make と入力しましょう:

make

これはとても時間がかかります! (なるべく薄い)コーヒーでも飲みながら待ちましょう。

実行

何もエラーが出ずに make が終了しましたか? では、いよいよ出来たての Firefox を実行しましょう。完成したバイナリは、mozilla/dist/bin 以下にあります。お馴染みの Firefox.exe がありますね? 早速起動しましょう。見慣れた画面が表示されるでしょう:

画像:Mh_004.png

デバッグ版のビルド

さて、最初に皆様に謝らなくてはいけません。前章で作成したビルドはリリース版で、Hack に向いていません。気持ちよく Hack するためには、デバッグ版のビルドが必要ですので、もう一度ビルドをやり直します。どうかもう小一時間お付き合いを!

ビルド設定の変更

先ほど作成した .mozconfig を以下のように変更してください。

mk_add_options MOZ_CO_PROJECT=browser
mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/obj-@CONFIG_GUESS@
ac_add_options --enable-application=browser
ac_add_options --disable-optimize
ac_add_options --disable-tests
ac_add_options --enable-debug
ac_add_options --enable-svg
ac_add_options --enable-svg-renderer-gdiplus
ac_add_options --disable-activex
ac_add_options --disable-activex-scripting

debug 用のコードが有効になり、最適化が無効になりました。また、実行時に共有ライブラリを使用するようになりました。共有ライブラリの使用は、コード変更後の再ビルドを劇的に速くします。

さらに、Cygwin のコンソールから以下のように入力してください:

export MOZ_DEBUG_SYMBOLS=1

この指定により、make は、デバッグ用のシンボルデータベース (pdb) を作成します(というより、作成したものを破棄しなくなります)。これにより、デバッガからソースコードを参照することができます。

ビルド

では、もう一度ビルドを行います。まず、リリースビルドのファイルを消しておきましょう:

make clean

続いて configure と make です:

./configure --disable-installer
make

実行

前回と同様に、mozilla/dist/bin/Firefox.exe を実行します。今回は、見慣れているものとは少し違う Firefox が起動する筈です。Debug 版のスクリーンショットを以下に示します:

画像:Mh_005.png

通常のウィンドウのほかに、コンソールが表示されています。また、通常の Firefox より少し動作が遅いことにも気づくでしょう。これらは、あなたが Debug 版のビルドに成功したことを示しています。

デバッガの使用

デバッガは、プログラムの動作を追跡し、解析するためのツールです。デバッガを使用して Firefox を解析することは、この巨大なシステムの構造を理解する大きな手助けとなるでしょう。

デバッガの設定

この記事では、デバッガとして Visual Studio を使用します。通常、Visual Studio を使用した開発では、コーディングとビルド、およびデバッグをすべて Visual Studio 上で行い、この場合はデバッガの設定は自動的に行われます。しかし我々は、Visual Studio の外でビルドされたプログラムを Visual Studio でデバッグするという変則的な事を行おうとしています。よって、少々設定を行う必要があります。

プロジェクトの作成

まず、空のソリューションとプロジェクトを作成します。「ファイル」メニューから「新規作成」→「プロジェクト...」を選択するか、<kbd>Ctrl+Shift+N</kbd> を押下してください。ウィザード形式のウィンドウが表示されますので、次図のように設定してください:

画像:Mh_006.png

まず、適当なプロジェクト名を入力してください。この例では「tutorial1」としました。そして、プロジェクトを生成する場所を指定してください。この例では、C:\mozhack としました(実際は、この下にさらにディレクトリが生成され、そこに格納されます)。

次に、プロジェクトの詳細設定を行います。少しわかりにくいですが、ウィンドウの左側にタブがあります。そこから「アプリケーションの設定」タブを選択し、「空のプロジェクト」にチェックを入れてください。チェックを入れたら「完了」ボタンを選択してください。

プロジェクトの設定

前項で作成したプロジェクトの設定を行います。「ソリューションエクスプローラ」の中から、プロジェクト(ソリューションではなく!)のアイコンを右クリックし、コンテキストメニューから「プロパティ」を選択してください。次図のようなプロジェクトのプロパティダイアログが表示されます:

画像:Mh_007_VSProject.png

左側のリストから「デバッグ」セクションを選択し、「コマンド」の欄にFirefoxの実行ファイルを指定してください。続いて、「OK」ボタンを選択してください。

デバッグ開始

いよいよデバッガを起動します。ツールバーの開始ボタン(青い三角のアイコン)を選択するか、F5キーを押下してください。Firefox が起動する筈です。

Firefox の起動途中にデバッガが終了してしまう(Visual Studio の画面のレイアウトが元に戻ります)場合があります。この場合は、一度 Firefox を終了してからやり直してください。

ブレークポイントの設定

ブレーク機能は、デバッガの強力な機能の一つです。ブレーク機能を使用すると、プログラム上の任意の場所で実行を中断し、プログラム内部の状態を観察することができます。

早速、適当な場所にブレークポイント(ブレークを行う場所)を設定します。Mozilla のソースツリーの中から、 mozilla/content/html/content/src/nsHTMLDivElement.cpp を Visual Studio で開いてください。ファイルを開いたら、コンストラクタ nsHTMLDivElement::nsHTMLDivElement(nsINodeInfo *aNodeInfo) にブレークポイントを設定します。83行目の左端の欄をクリックしてください。赤い印がつきましたか? では、Firefox で適当なページを開いてください。http://www.mozilla.org/ がよいでしょう。

ここまでの手順が正しければ、Firefox の実行が中断され、Visual Studio のウィンドウが表示されるでしょう。今、あなたは、変数の内容やコールスタック(関数の呼び出しの経路)を覗いたり、一行ずつプログラムを実行するといったことができます。

ソースコードを追う

Mozilla は巨大なプログラムです。手練のプログラマであっても、コードを見ただけで全体の構造を理解することは難しいでしょう。しかし今、デバッガを使用し、実際にプログラムを動作させながらコードを追うことが可能になりました。少しずつコードを追いながら、Mozilla の構造を見ていくとよいでしょう。本節では、デバッガを使用してソースコードを追う手順を紹介します。

コールスタック

前節でブレークした状態を維持してありますか? 終了してしまった場合は、もう一度実行して同じ場所でブレークしてください。あなたが Visual Studio の設定を特に変更していなければ、Visual Studio のウィンドウの右下に「呼び出し履歴」というペインがあるでしょう。次図に「呼び出し履歴」ペインを示します:

画像:Mh_008_callstack.png

「呼び出し履歴」は、コールスタックとも呼ばれます。コールスタックは、ブレークを設定した処理がどこから呼ばれたのかを記録しています。いま、nsHTMLDivElement 関数(コンストラクタ)が最上段に表示されています。その下には NS_NewHTMLDivElement 関数が表示されています。これは、NS_NewHTMLDivElement 関数が nsHTMLDivElement 関数を呼び出したことを示しています。NS_NewHTMLDivElement 関数のある段をダブルクリックすると、その関数の定義にジャンプします。NS_NewHTMLDivElement 関数の定義は以下のようなものです:

NS_IMPL_NS_NEW_HTML_ELEMENT(Div)

NS_NewHTMLDivElement 関数は、マクロで実装されているようです。このマクロの内容を追ってもよいのですが、この関数の機能は、名前から自明です。これ以上調べても、あまり面白い事実はないでしょう。

さらにコールスタックを辿りましょう。3段目の MakeContentObject 関数にジャンプしてください。ジャンプ先のコードの一部を次に示します:

 if (aNodeType == eHTMLTag_form) {
   if (aForm) {
     // the form was already created
     NS_ADDREF(aForm);
     return aForm;
   }
   nsGenericHTMLElement* result = NS_NewHTMLFormElement(aNodeInfo);
   NS_IF_ADDREF(result);
   return result;
 }

 contentCreatorCallback cb = sContentCreatorCallbacks[aNodeType];

 NS_ASSERTION(cb != NS_NewHTMLNOTUSEDElement,
              "Don't know how to construct tag element!");

 nsGenericHTMLElement* result = cb(aNodeInfo, aFromParser);

上に掲載したコードの最後の行(ハイライトしてあります)でブレークしている筈です。しかし、NS_NewHTMLDivElement という関数名が見当たりません。コードを注意深く見ると、どうやら sContentCreatorCallbacks という配列(のように見えるもの)に、要素を生成する関数の一覧が格納されているようです。NS_NewHTMLDivElement 関数もその中にあるのでしょう。

sContentCreatorCallbacks の正体は何でしょうか。プレフィックスから、これが static なグローバル変数であると推測できます。つまり、現在見ているファイル (nsHTMLContentSink.cpp) の中で宣言されている可能性が高いということです。<kbd>Ctrl+F</kbd> を押下し、「sContentCreatorCallbacks」を検索してみましょう。すると192行目の辺りに以下のようなコードがあります:

#define HTML_TAG(_tag, _classname) NS_NewHTML##_classname##Element,
#define HTML_OTHER(_tag) NS_NewHTMLNOTUSEDElement,
static const contentCreatorCallback sContentCreatorCallbacks[] = {
  NS_NewHTMLUnknownElement,
#include "nsHTMLTagList.h"
#undef HTML_TAG
#undef HTML_OTHER
  NS_NewHTMLUnknownElement
};

どうやら、sContentCreatorCallbacks の正体は単純な配列のようです。しかし、配列の要素の宣言は少々トリッキーです。とりあえず、#include ディレクティブで参照されている nsHTMLTagList.h を開きましょう。nsHTMLTagList.h は、mozilla/parser/htmlparser/public にあります。nsHTMLTagList.h の内容の一部を次に示します:

HTML_TAG(a, Anchor)
HTML_TAG(abbr, Span)
HTML_TAG(acronym, Span)
HTML_TAG(address, Span)
HTML_TAG(applet, Applet)

...(中略)...

HTML_TAG(tr, TableRow)
HTML_TAG(tt, Span)
HTML_TAG(u, Span)
HTML_TAG(ul, SharedList)
HTML_TAG(var, Span)
HTML_TAG(wbr, Shared)
HTML_TAG(xmp, Span)


/* These are not for tags. But they will be included in the nsHTMLTag
   enum anyway */

HTML_OTHER(text)
HTML_OTHER(whitespace)
HTML_OTHER(newline)
HTML_OTHER(comment)
HTML_OTHER(entity)
HTML_OTHER(doctypeDecl)
HTML_OTHER(markupDecl)
HTML_OTHER(instruction)

nsHTMLContentSink.cpp のコードと併せて読むと、このコードのからくりが解ります。nsHTMLContentSink.cpp では、 nsHTMLTagList.h を include する直前で HTML_TAGHTML_OTHER という2つのマクロを定義しています。どうやら、nsHTMLTagList.h の内容をマクロで書き換えることにより、NS_NewHTML**Element 関数へのポインタを動的に生成し、配列に追加しているようです。このように、C や C++ のコードでは、しばしばマクロを使ったトリックが使われています。

ファイル内の検索

ここまでで、sContentCreatorCallbacks の正体はわかりました。続いて、配列の添字として指定されていた aNodeType の正体を探してみます。MakeContentObject 関数の宣言より、変数 aNodeType の型は nsHTMLTag であることがすぐに判ります。では、型 nsHTMLTag の宣言はどこでしょうか。今度は、nsHTMLContentSink.cpp の中には無いようです。このような場合、<kbd>Ctrl+Shift+F</kbd> を押下して「ファイル内の検索」を行うことが良い選択です。「ファイル内の検索」機能は、複数のファイルを横断的に検索する機能です(find/grep と言えば通じる人もいるでしょう)。「ファイル内の検索」機能のダイアログボックスを次図に示します:

画像:Mh_009_findgrep.png

まず、検索のオプションを設定します。「検索対象」の項目には、ソースツリーのトップ(mozillaディレクトリ)を指定しておきましょう。「ファイルの種類」は、「*.cpp;*.h」でよいでしょう。「サブフォルダを探す」にチェックが入っていない場合は、チェックを入れてください。最後に、「検索する文字列」に「nsHTMLTag」を指定し、検索を実行しましょう。

何件ヒットしましたか? 私が試したところ216件でした。明らかに余計なものが混ざっています。そこで、もう一度「ファイル内の検索」ダイアログボックスを開き、「単語単位」というチェックボックスにチェックを入れてください。このオプションを指定すると、単語単位の完全一致のみがヒットします。つまり、「HTML」というクエリに対し、「HTML is a...」はヒットしますが、「XHTML is a...」はヒットしません。

今度は134件ヒットしました。期待したほど減りませんでしたか? しかし、先程よりは幾分ましです。この中から nsHTMLTag の宣言を探してみます。

mozilla/dist/include/htmlparser/nsHTMLTags.h の54行目に、それらしい列挙型の宣言があります。この付近のコードを次に示します:

#define HTML_TAG(_tag, _classname) eHTMLTag_##_tag,
#define HTML_OTHER(_tag) eHTMLTag_##_tag,
enum nsHTMLTag {
  /* this enum must be first and must be zero */
  eHTMLTag_unknown = 0,
#include "nsHTMLTagList.h"

  /* can't be moved into nsHTMLTagList since gcc3.4 doesn't like a
     comma at the end of enum list*/
  eHTMLTag_userdefined
};
#undef HTML_TAG
#undef HTML_OTHER

前項の nsHTMLContentSink.cpp と同じトリックを使っています。nsHTMLTagList.h の内容をマクロによって書き換え、eHTMLTag_**_tag という列挙子の宣言を生成しています。

ウォッチ

ここで今一度、nsHTMLContentSink.cpp の976行目(NS_NewHTMLDivElement 関数を呼び出していた場所)に戻ってください。そして、コールスタックのペインの左にあるペインを見てください。あなたが Visual Studio の初期設定を変えていなければ、「ローカル」というタブがある筈ですので、そのタブを開いてください。ない場合は、「デバッグ」メニューから「ウィンドウ」→「ローカル」を選択してください。次図に示すウィンドウが表示されます:

画像:Mh_010_localwatch.png

ウィンドウには、ローカル変数の名前と値が表示されています。変数の内容を見てみましょう。変数 aNodeType の値は、eHTMLTag_div です。これは、我々が div 要素生成の瞬間にブレークしていることから考えて妥当です。次に、変数 cb を見てみます。変数 cb の値は 0x02160760 NS_NewHTMLDivElement(nsINodeInfo *, int) です。971行目を見ると、cbsContentCreatorCallbacks から取り出した要素であることがわかります。つまり、

  1. aNodeType の値 eHTMLTag_div によって sContentCreatorCallbacks から関数ポインタが抽出され、変数 cb に格納された(971行目)
  2. 変数 cbNS_NewHTMLDivElement 関数を指していた
  3. cb> が指す処理をコールした(976行目)
  4. NS_NewHTMLDivElement 関数が呼び出され、そこからさらに nsHTMLDivElement::nsHTMLDivElement 関数が呼び出された(nsHTMLDivElement.cppの83行目、最初のブレーク地点)

という処理の流れが読み取れます。

このように、ウォッチ機能は、プログラムの動作の結果を確認し、コードの内容を理解する大きな手助けとなります。

ステップ実行

いま我々は、新たに div 要素が生成された瞬間を見ました。この生成された div 要素は、この先どこへ行くのでしょうか。HTML 文書がツリー構造のデータであることから推察すると、どこかで親要素と結び付けられる筈です(そうでなければ、このDIV要素は迷子になってしまいます)。この推察は正しいのでしょうか? 確かめてみましょう。

まず、最初にブレークした場所—nsHTMLDivElement::nsHTMLDivElement—に戻りましょう。もっとも簡単な方法は、コールスタックの最上段をダブルクリックすることです。続いて、<kbd>Shift+F11</kbd> キーを押下してください。何が起きましたか? 先程まで、ブレークポイントにあった黄色い矢印のアイコンが、78行目まで移動しました。もう一つ変化があります。コールスタックに注目してください。先程まで最上段にあった「nsHTMLDivElement::nsHTMLDivElement」が消え、2段目にあった「NS_NewHTMLDivElement」が繰り上がりました。つまり、nsHTMLDivElement::nsHTMLDivElement 関数が終了し、NS_NewHTMLDivElement 関数に処理が返されたということです。

今あなたが使った機能は「ステップアウト」と呼ばれます。ステップアウトは、ブレーク行を含む関数が終了するまで実行を続け、上位の(呼び出し元の)関数に処理が返ったところで実行を停止する機能です。さらにステップアウトを繰り返し、SinkContext::OpenContainer という関数がコールスタックの最上段に来たら、そこで停止してください。次に示すコードの場所で停止しています:

 nsGenericHTMLElement* content =
   mSink->CreateContentObject(aNode, nodeType, mSink->mCurrentForm,
                              docshell).get();

ウォッチ機能を利用し、変数 content の内容を確認してください。現在、変数 contentNULL ポインタです。ここで <kbd>F10</kbd> キーを押下してください。黄色い矢印のアイコンが次の行に移動しました。ここでもう一度、変数 content の内容を確認してください。変数 content への代入が完了し、変数 content は、まさに今生成された nsHTMLDivElement 型のオブジェクトを指しています。この変数 content を追えば、div 要素の行方を確認できそうです。

今あなたは、「ステップオーバー」という機能を使いました、「ステップオーバー」は、ブレーク行にある処理を実行し、次の処理を行う直前で停止します。ステップオーバーを繰り返し、1217行目まで進んでください。興味深いコードが現れました。1217行目付近のコードを次に示します:

 nsGenericHTMLElement* parent = mStack[mStackPos - 2].mContent;

 if (mStack[mStackPos - 2].mInsertionPoint != -1) {
   parent->InsertChildAt(content,
                         mStack[mStackPos - 2].mInsertionPoint++,
                         PR_FALSE);
 } else {
   parent->AppendChildTo(content, PR_FALSE);
 }

まず、メンバ変数 mStack から親要素を取得しています(mStack の詳細については、気にしないでおきましょう)。次に処理が分岐していますが、見たところ、どちらに進んでも parent の子要素として content を追加するという大意は変わらないでしょう。それでは、分岐の中に進むまでステップオーバーして下さい。おそらく1224行目(else 側)に進むでしょう。AppendChildTo という関数名から処理の内容は自明ですが、練習として関数の内容を追ってみます。今度は <kbd>F11</kbd> を押下してください。AppendChildTo 関数の中に入ることができました。今あなたが使った機能は「ステップイン」です。「ステップイン」は、ブレーク行にある処理にジャンプし、ジャンプ先の処理を開始する前に停止します。ジャンプ先にもまだまだ多くのコードがあります。デバッガの機能を活用し、さらにコードを追ってみてください。

本項では、「ステップイン」と「ステップオーバー」、「ステップアウト」の3つの機能を使用してコードを追いました。これらの機能は、コードが実際に何を行っているのか調べる際にとても役に立つでしょう。

content モジュール

前章で我々は、Mozilla のコードのごく一部を理解しました。しかし、Mozilla は、本当に巨大なプログラムです。すべてのコードを一行ずつ読んで理解することは、現実的ではありません。そこで、ここからは少し粒度の大きい解説をしましょう。

mozilla ディレクトリにあるファイルやディレクトリ群をもう一度確認してください。content と layout というディレクトリがあります。これらのディレクトリはそれぞれ、content モジュールと layout モジュールのコードを含んでいます。content と layout はそれぞれ、Document-View アーキテクチャで言うところの Document と View に相当します。この2つのモジュールは、Gecko の心臓部ですから、時間をかけて解析するだけの価値があるでしょう。本章でまず content モジュールの解説をし、次章で layout モジュールの解説をします。

Content Tree

Gecko の動作についての貴重な資料の一つは、「HTTP リクエストの一生」です。リンク先のページにある図の中央下、(6)という番号が振られている箇所を見てください。「Creates」というラベルの付いた矢印の先で、「IContent」と書かれた箱がツリーを形成しています。「IContent」と書かれた箱のツリーを便宜的に Content Tree と呼びましょう。Content Tree の正体は、我々が普段 JavaScript で扱っている、お馴染みの DOM ツリーです。前章で我々が追ったコードはこの、Content Tree を構築する処理—「Creates」というラベルの付いた矢印で示されています—の一部でした。

何か釈然としませんか? それは、あなたが(DOM に詳しいにもかかわらず)「IContent」などという名前を聞いたことがないからでしょう。その理由は、content モジュールに含まれるクラスの継承関係を見れば解ります:

画像:Mh_011_content_classes_inheritance.png

図の最下段(青くハイライトされています)には、お馴染みの HTML 要素を表すクラスがあります。それらのクラスは、nsGenericHTMLElement クラスを継承しています(他にもあるかもしれませんが、省略しました)。さらに基底クラスを辿っていくと、nsIContent インターフェイスに行き当たりました(黄色でハイライトされています)。これで疑問が解決しました。「HTTP リクエストの一生」の図は、「IContent のツリー」ではなく「IContent を継承したクラスのオブジェクトのツリー」を意味していたのです。もちろん、前章の nsHTMLDivElement もこのツリーのどこかに組み込まれます。この図は非常に抽象的ですが、具体例を考えながら見ることで、理解しやすくなるでしょう。

ParserとContentSink

引き続き、「HTTP リクエストの一生」の図を見ながら考えましょう。前節で、我々は、Content Tree についてはよく理解しました。本節では、Content Tree の元となる文書を解析し、ツリーを構築する処理—つまり、パースの過程を理解することにします。

Gecko のパース処理において重要なものは、Parser と ContentSink です。Parser はともかく、ContentSink とは実に抽象的で分かりにくい名前です。ContentSink が何をしているのか探るために、少しコードを追ってみることにします。

字句解析

mozilla/parser/htmlparser/src/nsParser.cpp を開いてください(content ディレクトリの外側にありますが、気にせず!)。そして、2030行付近を見てください。次のようなコードがあります:

       nsresult theTokenizerResult = mFlags & NS_PARSER_FLAG_CAN_TOKENIZE ? Tokenize(aIsFinalChunk) : NS_OK;   // kEOF==2152596456
       result=BuildModel(); 

たった2行ですが、とても重要なコードです。このコードを詳しく解析するために、2030行にブレークポイントを設定してください。 ブレークポイントを設定したら再び http://www.mozilla.org/ を開いてください。ブレークポイントにヒットし、nsParser.cpp の2030行で停止するでしょう。停止したら、2030行にステップインしてください。2831行の nsParser::Tokenize 関数にジャンプする筈です。nsParser::Tokenize 関数内の2862行に次のようなコードがあります:

result=theTokenizer->ConsumeToken(*mParserContext->mScanner, flushTokens);

この行に新たにブレークポイントを設定し、続行(F5 キー)してください。2862行で再び停止したら、さらにステップインしてください。nsHTMLTokenizer.cpp の549行、nsHTMLTokenizer::ConsumeToken 関数にジャンプします。さて、ここからは自力で、nsHTMLTokenizer::ConsumeToken 関数の内容や、そこからコールされている関数の内容を追ってみてください。何が行われているか理解できましたか?

いま我々が追っているコードは、字句解析と呼ばれる処理です。字句解析とは、テキストで記述されている言語—この場合はHTML—を解析し、字句の区切りを認識する処理です。'<' が見つかればタグであると判断し、その次の文字が '/' であれば終了タグと判断し—などと、実に泥臭いコードが記述されています。

構文解析

次にnsParser.cpp の2031行の内部を追いたいところですが、コードの量が多いため、駆け足で解説します。2031行の処理は構文解析です。構文解析は、(非構造的なデータ列にすぎない)字句解析結果を構造化する処理です。

前項で設定したブレークポイントを解除し、mozilla/parser/htmlparser/src/CNavDTD.cpp の 3263行に新たにブレークポイントを設定してください。再び続行して3263行で停止したら、ステップインしてください。さらにその先でmozilla/content/html/document/src/nsHTMLContentSink.cpp の 2941行にステップインしてください。いま、あなたは SinkContext::OpenContainer 関数のコードを見ている筈です。そして、このコードに見覚えがある筈です。前章を思い出してください。このコードは、nsHTML*Element を生成し、さらに DOM ツリーに追加するコードでした。

コールスタックを確認してください。確かに nsParser.cpp の2031行から呼び出されています。つまり、nsParser.cpp の2031行の内部の処理の結果、DOM ツリーに新たな要素が追加されたということです。

nsParser.cpp の 2030行と2031行の関係をよく考察してください。2030行は、「文字」の流れ(Stream)を「字句」の流れに変換します。2031行は、流れてくる「字句」の意味を認識して要素を生成し、さらに DOM ツリーという構造(Structure)に変換します。工場の流れ作業のように、少しずつデータが加工され、最終的に DOM ツリーという製品になり、そこで流れが停止します。流れてくるデータを洗面台(Sink)のように溜める場所こそ ContentSink です。Parser と ContentSink の関係を次の図に示します:

画像:Mh 012 HTMLParser.png

さて、あなたが余程の物好きでもない限り、DOM Inspector で DOM ツリーを眺めながら Web サーフィンをしたりはしないでしょう。あなたが見たいものは、DOM ツリーではなく、美しくレンダリングされた結果です。レンダリングの過程については次章で解説します。

layout モジュール

layout モジュールは、content モジュールが解析した文書の内容を視覚的にレンダリングします。Gecko がしばしば「レンダリングエンジン」と呼ばれることからも分かるように、レンダリングは、Gecko のもっとも重要な機能です。あなたが Gecko ベースのブラウザを利用している理由も、Gecko の優秀なレンダリング能力を見込んでのことでしょう。

frame

再び、「HTTP リクエストの一生」の図を参照してください。右下に「Frame」と書かれた箱がツリーを形成している部分があります。frame の各ノードは、content tree の各ノードと(基本的には)対応しており、1つの frame が 1つの content のレンダリングを管理します。この frame は、HTMLの frame 要素とは全く関係ありません。

あなたは、ここまで読んで、ある推測をするかもしれません。つまり、h1 要素には H1Frame というクラスが対応し、div 要素には DivFrame というクラスが対応し、em 要素には EMFrame クラスが対応し、frame の tree を構成する—この推測は一見妥当なように思えます。

この推測が妥当なものか確かめるために、実際に frame が構築される過程を追ってみます。mozilla/layout/base/nsCSSFrameConstructor.cpp の7768行にブレークポイントを設定してください。今回はさらに、ブレークポイントの条件を以下のように設定してください。

aTag == nsHTMLAtoms::div

この条件式により、div 要素に対応する frame が構築される瞬間にのみブレークが行われます。

では、適当な(div 要素を含む)ページを開いてください。ブレークしたら7865行までステップオーバーし、次の行の ConstructHTMLFrame 関数にステップインしてください。ConstructHTMLFrame 関数の動作は実に単純です。要素の種類を if-else で選別し、要素に対応した frame を構築します。if 文の条件がヒットするまでステップオーバーしてください。どうなりましたか?なんと、結局 div 要素に対応した frame が構築されないまま return してしまいました。

結論を述べると、div 要素に「特有の」*Frame クラスは存在しませんし、必要ありません。何故でしょうか。以下のような HTML 文書がどのようにレンダリングされるか考えてみてください:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="ja">
 <head>
  <title>test</title>
  <style type="text/css"><!--
   h1,h2,div,p{
    margin: 0.3em;
    padding: 0.3em;
    font-size: 100%;
    border: 1px solid #000;
    font-weight: 100;
   }
  --></style>
 </head>
 <body>
  <h1>mozilla</h1>
  <h2>mozilla</h2>
  <div>mozilla</div>
  <p>mozilla</p>
 </body>
</html>

特別なユーザースタイルシートが設定されていない限り、Firefox によるレンダリング結果は以下のようなものになります:

画像:Mh_012_css_box.png

全ての要素が同じようにレンダリングされました。理由は、全ての要素に同じスタイルが指定されているからです。—重要な事実に気付きましたか? レンダリングを行う際に重要な事は、要素の種類ではなく、要素にどのようなスタイルが指定されているかです。何もスタイルを指定していない状態で h1 要素が「大きな文字」でレンダリングされるのは、デフォルトスタイルシートでそう指定されているからに過ぎません。要素の種類が関係ないということは、要素の種類にかかわらず共通のコードを(レンダリングに)使用できるということです。ブロック要素をレンダリングする共通のコードは nsBlockFrame クラス、インライン要素をレンダリングする共通のコードは nsInlineFrame クラスにあります。この節の冒頭で追った div 要素には、最終的に nsBlockFrame クラスのオブジェクトが割り当てられている筈です。

ただし、HTML の全ての要素が共通のコードでレンダリング可能というわけではありません。img 要素などの置換要素には、専用の *Frame クラスが割り当てられます。

Reflow と Paint

frame によるレンダリング処理は、大きく2段階に分けられます。1つ目は、スタイルシートの指定や隣接する要素との関連を考慮し、要素をレンダリングする位置と大きさを決定する処理です。これを Reflow と呼びます。2つ目は、Reflow の結果決定された位置に、枠線や背景、文字などを描画する処理です。これを Paint と呼びます。Reflow と Paint は完全に分離されており、一つの関数の中に、配置に関する処理と描画に関する処理が同居するようなことはありません。

Reflow

Reflow処理は、tree 上で最上位の frame から順に再帰的に行われます。まず、各 frame は、自身の望まれる大きさ(desired size)を親 frame に報告します。次に、親 frame は、子 frameを適切な位置に配置します。全ての frame の位置と大きさが確定すると、Reflow は終了し、Paint できる状態になります。

次の画像は、Reflow の過程をアニメーションにしたものです:

画像:Reflow_trace_small.gif

配置に関する挙動に注目してください。各 frame の初期位置は、常に (x,y)=(0,0)、つまり親 frame の左上です。自身より下位の frame の配置が終わり、自身の大きさが決定した時点で、親 frame 中での自身の位置が決定し、適切な位置に移動しています。

Paint

Paint もまた、最上位の frame から順に再帰的に行われます。各 frame が実装している Paint 関数は、その名の通り Paint 処理を行うためのものですが、実際の処理は nsCSSRendering というクラスの静的関数に記述されており、各 frame が nsCSSRendering に移譲する形をとっています。 nsCSSRendering に処理を移譲している様子は、nsFrame::PaintSelf 関数に見られます。nsFrame::PaintSelf 関数は、nsFrame のサブクラスの Paint 関数から呼び出されるサブルーチンです。以下に nsFrame::PaintSelf 関数のコードを掲載します:

void
nsFrame::PaintSelf(nsPresContext*      aPresContext,
                   nsIRenderingContext& aRenderingContext,
                   const nsRect&        aDirtyRect,
                   PRIntn               aSkipSides,
                   PRBool               aUsePrintBackgroundSettings)
{
  // The visibility check belongs here since child elements have the
  // opportunity to override the visibility property and display even if
  // their parent is hidden.

  PRBool isVisible;
  if (mRect.height == 0 || mRect.width == 0 ||
      NS_FAILED(IsVisibleForPainting(aPresContext, aRenderingContext,
                                     PR_TRUE, &isVisible)) ||
      !isVisible) {
    return;
  }

  // Paint our background and border
  const nsStyleBorder* border = GetStyleBorder();
  const nsStylePadding* padding = GetStylePadding();
  const nsStyleOutline* outline = GetStyleOutline();

  nsRect rect(0, 0, mRect.width, mRect.height);
  nsCSSRendering::PaintBackground(aPresContext, aRenderingContext, this,
                                  aDirtyRect, rect, *border, *padding,
                                  aUsePrintBackgroundSettings);
  nsCSSRendering::PaintBorder(aPresContext, aRenderingContext, this,
                              aDirtyRect, rect, *border, mStyleContext,
                              aSkipSides);
  nsCSSRendering::PaintOutline(aPresContext, aRenderingContext, this,
                               aDirtyRect, rect, *border, *outline,
                               mStyleContext, 0);
}

後半のコードは、実にわかりやすいコードです。まず背景を描画し、border を描画し、outline を描画します。先に描かれたものが背後に回るため、必ずこの順番で描画される必要があります。

ここから、背景の描画(nsCSSRendering::PaintBackground 関数)を例に、具体的な描画処理を追います。背景の種類(単色塗りつぶしか、画像か)や border の形態(丸みがあるかどうか)により処理が分岐しますが、もっとも単純な「丸みなし、単色の乗りつぶし」の場合、mozilla/layout/base/nsCSSRendering.cpp の 3303行目付近にある次のコードに到達します:

  aRenderingContext.SetColor(color);
  aRenderingContext.FillRect(bgClipArea);

実に単純なコードですが、指摘すべき重要な点があります。それは、ここから先がプラットフォーム依存のコードであるという事です。Windows の場合、aRenderingContextnsRenderingContextWin クラスのインスタンスです。Linux+Gtk の場合、OS/2 の場合、Mac OS X の場合はそれぞれ、別のクラスのインスタンスが aRenderingContext に設定されます。

nsRenderingContextWin::FillRect 関数の実装は以下のようなコードです:

NS_IMETHODIMP nsRenderingContextWin :: FillRect(const nsRect& aRect)
{
  RECT nr;
	nsRect tr;

	tr = aRect;
	mTranMatrix->TransformCoord(&tr.x,&tr.y,&tr.width,&tr.height);
  ConditionRect(tr, nr);
  ::FillRect(mDC, &nr, SetupSolidBrush());

  return NS_OK;
}

return 直前の FillRect 関数は、Win32 GDIのAPIです。ここから先は Windows 内部の話であり、我々には関係ない事です。つまり、ここが Mozilla における描画処理の末端ということになります。

DisplayList

HTMLをレンダリングする最も単純なアルゴリズムは、文書の先頭から、文書中に現れる順に要素を描画するものです。frame オブジェクトのツリーを再帰的に辿って描画処理を行った場合、このアルゴリズムを使用したことになります。このアルゴリズムは、文書中で後に現れる要素が、前に現れたものを必ず上書きします。スタイルシートによる特別な指定がない場合、この動作は妥当なものです。逆にいえば、スタイルシートによる特別な指定がある場合—つまり、z-index プロパティにより要素の前後関係が逆転する場合—このアルゴリズムは破綻します。

この問題を解決するための機構が DisplayList です。DisplayList は、バッチ処理システムのようなものです。各要素の描画処理をバッチジョブとして DisplayList に投入し、スタイルシートで指定された前後関係に基いてジョブを並べ替えてから各ジョブを実行します。この機構により、描画の順序は、文書中の要素の順序から解放されます。

contentモジュールのhack

そろそろコードを読むことに飽きてきましたか? 本章では、Mozilla に新たにコードを追加する方法を解説します。本章では、独自に(勝手に)定義した XML 要素を我々の書いたコードでハンドリングすることを目標とします。

IDLの記述

XML の要素をハンドリングするためのコードは、content モジュールの *Element クラスとして実装されます。このクラスは、XPCOM コンポーネントである必要があります。XPCOM という用語を始めて見たという人は、MDC 内の他の記事を参照してください。XPCOM コンポーネントは ISupports から派生したインターフェイスを最低でも1つ実装する必要があります。いま、我々が実装すべきインターフェイスは、我々が独自に定義する XML 要素を表現する必要があります。当然ながら、このインターフェイスも我々が独自に定義する必要があります。よって、最初に我々がすべきことは、“独自の XML 要素を表現するインターフェイス”の IDL を記述することです。

ところで、“独自の XML 要素”の名前をまだ決めていませんでしたので決めておきましょう。名前空間のURIは、このページのURL( http://developer.mozilla.org/ja/docs...cking_tutorial )でよいでしょう。この名前空間の略称として tutorial を用います。要素の名前は、“tutorial”の頭文字をとって t とします。turotial:t 要素です。覚えましたか?

XML要素の名前が決定しました。Mozilla の命名規約に則ると、この要素に対応するインターフェイスの名前は nsIDOMTutorialTElement となります。

IID

XPCOM のインターフェイスは、それぞれ固有の UUID、IID(Interface ID) を持ちます。UUID は、重複が起こらないように生成された 128bit の数値による ID です。IID が利用される典型的な場面は、インターフェイスの QueryInterface(問い合わせ)です。QueryInterface は、オブジェクトに対し、あるインターフェイスを実装しているか尋ねる操作であり、このときオブジェクトが実装しているインターフェイスの IID の一覧と、問い合わせられたインターフェイスの IID をマッチさせます。 Visual C++ には、UUID を生成するためのプログラムである uuidgen.exe が付属しています。Mozilla のビルド環境を整備した後であれば、単に Cygwin のシェルから

uuidgen

と入力すれば、新たに生成された UUID が出力されます。

IDLファイルとMakefile.inの作成

まず、新しいIDLファイルを配置すべき場所を決める必要があります。我々が普段利用している HTML 要素や XMLHttpRequest などのインターフェイス記述は、/mozilla/dom/public/idl 以下のサブディレクトリに配置されています。このルールに従い、/mozilla/dom/public/idl に新しいサブディレクトリ、“tutorial”を作成し、そこに新しいファイル nsIDOMTutorialTElement.idl を作成することにします。

mkdir tutorial
cd tutorial
touch nsIDOMTutorialTElement.idl

これで新しいファイルが作成されましたが、Mozilla のビルドシステムは、この新しいファイルの存在を知りません。Mozilla のビルドシステムにファイルの存在を知らせるためには、Makefile.in というファイルを編集する必要があります。いま我々は、2つのMakefile.in を編集しなければなりません。1つ目は、/mozilla/dom/public/idl/tutorial に作成する新しい Makefile.in です。2つ目は、一つ上のディレクトリ /mozilla/dom/public/idl にある既存の Makefile.in です。

ドキュメントのタグと貢献者

 このページの貢献者: Gyuque, Taken, Shimono
 最終更新者: Gyuque,