14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Firefoxのソースを手探ってみる

Last updated at Posted at 2016-11-06

大学の実験「大規模ソフトウェアを手探る」で,Firefoxのソースコードを手探ったまとめです.
このページは実験レポートの一部です.こちらも御覧ください.

実験期間全10回(正確にはチュートリアルと発表を除いた8回)の間に,中身を見たこともないFirefoxのコードをどのように手探り,どのように変更していったのかを主に時系列順にまとめていきます.

機能としては,次の2つを実装しました.

  • URLの隠蔽機能
  • "タブを複製"メニューの追加

目次

  1. Firefoxとは
  2. URLの隠蔽機能の実装過程
  3. "タブを複製"メニューの追加過程

Firefoxとは

言わずと知れたブラウザです.ここを見てくださっている方は誰もがご存知だと思うので,詳細については省略します.

URLの隠蔽機能

Firefoxのコードを触るのは初めてだったので,まず最初に,簡単に実現できそうな,"URLを隠蔽する"という機能を実装してみることにしました.
次に示す画像のイメージです.URLの文字列を隠蔽します.
なお,この機能には実用性が全くないので,お遊びだと思っていただけるとありがたいと思います.

スクリーンショット 2016-11-05 0.58.37.png

この機能だけに言えたことではないですが,どの機能もブラウザの拡張機能を使って実現した方が良いです.しかし今回はソースコードを手探ることが目的であることから,その辺りは気にしないこととします.

まずはソースコードを見てみる

コードを手探っていく前に,Firefoxのソースのディレクトリ構造を見てみます.
ソースコードは,Firefox の簡単なビルド方法を参考に拾ってきます.

ソースコードをダウンロードして中身を見ると,さすがFirefox,大量のファイルがあります.一から調べていくと実験期間が終わってしまいそうです.
そこで今回は先輩の力を借ります.去年同じ実験でFirefoxをいじった人のレポート(ブログ)Mozilla Firefoxをほんの少し拡張を見ると,

ソースファイルは全て「mozilla-central」直下の「objdir-ff-release」に展開されます。主にcppファイルとxul,xml,html,css,jsファイルが存在し、GUIに近いイベント処理はjsが担ってることが分かりました。

とあります(ありがとうございました…非常に参考になりました…)

時系列がずれますが,色々いじっていくと,実際には次のような構成になっていることがわかってきました.

  • メモリ管理,スレッド管理など:C++
  • GUI:JavaScript
  • XUL(ズール),XPCOM:両者を結びつける

コンパイルと実行

詳しくは,同じ班の人が書いてくれた,ビルドについての記事を参考にしてください.
build:http://qiita.com/fujibo/items/59c74113389ba8a8b49d
方法だけ書きますと,ターミナルにて,ダウンロードしたディレクトリに移動し,

./mach buile

と打つことで,コンパイルとビルドが実行されます.
また,

./mach run

と打つことで,ビルドしたFirefoxが実行されます.

手探るべきファイル(群)を決める

さて,URLの表記を変更するということで,今回触るべきはどこかを考えると,GUI周りのJavaScriptであることが予測できます.
ですが,JavaScriptのファイルを,'find ./ -name "*.js"'のように検索すると,大量に候補があることがわかりました.
ここでも先輩の力を借りました.先ほどのサイトより,

そこそこの数がヒットしますがおそらく怪しいのはbrowser.jsという明らかに「僕browser関連引き受けてます」みたいなプログラム。

実はこのbrowser/chrome/browser/content/browser/にブラウザのGUI関連の多くがまとめられています。

先輩に感謝しながらディレクトリを見ていくと,他にもbrowser/base/content/というディレクトリがあり,こちらがメインのように思えたので,この中身を手探っていくことにしました.
ここまでで,どこから探っていくべきかの方針が立ちました.

どうやって手探るか

次に決めなければならないのは,どうやって探るかということです.方法としては

  • GDBでデバッグ
  • printデバッグ
  • grepで該当箇所を探す

があります.
例えば今回はURLBarに表示されている文字列を変えたいということなので,これらの方法を用いて該当箇所を探すことが重要です.
GDBでデバッグというのは,GDBで追跡しながらURLBarにアクセスしていそうな場所を見つけるという意味です.printデバッグも,print機能を使ってどの順で動いていくかを探るのでほぼ同じです.
ただFirefoxが非常に大きなソフトウェアなため,URLBarにアクセスしている箇所を見つけるまでに多くの時間がかかりそうなこと,また,そもそもデバッグしたいのがJavaScriptでありGDBが使えないことから,まずはgrepでそれらしい場所を探すこととします.

実際に手探る

それでは,先ほどのbrowser/base/content/browser.jsに対して,'grep -rnI "URL" browser.js'としてgrepサーチをかけます.
すると大量の検索結果が表示されるので,根気強く探していくと,ありました.次のようなコードがbrowser.jsの2378行目に見つかりました.

browser.js
function URLBarSetURI(aURI) {
  var value = gBrowser.userTypedValue;
  var valid = false;

  if (value == null) {
    let uri = aURI || gBrowser.currentURI;
    // Strip off "wyciwyg://" and passwords for the location bar
    try {
      uri = Services.uriFixup.createExposableURI(uri);
    } catch (e) {}

    // Replace initial page URIs with an empty string
    // 1. only if there's no opener (bug 370555).
    // 2. if remote newtab is enabled and it's the default remote newtab page
    let defaultRemoteURL = gAboutNewTabService.remoteEnabled &&
                           uri.spec === gAboutNewTabService.newTabURL;
    if ((gInitialPages.includes(uri.spec) || defaultRemoteURL) &&
        checkEmptyPageOrigin(gBrowser.selectedBrowser, uri)) {
      value = "";
    } else {
      // We should deal with losslessDecodeURI throwing for exotic URIs
      try {
        value = losslessDecodeURI(uri);
      } catch (ex) {
        value = "about:blank";
      }
    }

    valid = !isBlankPageURL(uri.spec);
  }

  gURLBar.value = value;
  gURLBar.valueIsTyped = !valid;
  SetPageProxyState(valid ? "valid" : "invalid");
}

名前からして,URLBarに表示されるURLを決定していそうです.
中身を見ていくと,次のような一文がありました.

browser.js
        value = losslessDecodeURI(uri);

urlを整形して,valueに入れる…まさにURLBarに表示する文字列を決めていそうです.

実行されているかのチェック

コードを変更する前に,本当にここが実行されているのかをチェックします.
こういう時はprintデバッグだ!と思い,console.log("...")と書いて実行します…が反応がありませんでした.
おかしいな?ということで,ファイルの先頭にconsole.log("...")と書いてみると,こちらも反応しませんでした.
どうやらJavaScriptのlog出力はターミナル側までは実行されないようです.
ここで私が思いついたのが,わざとエラーが出る記述をその場所に書いておくことです.先ほどのURLBarSetURI関数の中に,適当な文字列を入れます.

browser.js
function URLBarSetURI(aURI) {
		abcde
       ...
       value = losslessDecodeURI(url)
       ...
}

この状態で実行し,検索などをしてみると,Enterキーを押した瞬間にターミナル側にエラーが出た旨の表示が出ました.
どうやら,予想通りのタイミングで実行されているようです.

ちなみに他のチームメンバーがalertメソッドが実行できることに気づいたので,この後は代わりにalert("...")をつけることでデバッグを行いました.

(ところで,また時系列がずれますが,この後の機能を実装している間にJavaScriptのデバッガを見つけたので,デバッガ発見後はそれを用いました)

コードを改変する

実行されていることがわかれば後は書き換えるだけです.

browser.js
function URLBarSetURI(aURI) {
       ...
       value = "??? confidential ???"
       ...
}

と無理やり文字列を書き換えます.コンパイルして実行し,適当な文字列で検索をかけます.すると,次の画像のように,URLに表示される文字列を置き換えることに成功しました.

スクリーンショット 2016-11-05 0.58.37.png

色々な作業を行って見ても,URLBarは隠蔽されたままでした.
これで,URL隠蔽機能が実現できました.

まとめ

結果としては1行書き換えただけですが,その場所を探すのが非常に大変でした…
いざまとめてしまうと簡単ですが,これをひたすら手探るのは骨が折れす作業です.

ここまで作業としては4日かかっています(うち2日がビルドとgit周りですが)

さて,後4日分あるので,もう一つの機能の実装に試みました.

"タブを複製"メニューを追加

もう一つ何を実装しようかなと考えたところ,"タブを複製"メニューの追加が良さそうだと考えました.
私は普段GoogleChromeを使っているのですが,時々使う機能に"タブを複製"があります.
Chromeでは,右クリックメニューに次の図のように表示されています.

スクリーンショット 2016-11-05 0.58.37.png

ネットショッピングをしている時など,今のタブを残したまま別の検索結果を表示したい時に,"新しいタブ→ショッピングサイト検索→商品名検索"が面倒臭いので,"タブを複製→商品名検索"という操作を行うことが多いです.
何度かFirefoxを使ったことはあるのですが,不満点の一つがこの”タブを複製”機能がないことでした.

今回は,これをFirefoxに追加しようと考え,実装しました.

コードを手探る

それでは,コードを手探っていこうと思います.
上と同じく,grepでそれらしいキーワードを検索します.今回は右クリックメニューを意味する"context menu"で検索しました.
'grep -rnI "contextmenu" browser/base/content'を行うと,色々な結果が出てきますが,その中で,nsContextMenu.jsというファイルが結果に上がってきました.
中を見てみると,

nsContextMenu.js
...
    this.showItem("context-openlink", shouldShow && !isWindowPrivate);
    this.showItem("context-openlinkprivate", shouldShow);
    this.showItem("context-openlinkintab", shouldShow && !inContainer);
    this.showItem("context-openlinkincontainertab", shouldShow && inContainer);
    this.showItem("context-openlinkinusercontext-menu", shouldShow && !isWindowPrivate && showContainers);
    this.showItem("context-openlinkincurrent", this.onPlainTextLink);
    this.showItem("context-sep-open", shouldShow);
...

というように,明らかにリンクを右クリックした時のメニューが書いてあります.これじゃないか?と思いながら,タブを右クリックした時のメニューを探します."new tab"などのワードでファイル内を検索しました…が見つかりません.何故かタブを右クリックした時のメニュー名が一つも見つかりませんでした.

このgrep作業だけで1日分使った気がします…

デバッガ発見

突然デバッガの話題になってしまいましたが,時系列的にこのタイミングで見つかったので,ここでそれについて書きたいと思います.

「firefox js デバッグ」などで検索すると,ほとんどのサイトが,"Firefoxを用いて,Webサイト上のJavaScriptをデバッグする"方法について書いてあります.
今回知りたいのは,"Firefox自身のJavaScriptをデバッグ"したいので,上述のものでは対応できません.

タブのContextMenuのgrep作業で疲れたので,ネットで"mozilla javascript debug"というワードで検索してみたところ,次のようなサイトを見つけました.

Debugging Javascript

中を見ると,

This document is intended to help developers writing JavaScript code in Mozilla, mainly for Mozilla itself, but it may also be useful for web developers. It should give pointers to tools, aids and tricks which make debugging your code easier.

とあります.

...mainly for Mozilla itself...

とあるので,これを使えばFirefox本体をデバッグできそうです.
指示に従って設定を行ってみます.

  1. URLBarに"about:config"と入力し,Firefoxの詳細設定画面を出します.
  2. 検索ウィンドウに"devtools.chrome.enabled"と入力すると,同名の設定項目が表示され,"false"となっているところを"true"に変更します.
  3. "devtools.debugger.remote-enabled"についても同様にfalseの所をtrueに変更します.
  4. 一度再起動をかけると,一番上のメニュー画面のTools/Web Developer/BrowserToolboxという項目が利用可能になり,これを選択します.すると次のような画面が出てきます.
スクリーンショット 2016-11-05 0.58.37.png

このツールを使うと,JavaScriptファイル,XULファイルにたいしてブレークポイントを貼れるようになります.

再び手探る

さて,デバッガという武器を手にしたので,効率よくコンテキストメニューの場所を探せそうです…が,ブレークポイントを貼る場所を決めるために結局メニューを司る部分を探さなくてはいけません.
再び"pin tab"あたりのワードで検索をかけると,今度は見つかりました.browser.jsの7539行目に,

browser.js
...
document.getElementById("context_pinTab").hidden = this.contextTab.pinned;
...

という記述が見つかりました.どうやら別の場所で定義されているメニューの項目を呼び出し,ピン留されているか確かめているようです.
試しにブレークポイントを貼ってタブの上で右クリックを行うと,ちゃんと止まることが確認できました.
挙動を調べるために,ここからステップ実行してどのように動作しているのかを確かめます.
すると,まずbrowser.js内の
document.getElementById("context_pinTab").hidden = this.contextTab.pinned;
次に,tabbrowser.xml内の

return this.getAttribute("pinned") == "true";

など多くのファイルを経由しながら,7576行目の

	...
   this.contextTab.addEventListener("TabAttrModified", this, false);
    aPopupMenu.addEventListener("popuphiding", this, false);
  },

の最後の部分でステップインを行うと,browser.xulというファイルにアクセスすることがわかります.

結果,重要そうなファイルとして,tabbrowser.xmlとbrowser.xulというファイルにアクセスできることがわかりました(デバッガはちゃんとこれらのファイルにアクセスする様子も見せてくれました)

通った場所から,メニューを設定している部分を探すと,tabbrowser.xmlの265行目にコールバック関数の記述があることが確認できました.

tabbrowser.xml
      <method name="pinTab">
        <parameter name="aTab"/>
        <body><![CDATA[
          if (aTab.pinned)
            return;

          if (aTab.hidden)
            this.showTab(aTab);

          this.moveTabTo(aTab, this._numPinnedTabs);
          aTab.setAttribute("pinned", "true");
          this.tabContainer._unlockTabSizing();
          this.tabContainer._positionPinnedTabs();
          this.tabContainer.adjustTabstrip();

          this.getBrowserForTab(aTab).messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: true })

          if (aTab.selected)
            this._setCloseKeyState(false);

          let event = document.createEvent("Events");
          event.initEvent("TabPinned", true, false);
          aTab.dispatchEvent(event);
        ]]></body>
      </method>

また,browser.xulを見ると,メニューの一覧を設定しているらしい場所が見つかります.

browser.xul
  <popupset id="mainPopupSet">
    <menupopup id="tabContextMenu"
               onpopupshowing="if (event.target == this) TabContextMenu.updateContextMenu(this);"
               onpopuphidden="if (event.target == this) TabContextMenu.contextTab = null;">
      <menuitem id="context_reloadTab" label="&reloadTab.label;" accesskey="&reloadTab.accesskey;"
                oncommand="gBrowser.reloadTab(TabContextMenu.contextTab);"/>
      <menuitem id="context_toggleMuteTab" oncommand="TabContextMenu.contextTab.toggleMuteAudio();"/>
      <menuseparator/>
      <menuitem id="context_pinTab" label="&pinTab.label;"
                accesskey="&pinTab.accesskey;"
                oncommand="gBrowser.pinTab(TabContextMenu.contextTab);"/>
      <menuitem id="context_unpinTab" label="&unpinTab.label;" hidden="true"
                accesskey="&unpinTab.accesskey;"
                oncommand="gBrowser.unpinTab(TabContextMenu.contextTab);"/>
      <menuitem id="context_openTabInWindow" label="&moveToNewWindow.label;"
                accesskey="&moveToNewWindow.accesskey;"
                tbattr="tabbrowser-multiple"
                oncommand="gBrowser.replaceTabWithWindow(TabContextMenu.contextTab);"/>
...

ここでメニューを設定しているようなので,試しに,menuitemを追加してみます.

browser.xul
      <menuitem id="context_toggleMuteTab" oncommand="TabContextMenu.contextTab.toggleMuteAudio();"/>
      <menuseparator/>
      <menuitem id="context_pinTab" label="&pinTab.label;"
                accesskey="&pinTab.accesskey;"
                oncommand="gBrowser.pinTab(TabContextMenu.contextTab);"/>

となっている所を,次のように変更してみます.

browser.xul
      <menuitem id="context_toggleMuteTab" oncommand="TabContextMenu.contextTab.toggleMuteAudio();"/>
      <menuseparator/>
      <menuitem id="context_toggleMuteTab" 
      label = "テスト"
      oncommand="TabContextMenu.contextTab.toggleMuteAudio();"/>
      <menuseparator/>
      <menuitem id="context_pinTab" label="&pinTab.label;"
                accesskey="&pinTab.accesskey;"
                oncommand="gBrowser.pinTab(TabContextMenu.contextTab);"/>

toggleMuteTabを複製し,label = "テスト"として追加します.
コンパイルして実行すると,

スクリーンショット 2016-11-05 0.58.37.png

となり,狙い通りにメニューが増えていることがわかります.また,メニューをクリックするとMute機能がON/OFFすることも確認できます.

コードを改変する

以上より,"タブを複製"メニュー追加するためには,次のことが必要だと考えられます.

  1. tabbrowser.xmlに,タブを複製するメソッドを追加する
  2. browser.xulに,id, label, oncommandをそれぞれ追加する

まずは1からやってみます.tabbrowser.xmlにタブを複製するduplicate_tabメソッドを追加しようとしたのですが,3177行目に

tabbrowser.xml
      <method name="duplicateTab">
        <parameter name="aTab"/><!-- can be from a different window as well -->
        <body>
          <![CDATA[
            return SessionStore.duplicateTab(window, aTab);
          ]]>
        </body>
      </method>

という記述があり,すでにあることが確認できました.
コードを書く必要があるのかと身構えましたが,結局このメソッドのみで良さそうです.

次に,browser.xulにmenuitemを追加します.さっきのテストで追加したコードを次のように書き換えます.

browser.xul
      <menuitem id="context_toggleMuteTab" oncommand="TabContextMenu.contextTab.toggleMuteAudio();"/>
      <menuseparator/>
      <menuitem id="context_dupTab" 
      label = "duplicate tab"
      oncommand="gBrowser.duplicateTab(TabContextMenu.contextTab)"/>
      <menuseparator/>
      <menuitem id="context_pinTab" label="&pinTab.label;"
                accesskey="&pinTab.accesskey;"
                oncommand="gBrowser.pinTab(TabContextMenu.contextTab);"/>

idは適当に"context_dupTab"とし,label(表示される名前)は"duplicate tab"としてみます.oncommand(実行されるメソッド)は下のpinTabを参考に,duplicateTabメソッドを書きます.

さて,これで準備は整いました.コンパイルし,実行し,タブ上で右クリックをすると…

スクリーンショット 2016-11-05 0.58.37.png

ついに"タブを複製"を追加することができました.
クリックすると,ちゃんとタブが複製されました.

感想と今後の課題

今後の課題としては,タブを複製メニューを追加する際に,label(表示名)をハードコーディングしている点です.他のメニューでは"&reloadTab.label"となっていて,実体は別の場所にあるそうです.探したのですが,見つかりませんでした…
この状態だと,多言語に対応できないなど様々な問題点があります.

感想としては,「ブラウザを扱うのは大変だ」の一言に尽きます.
GUIはJavaScript,コア部分はC++,ビルドツールはPythonなど,様々な言語で書かれており,また行数も数十万行に及びます.まさに「大規模ソフトウェア」と呼べる代物だったと思います.
今回は,FirefoxのGUI周りを読むことで,こういったソフトウェアがどのようにGUIのコードを書かれていて,イベント駆動動作がどのように行われているかの理解が深まったように思います.

14
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?