はじめに
この記事は,東京大学工学部電気電子工学科・電子情報工学科の実験「大規模ソフトウェアを手探る」のレポートとして書かれています.「大規模ソフトウェアを手探る」とは,世の中で実際に使われている大規模なオープンソースソフトウェアを改良し,機能拡張しようという実験です.
今回は題材としてChromeのオープンソース版であるChromiumを選び,「リンクの色を新しいタブで開くか否かで変える」という改良に挑戦しました.
Chromiumとは
Chromiumとは,その名の通りGoogle Chromeのベースになっているオープンソースのウェブブラウザです.Chromeの他,OperaもChromiumをベースにしており,さらにMicrosoft Edgeも現在Chromiumベースのバージョンを開発中です.
古い情報になりますが,質問投稿サイトQuoraの投稿によると,2012年時点でChromiumのコード量は500万行程度に達していたそうです.現在はさらに大規模化が進んでいると考えられます.
なにをやったか
私達の班は,Chromium上で「リンクの色を新しいタブで開くか否かで変える」ことを目標に定めました.具体的には,htmlのリンクタグに<target="_blank">
という指定があった場合,リンクの色をデフォルトの青色から緑色に変えるというものです.また,新しいタブで開くリンクに対しても,訪問済みのリンクの色を赤色にして緑色の未訪問のリンクと区別できるようにしました.
Chromiumのビルド
早速Chromiumのビルドに取り掛かります.ビルドは基本的に公式ドキュメントの記載に準じて行いました.なお,OSはUbuntu 18.04です.
ビルドの準備
ビルドのためにはGitとPython2xが事前にインストールされている必要があります.Pythonに関してはUbuntuにデフォルトでインストールされていますが,gitがインストールされていない場合は以下のコマンドによりインストールします.
sudo apt-get install git
depot_toolsのインストール
まず,以下のコマンドによりビルドに必要なツールdepot_toolsを持ってきます.
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
次に,持ってきたdepot_toolsにパスを通します.例えば"/hoge/fuga/depot_tools"にdepot_toolsを持ってきた場合,以下のコマンドによりパスを通します.
export PATH="$PATH:/hoge/fuga/depot_tools"
このコマンドで環境変数PATHに"/hoge/fuga/depot_tools"を一時的に追加しましたが,ターミナルを閉じた時点でこの追加分は元に戻ってしまいます.
コードの取得
ソースコードを入れたいディレクトリを作り,そのディレクトリに移動して以下のコマンドを実行します.
fetch --nohooks --no-history chromium
今回はバージョン履歴の取得は不要なので,オプション--no-historyを付けます.
コマンドの実行が完了するとカレントディレクトリの中にsrcディレクトリができているはずなので,そこに移動します.
cd src
ビルドの依存性解決のためのツールを実行します.
./build/install-build-deps.sh
その後,ビルドに必要なファイルを追加で取得します.
私達がこのコマンドを実行した際,エラーが発生して詰まりました.エラーの詳細と解決策は章末に記載します.
gclient runhooks
ビルド実行
いよいよChromiumのビルドを実行します.Chromiumのビルドにはninjaというツールを使用します.まず,ビルド用のディレクトリを作成します."(src/)out/Default"にビルドしたい場合は以下のコマンドを実行します.
gn gen out/Default
次に,ビルドのオプションを指定します.オプションの詳細は公式ドキュメントに記載されていますが,今回は以下のオプションを使用しました.
use_jumbo_build=true
enable_nacl=false
"use_jumbo_build=true"は複数のファイルを同時にコンパイルするという意味で,"enable_nacl=false"はネイティブクライアント(NaCl)へのサポートをしないという意味です.これらのオプションにより,ビルド時間が短縮できます.
以下のコマンドを実行するとvimでオプション指定用のファイルが開かれるので,上記のオプション2行を追記します.
gn args out/Default
いよいよChromiumをビルドします.以下のコマンドにより,"(src/)out/Default"にChromiumがビルドされます.このディレクトリは上記の"gn gen"コマンドで指定したディレクトリと同一でなければいけません.
autoninja -C out/Default chrome
ビルドにはとても長い時間がかかります.少なくとも1時間以上はかかるので,気長に待ちましょう.なお,2回目以降のビルドでは変更したソースコードとその周辺のファイルのみをコンパイルするため,あまり時間はかかりません.
Chromiumの実行
ビルドが終了したら,早速Chromiumを実行してみましょう.
out/Default/chrome
gclient runhooks 実行時のエラー
gclient runhooks
このコマンドを実行した際,以下のようなエラーが発生しました.
Error: Command 'vpython src/third_party/binutils/download.py' returned non-zero exit status 1 in /home/***/chromium
このエラーは,環境変数PATHからanaconda関連のパスを一時的に削除することで解消できました.
gclient実行時にはPython2xとして書かれた.pyファイルが実行されますが,anacondaへのパスがPATHの最初の方に書かれている場合,UbuntuのシステムにインストールされているPython2xではなく,anacondaによりインストールしたPython3xがこの.pyファイルを実行してしまうためにエラーが発生します.そのため,PATHからanaconda関連のパスを削除するか,anaconda関連のパスを後ろの方に持っていくことでエラーを解消できます.
具体的には,以下のようなコマンド操作を行いました.あくまでもこれは私の環境の例です.
echo $PATH
(結果) /home/***/anaconda3/bin:(その他のパス)
export PATH="(その他のパス)"
手探ってみる
htmlの基礎知識
我々の班の目標はハイパーリンクが新しいタブで開くかどうかでそのリンクの表示色を変えるということであったが,そのためには実際にハイパーリンクがどのように記述されているかを知る必要がある.ハイパーリンクはhtml内で以下のように記述されている.
<!-- 今のタブで開く -->
<a href="hogehoge" target="_self"> fuga </a>
<!-- 新しいタブで開く -->
<a href="hogehoge" target="_blank"> fuga </a>
hrefによってリンク先のURLを指定し,targetによってどのタブで開くかを指定している.また,targetは省略するとデフォルトで"_self"になるようである.
よって,各リンクに対してこのtargetの値を取得し,その値によって色を変更してやればよいということだ.
How to 手探り
Chromiumに対してどのような機能を実装したいか,そのためにはどのような変更を加えればよいかはおおよそ定まったのであとは変更をすればよいのであるが,いかんせん規模が大きすぎるためにどの部分を変更すれば所望の機能を実装できるのかが簡単には見つからない.そのため,何らかのツールを使って効率的に手探る必要があり,この部分がこの実験のメインとなるところであろう.以下,我々が使用したツールを紹介する.
- gdb
gdbのホームページ
ステップ実行したり,ブレークポイントを置いて適当な関数で実行を止めたりと便利なやつ.我々が使用した限りでは,Chromiumが大規模であるためにシンボルを読み込むのにかなり時間がかかる上,Chromiumが複数プロセスを生成しているのかマルチスレッドなのか知らないがどうもうまくブレークポイントに止まってくれず,あまり使用していない.きっとうまく使ってやれば役に立ってくれる子なのであろう. - Code Search
Chromium Code Search - The Chromium Projects
Googleの提供しているChromiumのコード検索機能である.言語やディレクトリを指定して検索できる.また,ソースコード上で関数や変数を選択するとその関数や変数の定義や,関数の呼び出し元を表示する機能もあり,gdbでうまく追跡できなかったときに大変重宝した. - printf
原始的な手法であるがとても有用.適当な場所にLOG(INFO) << "hoge";
などと記述して実行してやることによって変数の中身を見たりすることができる. - 強制セグフォ
関数の呼び出し元が知りたいときなどに,関数内でNULLポインタにアクセスするなどしてSegmentation Faultを起こさせることによって,デバッガにコールスタックを表示させてその内容から実行の流れを知ろうというもの.あまりよろしくないかもしれない.
初めての手探り
とりあえずhtmlをparseしているところとかレンダリングしているところまで実行してみようとしてgdbでステップ実行してみるも進めど進めど目的の場所へとはたどり着かない.CodeSearchで検索をかけてみるも見つからない.リンクの色を指定していそうな場所を見つけるもWindows用のライブラリであって関係ないことが判明する.などなどいろいろやってはみるものの何の成果も得られないときが数日続いた.つらかった.
発見
ChromiumのレンダリングにはBlinkというレンダリングエンジンが使われているということを知り,"third_party/blink/"というディレクトリ内を調べてみることにした.このディレクトリの中で"linkcolor"などとそれらしい単語で検索をかけていると以下のようにいかにもな部分を発見した.
void TextLinkColors::ResetLinkColor() {
link_color_ = Color(0, 0, 238);
}
void TextLinkColors::ResetVisitedLinkColor() {
visited_link_color_ = Color(85, 26, 139);
}
void TextLinkColors::ResetActiveLinkColor() {
active_link_color_ = Color(255, 0, 0);
}
この関数の呼び出しもとを探ってみると,
"src/third_party/blink/renderer/core/dom/text_link_colors.cc"内の"ColorFromCSSValue"という関数の中でリンクの色を決定していることが判明した.
ということで,実際にどのタブで開くか,つまりtargetの値によってこの関数内に分岐を作ってやればよさそうであるが,どうにもこの関数にはtargetの情報は渡されてないようである.そこでこの関数の呼び出し元を見てみるとそのほとんどにおいて"state"という名前の変数の中にtargetの情報が含まれているようであった.そこでColorFromCSSValueの引数に新たにstateを加えることとした.
変更その1
実際に変更を施した点は以下の通りである.
まず,リンクの色を扱っているTextLinkColorsクラスに新しいタブで開く場合の変数や関数を追加した.そして,ColorFromCSSValueの引数にstateを追加し,
state.GetElement().getAttribute(html_names::kTargetAttr)で取得したtargetの値によって分岐を作成した.また,stateを利用できるようにするためにstateのクラスの宣言とそのクラスの情報が書かれたファイルをインクルードした.
…
class StyleResolverState;
…
public:
…
const Color& NewTabLinkColor() const { return newtab_link_color_; }
const Color& NewTabVisitedLinkColor() const { return newtab_visited_link_color_; }
void SetNewTabLinkColor(const Color& color) { newtab_link_color_ = color; }
void SetNewTabVisitedLinkColor(const Color& color) { newtab_visited_link_color_ = color; }
void ResetNewTabLinkColor();
void ResetNewTabVisitedLinkColor();
//stateを追加したColorFromCSSValue
Color ColorFromCSSValue(const CSSValue&,
Color current_color,
WebColorScheme color_scheme,
bool for_visited_link,
StyleResolverState& state) const;
private:
…
Color newtab_link_color_;
Color newtab_visited_link_color_;
…
…
#include "third_party/blink/renderer/core/css/resolver/style_resolver_state.h"
…
TextLinkColors::TextLinkColors() : text_color_(Color::kBlack) {
…
ResetNewTabLinkColor();
ResetNewTabVisitedLinkColor();
}
…
void TextLinkColors::ResetNewTabLinkColor() {
newtab_link_color_ = Color(0, 238, 0);
}
void TextLinkColors::ResetNewTabVisitedLinkColor() {
newtab_visited_link_color_ = Color(238, 0, 0);
}
//stateを追加したColorFromCSSValue
Color TextLinkColors::ColorFromCSSValue(const CSSValue& value,
Color current_color,
WebColorScheme color_scheme,
bool for_visited_link,
StyleResolverState& state) const {
…
if (auto* pair = DynamicTo<CSSLightDarkColorPair>(value)) {
…
return ColorFromCSSValue(color_value, current_color, color_scheme,
for_visited_link, state);
}
…
switch (value_id) {
…
case CSSValueID::kWebkitLink:
if (state.GetElement().getAttribute(html_names::kTargetAttr) == "_blank") {
return for_visited_link ? NewTabVisitedLinkColor() : NewTabLinkColor();
} else {
return for_visited_link ? VisitedLinkColor() : LinkColor();
}
…
}
…
もともとリンクを訪問済みかで色分けはなされていたので,新しいタブで開くときも訪問済みかどうかで色分けがされるようにもしてある.また,ColorFromCSSValueの呼び出し元の中にはstateがないものも存在したので,stateを引数に加えたものでオーバーロードするという実装にすることでそのような場合にはもとの動作のままとなるようにした.また,インクルードを"text_link_colors.h"ではなく"text_link_colors.cc"で行っているのは相互インクルードを防ぐためである.
さらに,ColorFromCSSValueの呼び出し元のうちstateを持っているものでは引数にstateを加えた.具体的には以下の箇所で変更した.
- src/third_party/blink/renderer/core/css/resolver/style_builder_converter.ccの120,1416,1430,1795行目
- src/third_party/blink/renderer/core/dom/text_link_colors.ccのオーバーロードしたColorFromCSSValue内での呼び出し
バグった
いざテスト!
あれ…
一段目の色変わってないじゃん
手探ってみる
早速手探ってみる。
しばらくhtmlを書き換えて実験してみると、
どうやらhtmlの階層?の一番浅いところだけは一番最初の
リンクの種類に合わせてカラーリングしていることがわかった。
とりあえずcolorfromCSSvalue()の呼び出し元に順にログを吐き出させてみると、
style_resolver.cc内のApplyMatchPriorites()の中の
ApplyMatchHighPriorites()の呼び出し前のcache_success.ISFullCacheHit()の値で分岐していることが判明(これもだいぶ遡らないといけなくて大変だった…)。
どうやらキャッシュの機能が原因らしい。
ここで更にキャッシュの生成や比較を手探ってもいいけど、時間がないので
とりあえず手荒く、キャッシュの機能をスキップさせてみると
無事手元のhtmlでもカラーリングが正しくなった!!
変更その2
ということで、ApplyMatchedCache()内を
if(!state.GetElement().hasattribute(html_names::KHrefAttr)){
//以下キャッシュの判定
}
とスキップさせていただき、完成!!
感想
今回はChromiumに対して「リンクの色を新しいタブで開くか否かで変える」という改良を行いました.目標を立てた当初は,この機能の実装は比較的容易にできると思っていました.「大規模ソフトウェアを手探る」では合計8日間×3時間半の時間を使えるので,この機能の改良を数日で終わらせて残りを別の機能(ショートカットキーをカスタマイズ可能にする等)の実装に費やそうと考えていましたが,結局この機能の実装にすべての時間を費やしてしまいました.
私達が行ったコードの変更は全て合わせても数十行程度に収まったのですが,大量のソースコードの中のどこを改良すればよいのかを見つけることに非常に苦労しました.実際に大規模ソフトウェアを手探ってみて,大規模ソフトウェアの中身を解析することの難しさを思い知らされると同時に,そのようなソフトの開発者の方々に尊敬の念をいだきました.
はじめは、大きなプロジェクトだからソースもコーディング規約がきっちり守られている分わかりやすいかなと思っていたけど、そもそもの構造が複雑過ぎて断片でも理解するのが大変でした。今回追加した機能は結構便利だと思うんですけど、なんでデフォルトでないんでしょうね、歴史的経緯とかでしょうか。ブラウザに多くを求めるのは一般的ではないかもしれないですね(今回グループを組んだ2人はアドオンも入れてなかったらしい)。