僕と学科と男の娘アイドル

この記事は、eeic Advent Calendar 2016 その2、23日目の記事です。
qiita.com

eeic Advent Calendar 2016とはいえその2だし、ゆる〜い記事でいいかな〜とか思ってたら皆さん強い記事ばかりで戦々兢々としております。
ここらでいっちょ、この後に書く方への(あるいは来年度書く方への)ハードル下げにでもなればいいかなと思います。

                                               

さて、eeic。つらいですね。

溜まる課題、終わらない実験、強い先輩、強い同期、強い後輩…etc.
全部をまともに受け止めてたら精神が崩壊して某氏のように「チノちゃん」しか話せない身体にされてしまいます。

哀れeeicの犠牲者Zくん。彼はチノちゃんの救いを一心に求めています。


今回はこのつらい日常を僕が生きていられる心の支えについてお話し、もしかしたらこれを読んで下さっている皆さんが新しい扉を開くお手伝いなんかもできたら、と考えております。

男の娘との出逢い

思い返せばHUNTERXHUNTERのクラピカを始め、性別は問わず中性的なキャラクターを推す傾向が僕にはありました。
そして2010年、僕の嗜好を決定づけるあのキャラクターに出逢います。

f:id:iuias:20161221104706j:plain

はい。
ダンガンロンパ 希望の学園と絶望の高校生」における超高校生のプログラマー、不二咲千尋です。
(書いてて気づきましたがこれ自体が結構なネタバレになってしまいます。が、アニメも既に放送済みだし多少はね?)

本編チャプター2でスポットの当たるかわいさと強さに僕の心は射抜かれ、千尋の属性であるところの「男の娘」にどっぷり嵌まり込んでしまいました。
千尋について語ろうとするとそれだけでまた一本記事が書けるのですが、今回はこのくらいにしておきます。

「男の娘」という扉が開かれた僕は2016年3月、あのゲームと出会ってしまいます。

邂逅、あんさんぶるスターズ!

f:id:iuias:20161221110824j:plain

あんさんぶるスターズ!です。

ゲーム自体はこのように紹介されています。

舞台は男性アイドルを育成する「私立夢ノ咲学院」。
あなたは、そんな夢ノ咲学院に転校してきたたった一人の女子生徒。
プロデュース科の第1号として選ばれ、アイドルたちをプロデュースすることに… (公式サイトより引用)

早い話がプロデューサーの女の子になって男性アイドルとキャッキャウフフするゲームです。
eeicには女の子しかいませんから、女の子プロデューサーにこそなるべきなのです。
デレステなどのような音ゲーではなく、アイドル自体のカスタマイズとストーリーを楽しむゲームになっています。

また、あんスタの特徴はしっかりと作り込まれたストーリーにあります。
メインライターは「ささみさん@がんばらない」などの日日日先生、サブライターは結城由乃先生です。
意外と重い内容(精神崩壊からの多重人格、夢破れて自殺など)になっていて、「アイドルもの」って感覚で始めた僕は大変な衝撃を受けました。
これがまた深い課金沼へとハマっていく理由になっているわけです…。

他の多くのソーシャルゲームと同じように、あんスタにも「イベント」という概念があります。
イベント時にはストーリーが書き下ろされ、イベント専用プロデュースが行えます。しかしこのゲーム、そのイベントのペースが少しどうかしています。
イベントスケジュールが次の図です。
f:id:iuias:20161221114052p:plain
何も無い日が4日しかありません。頭おかしいんじゃねえの。
こんなスケジュールなので、僕がeeicでの生活を放り出して人生を捧げるには丁度良かったのかもしれません。

しののんぅぅうううわぁあああああん!!!

心の声が漏れ出てしまいましたが、このゲームでの僕の一推しキャラクターは「しののん」こと紫之創(しのはじめ)くんです。

f:id:iuias:20161221115004j:plain
首筋に息を吹きかけられて驚くしののん。慌てる様子もかわいい。

3年生1人と1年生3人から構成される初心者ユニット「Ra*bits」のメンバーであり、献身的でやや人見知りな男の子です。本人が素でやってることが結果として死人が出るレベルのあざとさになるような子です。
プレイヤーの分身である「転校生ちゃん」によくスカートを穿かせられそうになっていますが、気持ちはわかります(よね?)。

夢ノ咲学院の誇る三人の御曹司(天祥院英智、姫宮桃李、朱桜司)をオトす隠れた人たらしでもあります。
f:id:iuias:20161221120126j:plain
微笑ましきかな。天使と天使がお話してるもの。

こんなしののんですが、家が貧乏で苦労するときもあるようです。

f:id:iuias:20161221120557j:plain
僕が養ってあげたい。

また最近のイベントではこんな発言もしてます。

f:id:iuias:20161221120659j:plainf:id:iuias:20161221120707j:plain
しののんの闇が垣間見える発言。誰にとって都合がいいんだ?それを君は受け入れるのか?

こんなに多角的に攻められたらしののんのことを好きになるしかなくないですか?
ないですよね?ないです。(断定)

しののんが好きすぎてうっかりイベント走りすぎたりもしました。

f:id:iuias:20161221124225j:plain
ランキング報酬が最高レア度しののんのイベント。700位を目標に!とか言ってたのに気がついたら329位でした。不思議な事もあるものです。

かわいいしののんの画像を見て何とか精神の均衡を保っています。しののんが居なければ僕はもう生きていけません。

f:id:iuias:20161221124743j:plainf:id:iuias:20161221124749j:plainf:id:iuias:20161221124756j:plainf:id:iuias:20161221124802j:plainf:id:iuias:20161221124857j:plain
あぁああああ…ああ…あっあっー!あぁああああああ!!!しののんしののんしののんぅううぁわぁああああ!!!

ここまでこの記事を読んで下さっているあなたならもう、しののんの尊さには気付かれていることでしょう。
男の子でありながら可愛い、けれど本人は自分は男だと主張する。そんな不安定で美しい存在。それが男の娘であり、しののんなのです。
「わかる!」と少しでも思った貴方。扉は開きかけているようですよ?

さいごに

あんスタ絡みで言えば「イベントシミュレータ」(一日当たりの獲得ポイント目安を示してくれるツール)をpythonで作ったりしてたので、電子情報の人間なんだからそれを紹介すればよかったかなとも思います。こんな風に使ってます。


これの紹介は気が向いたらやります。

というかこの記事、eeicと何も関係ないですね。
ともあれ、しののんかわいいよ記事でした。皆であんスタやりましょう。

刀剣鑑賞記録#1 宗三左文字

はじめに

大規模ソフトウェアのレポートのために作成したはいいものの、
何に使おうか一切考えていなかったがために更新が滞っていました。

そこで、以前からの趣味である刀剣鑑賞の記録をつけることにしました。
見て終わり!じゃなくて、見てどう思ったのか後で見返す楽しみを生もうという次第ですね。

戦国時代展

f:id:iuias:20161204164654j:plain

ということで、行って参りました「戦国時代展」。
江戸東京博物館にて2017年1月29日まで開催されている特別展です。
戦国時代に日本で生成された様々な歴史資料や美術工芸品を、時代の流れと共に追うことが出来る展示になっています。
刀剣だけでなく、甲冑や青磁器、書状など戦国時代の生活の一端に触れることができるものでした。
観覧料は一般1350円、大学生1080円(その他は公式サイトをご確認下さい)です。

戦国時代展では会期中に展示品の入れ替えがあり、今回の目的である「宗三左文字(義元左文字)」は2016年12月18日までの展示となっています。
一方、「五虎退」や「童子切安綱」の展示も2017年1月2日から控えているため、年をまたいでもう一度行くことになります(確信)。

宗三左文字って?

f:id:iuias:20161204171020p:plain

南北朝時代の刀剣です。旧国宝であり、現在は重要文化財に指定されています。
筑前隠岐の左衛門尉安吉(左文字源慶)の作と伝えられています。
左文字源慶は銘に「左」の一文字を切るため「左文字」と呼ばれ、左文字一派の祖となった人物です。

号「宗三左文字」「義元左文字」の由来はどちらも所持者です。
前者は三好政長(隠居後の号が半隠軒宗三)、後者は今川義元にちなみます。
桶狭間にて今川義元織田信長に討ち取られた際、織田信長の手に渡り、4寸磨上げて金象嵌を入れ愛蔵されました。
その後も豊臣秀吉徳川家康の手に渡りました。別名「天下取りの刀」。
明暦の大火で一度焼けて再生されているため、当初とは刃文等が異なります。
現在は建勲神社(信長を祀る神社)所蔵です。

鑑賞+感想

文化遺産データベース
上のリンクから品質・形状について引用します。
「鎬造り、棟丸、無銘、彫り物表裏棒樋掻流し、鍛板目、刃文直刃に乱れ交じり。帽子尖り気味に返る。茎磨上げ、先栗尻、目釘孔二。茎表裏に金象嵌文字あり。」
正直なんのこっちゃ、という感じなので以下で軽く解説をば。

鎬造り
しのぎづくり。本造りともいう。鎬筋と横手*1のある日本刀の基本形。
棟丸
棟(刃の反対側の部分)が丸みを帯びている。
無銘
銘が切られていない。
彫り物表裏棒樋掻流し
刀表面に彫り物がしてあるかどうか。表裏両方に棒樋(ぼうび)*2が茎(なかご)まで通っている。
鍛板目
地金表面の様子が、板目肌*3である。
刃文直刃に乱れ交じり
刃文は直刃であり、部分的に乱れ(揺れやのたうち)が見られる。
帽子尖り気味に返る
帽子*4がやや尖って先端でUターンするように見える。
茎磨上げ
信長によって磨上げられているため、当初よりも茎の部分が短くなっている。
先栗尻
茎の下端(茎尻)が栗の尻のように丸みを帯びている。
目釘孔二
目釘孔*5が2つ開いている。
茎表裏に金象嵌文字あり
茎の両面に金象嵌(彫った後に金を嵌め込む)で文字がある。なお、内容は「織田尾張守信長」「永禄三年五月十九日 義元討刻彼所持刀」。

以下実際に見た感想です。
スラッとした印象を受ける刀身にはっきりと判別できるくらいわかりやすい板目肌でした。
切先は小さめで帽子は小丸、かわいらしくキレイな帽子でした。
刃文は薄く(幅が狭く、の方が正確かも)、すっと通った直刃が先のほうで少し乱れているように思いました。
信長が磨上げてくださったので、2つ目の目釘孔が茎尻ギリギリになってるのが面白かったです。
象嵌「義元討刻〜」もしっかり見えるように展示してくれてまして、単純に茎に彫られているよりはっきり見えるし綺麗でした。
確かに丸棟で、刀身に溝が茎までしっかり1本ありました。「天下取りの刀」と言われるだけの華やかさはありましたね。

さいごに

宗三左文字が見れる期間は残り少ないです。
ちょっと足を伸ばして見に行ってみては如何でしょうか?

*1:他の刀の各部位と合わせて日本刀の各部名称も参照するとよいかもです。

*2:まっすぐの溝

*3:木材の板目の様な文様

*4:切先における刃文

*5:柄(刀の持ち手)に茎を固定するための穴

Chromiumを手探った#4 - タブページを可変にしよう

#3はこちら

ソースコードを書き換え、タブページを自在に変更できるようにしていきます。

新しいタブで開くページの変更

問題の「新しいタブを開く」という行為を司る、.../src/chrome/browser/ui/browser_tabstrip.ccなるファイルが(不撓不屈のgrepによって)発見されました。この中のAddTabat()関数によってタブページが開かれるので、あとはこの関数のURLを書き換えるだけです。

  • .../src/chrome/browser/ui/browser_tabstrip.cc
...

void AddTabAt(Browser* browser, const GURL& url, int idx, bool foreground) {
  // Time new tab page creation time.  We keep track of the timing data in
  // WebContents, but we want to include the time it takes to create the
  // WebContents object too.
  base::TimeTicks new_tab_start_time = base::TimeTicks::Now();
  chrome::NavigateParams params(browser,
      url.is_empty() ? GURL(chrome::kChromeUINewTabURL) : url,
      ui::PAGE_TRANSITION_TYPED);
  params.disposition = foreground ? WindowOpenDisposition::NEW_FOREGROUND_TAB
                                  : WindowOpenDisposition::NEW_BACKGROUND_TAB;
  params.tabstrip_index = idx;
  chrome::Navigate(&params);
  CoreTabHelper* core_tab_helper =
      CoreTabHelper::FromWebContents(params.target_contents);
  core_tab_helper->set_new_tab_start_time(new_tab_start_time);
}

...

これを次のように書き換えます。

...

// includeするファイルの追加
#include "components/prefs/pref_service.h"
#include "chrome/common/pref_names.h"

...

void AddTabAt(Browser* browser, const GURL& url, int idx, bool foreground) {

  // 追加ここから
  PrefService* prefs = browser->profile()->GetPrefs();
  GURL new_dedede(prefs->GetString(prefs::kHomePage));
  // 追加ここまで

  // Time new tab page creation time.  We keep track of the timing data in
  // WebContents, but we want to include the time it takes to create the
  // WebContents object too.
  base::TimeTicks new_tab_start_time = base::TimeTicks::Now();
  chrome::NavigateParams params(browser,
      url.is_empty() ? new_dedede : url, // GURL(chrome::kChromeUINewTabURL)をnew_dededeに
      ui::PAGE_TRANSITION_TYPED);
  params.disposition = foreground ? WindowOpenDisposition::NEW_FOREGROUND_TAB
                                  : WindowOpenDisposition::NEW_BACKGROUND_TAB;
  params.tabstrip_index = idx;
  chrome::Navigate(&params);
  CoreTabHelper* core_tab_helper =
      CoreTabHelper::FromWebContents(params.target_contents);
  core_tab_helper->set_new_tab_start_time(new_tab_start_time);
}

...

ホームページの情報の取得方法は、.../src/chrome/browser/ui/views/toolbar/home_button.cc(ホームボタンの挙動に関するファイル)のOnPerformDrop()関数(ホームボタンが押された際に実行される)内でホームページ情報を取得している部分を参考にしました。
追加した#includeもhome_button.ccを参考に、preferenceに関するものです。


ソースコードを書き換えてビルドし直す(もう一度ninjaを実行する)ことで、変更点が適用されます。
これにより、新しいタブを開いた時に、ホームページとして登録されているURLを開くようになります。(これは設定から変更可能です)

問題点

改変したのがbrowser_tabstrip.ccのみなので、新しくChromiumを起動した時のページはkChromeUINewTabURLのままです。
つまり、設定画面でのタブページの変更が新しくChromiumを起動したとき(スタートアップページ)のみ適用されません。

スタートアップページもタブページに

起動時にページを開く挙動は、.../src/chrome/browser/ui/startup/startup_browser_creator_impl.cc内のAddStartupURLs()関数として定義されています。これをbrowser_tabstrip.ccと同じように書き換えれば良さそうです。

  • startup_browser_creator_impl.cc
...

void StartupBrowserCreatorImpl::AddStartupURLs(
    std::vector<GURL>* startup_urls) const {
  // If we have urls specified by the first run master preferences use them
  // and nothing else.
  if (browser_creator_ && startup_urls->empty()) {
    if (!browser_creator_->first_run_tabs_.empty()) {
      std::vector<GURL>::iterator it =
          browser_creator_->first_run_tabs_.begin();
      while (it != browser_creator_->first_run_tabs_.end()) {
        // Replace magic names for the actual urls.
        if (it->host() == "new_tab_page") {
          startup_urls->push_back(GURL(chrome::kChromeUINewTabURL));
        } else if (it->host() == "welcome_page") {
          startup_urls->push_back(internals::GetWelcomePageURL());
        } else {
          startup_urls->push_back(*it);
        }
        ++it;
      }
      browser_creator_->first_run_tabs_.clear();
    }
  }
  // Otherwise open at least the new tab page (and any pages deemed needed by
  // AddSpecialURLs()), or the set of URLs specified on the command line.
  if (startup_urls->empty()) {
    AddSpecialURLs(startup_urls);
    startup_urls->push_back(GURL(chrome::kChromeUINewTabURL));

    // Special case the FIRST_RUN_LAST_TAB case of the welcome page.
    if (welcome_run_type_ == WelcomeRunType::FIRST_RUN_LAST_TAB)
      startup_urls->push_back(internals::GetWelcomePageURL());
  }

  if (signin::ShouldShowPromoAtStartup(profile_, is_first_run_)) {
    signin::DidShowPromoAtStartup(profile_);

    const GURL sync_promo_url = signin::GetPromoURL(
        signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE,
        signin_metrics::Reason::REASON_SIGNIN_PRIMARY_ACCOUNT, false);

    // No need to add if the sync promo is already in the startup list.
    bool add_promo = true;
    for (std::vector<GURL>::const_iterator it = startup_urls->begin();
         it != startup_urls->end(); ++it) {
      if (*it == sync_promo_url) {
        add_promo = false;
        break;
      }
    }

    if (add_promo) {
      // If the first URL is the NTP, replace it with the sync promo. This
      // behavior is desired because completing or skipping the sync promo
      // causes a redirect to the NTP.
      if (!startup_urls->empty() &&
          startup_urls->at(0) == chrome::kChromeUINewTabURL) {
        startup_urls->at(0) = sync_promo_url;
      } else {
        startup_urls->insert(startup_urls->begin(), sync_promo_url);
      }
    }
  }
}

...

ところが、ここで問題が発生します。先ほどのAddTabat()関数は引数にBrowser* browserを取っていたため、browser->profile()->GetPrefs()を用いることができましたが、今回のAddStartupURLs()にはBrowser* browserが引数として指定されていません。
単純に引数を追加してもうまく行かず、ここだけは違う方法でGetPrefs()する必要があります。

ここではBrowser* browserは定義されていませんが、Profile* profile_が定義されているため、これを用いて、profile_->GetPrefs()とすると上手くいきました。以下が書き換えたあとのコードです。

...

void StartupBrowserCreatorImpl::AddStartupURLs(
    std::vector<GURL>* startup_urls) const {

  // 追加ここから
  PrefService* prefs = profile_->GetPrefs();
  GURL new_dedede(prefs->GetString(prefs::kHomePage));
  // 追加ここまで

  // If we have urls specified by the first run master preferences use them
  // and nothing else.
  if (browser_creator_ && startup_urls->empty()) {
    if (!browser_creator_->first_run_tabs_.empty()) {
      std::vector<GURL>::iterator it =
          browser_creator_->first_run_tabs_.begin();
      while (it != browser_creator_->first_run_tabs_.end()) {
        // Replace magic names for the actual urls.
        if (it->host() == "new_tab_page") {
          startup_urls->push_back(new_dedede); // GURL(chrome::kChromeUINewTabURL)をnew_dededeに
        } else if (it->host() == "welcome_page") {
          startup_urls->push_back(internals::GetWelcomePageURL());
        } else {
          startup_urls->push_back(*it);
        }
        ++it;
      }
      browser_creator_->first_run_tabs_.clear();
    }
  }
  // Otherwise open at least the new tab page (and any pages deemed needed by
  // AddSpecialURLs()), or the set of URLs specified on the command line.
  if (startup_urls->empty()) {
    AddSpecialURLs(startup_urls);
    startup_urls->push_back(new_dedede)); // GURL(chrome::kChromeUINewTabURL)をnew_dededeに

    // Special case the FIRST_RUN_LAST_TAB case of the welcome page.
    if (welcome_run_type_ == WelcomeRunType::FIRST_RUN_LAST_TAB)
      startup_urls->push_back(internals::GetWelcomePageURL());
  }

  if (signin::ShouldShowPromoAtStartup(profile_, is_first_run_)) {
    signin::DidShowPromoAtStartup(profile_);

    const GURL sync_promo_url = signin::GetPromoURL(
        signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE,
        signin_metrics::Reason::REASON_SIGNIN_PRIMARY_ACCOUNT, false);

    // No need to add if the sync promo is already in the startup list.
    bool add_promo = true;
    for (std::vector<GURL>::const_iterator it = startup_urls->begin();
         it != startup_urls->end(); ++it) {
      if (*it == sync_promo_url) {
        add_promo = false;
        break;
      }
    }

    if (add_promo) {
      // If the first URL is the NTP, replace it with the sync promo. This
      // behavior is desired because completing or skipping the sync promo
      // causes a redirect to the NTP.
      if (!startup_urls->empty() &&
          startup_urls->at(0) == new_dedede) { // GURL(chrome::kChromeUINewTabURL)をnew_dededeに
        startup_urls->at(0) = sync_promo_url;
      } else {
        startup_urls->insert(startup_urls->begin(), sync_promo_url);
      }
    }
  }
}

...

こうして、Chromiumを立ち上げたり、新しいタブを開いたときのページを、設定画面から変更できるホームページとすることができました。

ホームページと新しいタブページを独立に

さて、ここまで見てきた方法では、タブページとホームボタンを押して開くホームページが必ず一致してしまいます。
これでは改善として不十分であろうということで、以下では設定画面のURL入力フォームを2つに増やし、片方をホームページ・片方をタブページとするために書き換えた部分を見ていきます。
様々なファイルに渡るため、変更後のコードの変更部分のみここでは紹介します。

  • .../src/chrome/browser/resources/options/home_page_overlay.html
...

    <h2>ホームページ</h2> <!-- わかりやすさのための追記 --> 
    <div class="radio controlled-setting-with-label">
      <label>
        <input id="homepage-use-ntp" type="radio" name="homepage"
            pref="homepage_is_newtabpage" value="true"
            metric="Options_Homepage_IsNewTabPage" dialog-pref>
        <span>
          <span i18n-content="homePageUseNewTab"></span>
          <span class="controlled-setting-indicator"
              pref="homepage_is_newtabpage" value="true" dialog-pref></span>
        </span>
      </label>
    </div>
    <div class="radio controlled-setting-with-label">
      <label class="option-name">
        <input id="homepage-use-url" type="radio" name="homepage"
            pref="homepage_is_newtabpage" value="false"
            metric="Options_Homepage_IsNewTabPage" dialog-pref>
        <span>
          <span id="homepage-use-url-label" i18n-content="homePageUseURL">
          </span>
          <span class="controlled-setting-indicator"
              pref="homepage_is_newtabpage" value="false" dialog-pref></span>
        </span>
      </label>
      <input id="homepage-url-field" type="url" data-type="url"
          class="weakrtl favicon-cell stretch" pref="homepage"
          aria-labelledby="homepage-use-url-label"
          metric="Options_Homepage_URL" dialog-pref>
      </input>
      <span id="homepage-url-field-indicator"
          class="controlled-setting-indicator" pref="homepage"
          dialog-pref>
      </span>
    </div>
<!-- 追加部分ここから -->
    <h2>新しいタブ</h2>
    <div class="radio controlled-setting-with-label">
      <label>
        <input id="dedede-use-ntp" type="radio" name="dedede"
            pref="dedede_is_default" value="true"
            metric="Options_Dedede_IsDefault" dialog-pref>
        <span>
          <span>デフォルトの新しいタブ</span>
          <span class="controlled-setting-indicator"
              pref="dedede_is_default" value="true" dialog-pref></span>
        </span>
      </label>
    </div>
    <div class="radio controlled-setting-with-label">
      <label class="option-name">
        <input id="dedede-use-url" type="radio" name="dedede"
            pref="dedede_is_default" value="false"
            metric="Options_Dedede_IsDefault" dialog-pref>
        <span>
          <span id="dedede-use-url-label" i18n-content="homePageUseURL">
          </span>
          <span class="controlled-setting-indicator"
              pref="dedede_is_default" value="false" dialog-pref></span>
        </span>
      </label>
      <input id="dedede-url-field" type="url" data-type="url"
          class="weakrtl favicon-cell stretch" pref="dedede"
          aria-labelledby="homepage-use-url-label"
          metric="Options_Dedede_URL" dialog-pref>
      </input>
      <span id="dedede-url-field-indicator"
          class="controlled-setting-indicator" pref="dedede"
          dialog-pref>
      </span>
    </div>
  </div>
<!-- 追加部分ここまで -->

...
  • .../src/chrome/browser/resources/options/home_page_overlay.js
...

      var self = this;
      options.Preferences.getInstance().addEventListener(
          'homepage_is_newtabpage',
          this.handleHomepageIsNTPPrefChange.bind(this));

      // 追加部分ここから
      options.Preferences.getInstance().addEventListener(
          'dedede_is_default',
          this.handleDededeIsDefaultPrefChange.bind(this));
      // 追加部分ここまで

      var urlField = $('homepage-url-field');
      urlField.addEventListener('keydown', function(event) {
        // Don't auto-submit when the user selects something from the
        // auto-complete list.
        if (event.key == 'Enter' && !self.autocompleteList_.hidden)
          event.stopPropagation();
      });
      urlField.addEventListener('change', this.updateFavicon_.bind(this));

      // 追加部分ここから
      var dededeField = $('dedede-url-field');
      dededeField.addEventListener('keydown', function(event) {
        // Don't auto-submit when the user selects something from the
        // auto-complete list.
        if (event.key == 'Enter' && !self.autocompleteList_.hidden)
          event.stopPropagation();
      });
      dededeField.addEventListener('change', this.updateFavicon_.bind(this));
      // 追加部分ここまで

...

    handleHomepageIsNTPPrefChange: function(event) {
      var urlField = $('homepage-url-field');
      var urlFieldIndicator = $('homepage-url-field-indicator');
      urlField.setDisabled('homepage-is-ntp', event.value.value);
      urlFieldIndicator.readOnly = event.value.value;
    },

    // 追加部分ここから
    handleDededeIsDefaultPrefChange: function(event) {
      var urlField = $('dedede-url-field');
      var urlFieldIndicator = $('dedede-url-field-indicator');
      urlField.setDisabled('dedede-is-default', event.value.value);
      urlFieldIndicator.readOnly = event.value.value;
    },
    // 追加部分ここまで

    /**
     * Updates the background of the url field to show the favicon for the
     * URL that is currently typed in.
     * @private
     */
    updateFavicon_: function() {
      var urlField = $('homepage-url-field');
      urlField.style.backgroundImage = cr.icon.getFavicon(urlField.value);

    // 追加部分ここから
      var dededeField = $('dedede-url-field');
      dededeField.style.backgroundImage = cr.icon.getFavicon(dededeField.value);
    // 追加部分ここまで
    },

...

    handleConfirm: function() {
      // Strip whitespace.
      var urlField = $('homepage-url-field');
      var homePageValue = urlField.value.replace(/\s*/g, '');
      urlField.value = homePageValue;

    // 追加部分ここから
      var dededeField = $('dedede-url-field');
      var dededePageValue = dededeField.value.replace(/\s*/g, '');
      dededeField.value = dededePageValue;
    // 追加部分ここまで

      // Don't save an empty URL for the home page. If the user left the field
      // empty, switch to the New Tab page.
      if (!homePageValue)
        $('homepage-use-ntp').checked = true;

    // 追加部分ここから
      if (!dededePageValue)
        $('dedede-use-ntp').checked = true;
    // 追加部分ここまで

      SettingsDialog.prototype.handleConfirm.call(this);
    },

...
  • .../src/chrome/common/pref_names.h
...

extern const char kHomePageIsNewTabPage[];
extern const char kHomePage[];

// 追加部分ここから
extern const char kDedede[];
extern const char kDededeIsDefault[];
// 追加部分ここまで
 
#if defined(OS_WIN)
extern const char kLastProfileResetTimestamp[];

...
  • .../src/chrome/common/pref_names.cc
...

// This is the URL of the page to load when opening new tabs.
const char kHomePage[] = "homepage";

// 追加部分ここから
const char kDedede[] = "dedede";
const char kDededeIsDefault[] = "dedede_is_default";
// 追加部分ここまで
  
#if defined(OS_WIN)

...
  • .../src/chrome/browser/ui/browser_tabstrip.cc
...

void AddTabAt(Browser* browser, const GURL& url, int idx, bool foreground) {

  PrefService* prefs = browser->profile()->GetPrefs();
  GURL new_dedede(prefs->GetString(prefs::kDedede));

  // 追加部分ここから
  if(prefs->GetBoolean(prefs::kDededeIsDefault)){
    new_dedede = GURL(chrome::kChromeUINewTabURL);
  }
  // 追加部分ここまで

...
  • .../src/chrome/browser/ui/startup_browser_creator_impl.cc
...

void StartupBrowserCreatorImpl::AddStartupURLs(
    std::vector<GURL>* startup_urls) const {

  PrefService* prefs = profile_->GetPrefs();
  GURL new_dedede(prefs->GetString(prefs::kDedede));

  // 追加部分ここから
  if(prefs->GetBoolean(prefs::kDededeIsDefault)){
    new_dedede = GURL(chrome::kChromeUINewTabURL);
  }
  // 追加部分ここまで

...

ここまでひと通り書き換えて、終わった―!とビルドして実行するとコアダンします。(src/chrome/browser/ui/webui/options/core_options_handler.cc、376行目のNOTREACHED();において)
前後のコードを睨んだところ、どうやらpref_name(dedede, dedede_is_default)が認識されていないようです。
これについては、
www.chromium.org
を参考に、RegisterStringPref()という関数でpref_names.ccに宣言した変数を登録することで解決できます。
書き方や登録する場所を探すために"RegisterStringPref(prefs::kHomePage"でgrepすると、.../src/chrome/browser/profiles/profile_impl.ccで登録されていることが分かりました。

  • .../src/chrome/browser/profiles/profile_impl.cc
...

  registry->RegisterStringPref(prefs::kHomePage,
                               std::string(),
                               home_page_flags);
  
  // 追加部分ここから
  registry->RegisterStringPref(prefs::kDedede, std::string());
  registry->RegisterBooleanPref(prefs::kDededeIsDefault, true);
  // 追加部分ここまで
  
#if defined(ENABLE_PRINTING)

...

こんどこそ!とビルドし直すと、コアダンプすることはなくなり、一見所望の動作をしているかのように見えます。
が、ホームページの設定を"新しいタブを開く"にした状態でホームボタンを押すと設定した新しいタブのページではなくデフォルトの新しいタブ(url_constants.ccで定義されたもの)のページを開いてしまいました。
これを解決するためには、profile_impl.ccを更に弄る必要があります。

  • .../src/chrome/browser/profiles/profile_impl.cc
...

GURL ProfileImpl::GetHomePage() {
  // --homepage overrides any preferences.
  const base::CommandLine& command_line =
      *base::CommandLine::ForCurrentProcess();
  if (command_line.HasSwitch(switches::kHomePage)) {
    // TODO(evanm): clean up usage of DIR_CURRENT.
    //   http://code.google.com/p/chromium/issues/detail?id=60630
    // For now, allow this code to call getcwd().
    base::ThreadRestrictions::ScopedAllowIO allow_io;

    base::FilePath browser_directory;
    PathService::Get(base::DIR_CURRENT, &browser_directory);
    GURL home_page(url_formatter::FixupRelativeFile(
        browser_directory,
        command_line.GetSwitchValuePath(switches::kHomePage)));
    if (home_page.is_valid())
      return home_page;
  }

  // 追加部分ここから
  GURL new_dedede(GetPrefs()->GetString(prefs::kDedede));
  if(GetPrefs()->GetBoolean(prefs::kDededeIsDefault)){
    new_dedede = GURL(chrome::kChromeUINewTabURL);
  }
  // 追加部分ここまで

  if (GetPrefs()->GetBoolean(prefs::kHomePageIsNewTabPage))
    return new_dedede; // GURL(chrome::kChromeUINewTabURL);をnew_dededeに
  GURL home_page(url_formatter::FixupURL(
      GetPrefs()->GetString(prefs::kHomePage), std::string()));
  if (!home_page.is_valid())
    return new_dedede; // GURL(chrome::kChromeUINewTabURL);をnew_dededeに
  return home_page;
}

...

その後、src/chrome/browser/resources/options/browser_options.js(html)に加筆し表示を整えることもできます。

ここまで弄ってようやく、下画像のような設定ページにURLを入力し、ホームページとタブページを独立に設定することを実現できました!
f:id:iuias:20161028150827p:plain

おわりに

今回Chromiumを弄ってみて、ブラウザってこんなに大規模なソフトウェアだったんだな、と再確認させられました。
傍から見れば少しかもしれない機能変更をするのにこんなにもいろいろなファイルを書き換える必要があるなんて、気楽に構えていた実験開始時の自分を殴りたい気分です。
また、もう少し時間があれば、もっと(ネタ的な意味で)面白い追加機能を付けれたんじゃないかと、それだけが心残りです。

とはいえ、この実験で身につけたコードサーチの方法や、諦めない姿勢は非常に大切なものですし、終わってしまえばなかなか楽しい実験だったと思います。

この記事を読んでくださっている皆さんも、Chromium弄ってみましょう。自分好みのブラウザにしちゃいましょう!

Chromiumを手探った#3 - URLの受け渡しを解明しよう

#2はこちら

ファイルの中でURLが受け渡される流れを追います。

printfデバッグ

前回url_constant.ccというファイル内で定義されたkChromeUINewTabURLという変数が発見されましたが、const char型であり外部から書き換えるのが困難でした。
そのため、一先ず設定画面(chrome://settings)から、デザイン「ホームボタンを表示する」にチェックを入れたときに表示されるホームページ変更画面
f:id:iuias:20161028090753p:plain
に存在するURL入力ボックスを用いてURLを取得し、それをkChromeUINewTabURLの代わりに用いるという方針を立てました。

ここからはこのボックス内に入力されたURL(文字列)がどのように受け渡され、実際にホームページを開く際にどのように活用されているのかを確認していきます。

ファイル間でのデータの受け渡しを追うのはソースコードとにらめっこしていても難しいため、ここで所謂"printfデバッグ"を活用します。
数々の.ccファイルや.jsファイルの中身を(grepなどを用いて)確認する中で、.ccファイル内での関数RegisterMessageCallback()とCallJavascriptFunctionUnsafe()を発見しました。前者はどうやら.jsファイル内のchrome.send()関数で送られた関数の中身を受け取る関数であり、後者は.jsファイル内の関数を呼び出す関数のようです。
これらによって実際に送られてきたデータを調べるために、.ccファイルでは

LOG(INFO)<<hoge;

と気になる変数hogeの直後に入力すれば、その部分が実行されたときに標準出力にhogeの中身が出力されることを利用して、その変数が本当にURLかどうか、また今回確認したい動きに関係のある部分かどうかを判断することができます。また、.jsファイルでは

alert(hoge);

でアラートダイアログを発生させ、同様の確認を行うことができます。

ホームページURLの流れ

先ほどのホームページ変更画面のURLと、"homepage(home_page)"でのgrepの結果、.../src/chrome/browser/resources/options/home_page_overlay.js(html)なるファイルが発見されました。
ここから入力されたURLを前述のprintfデバッグも活用しながら追跡すると、次のようになりました。

  • .../src/chrome/browser/resources/options/home_page_overlay.js
...

    /**
     * Sets the 'show home button' and 'home page is new tab page' preferences.
     * (The home page url preference is set automatically by the SettingsDialog
     * code.)
     */
    handleConfirm: function() {
      // Strip whitespace.
      var urlField = $('homepage-url-field');
      var homePageValue = urlField.value.replace(/\s*/g, '');
      urlField.value = homePageValue;
      // Don't save an empty URL for the home page. If the user left the field
      // empty, switch to the New Tab page.
      if (!homePageValue)
        $('homepage-use-ntp').checked = true;
      SettingsDialog.prototype.handleConfirm.call(this);
    },

...

home_page_overlay.htmlで入力されたURLはhomepage-url-fieldに格納され、home_page_overlay.jsのこの部分でpref="homepage",type="url",valueがURLのurlFieldなる変数としてSettingsDialog.prototype.handleConfirm.call(this);によって同ディレクトリ内のsettings_dialog.jsに送られます。

  • .../src/chrome/browser/resources/options/settings_dialog.js
...

  SettingsDialog.prototype = {
    __proto__: Page.prototype,

    /** @override */
    initializePage: function() {
      this.okButton.onclick = this.handleConfirm.bind(this);
      this.cancelButton.onclick = this.handleCancel.bind(this);
    },


    /**
     * Handles the confirm button by saving the dialog preferences.
     */
    handleConfirm: function() {
      PageManager.closeOverlay();

      var prefs = Preferences.getInstance();
      var els = this.pageDiv.querySelectorAll('[dialog-pref]');
      for (var i = 0; i < els.length; i++) {
        if (els[i].pref)
          prefs.commitPref(els[i].pref, els[i].metric);
      }
    },

...

ここからprefs.commitPref(els[i].pref, els[i].metric);によって同ディレクトリ内のpreferences.jsに送られています。

  • .../src/chrome/browser/resources/options/preferences.js
...

  /**
   * Sets a string preference that represents a URL and signals its new value.
   * The value will be fixed to be a valid URL when it gets committed to Chrome.
   * @param {string} name Preference name.
   * @param {string} value New preference value.
   * @param {boolean} commit Whether to commit the change to Chrome.
   * @param {string} metric User metrics identifier.
   */
  Preferences.setURLPref = function(name, value, commit, metric) {
    if (!commit) {
      Preferences.getInstance().setPrefNoCommit_(name, 'url', String(value));
      return;
    }

    var argumentList = [name, String(value)];
    if (metric != undefined) argumentList.push(metric);
    chrome.send('setURLPref', argumentList);
  };

...

ここでchrome.sendが行われ、URLが.jsファイルから.ccファイルに受け渡されます。
URLは.../src/chrome/browser/ui/webui/options/core_options_handler.ccによって受け取られています。

  • .../src/chrome/browser/ui/webui/options/core_options_handler.cc
...

void CoreOptionsHandler::RegisterMessages() {
  registrar_.Init(Profile::FromWebUI(web_ui())->GetPrefs());
  local_state_registrar_.Init(g_browser_process->local_state());

  web_ui()->RegisterMessageCallback("coreOptionsInitialize",
      base::Bind(&CoreOptionsHandler::HandleInitialize,
                 base::Unretained(this)));

...

  web_ui()->RegisterMessageCallback("setURLPref",
      base::Bind(&CoreOptionsHandler::HandleSetURLPref,
                 base::Unretained(this)));
}

...

void CoreOptionsHandler::HandleSetURLPref(const base::ListValue* args) {
  HandleSetPref(args, TYPE_URL);
}

...

void CoreOptionsHandler::HandleSetPref(const base::ListValue* args,
                                       PrefType type) {
  DCHECK_GT(static_cast<int>(args->GetSize()), 1);

  std::string pref_name;
  if (!args->GetString(0, &pref_name))
    return;

  const base::Value* value;
  if (!args->Get(1, &value))
    return;

  std::unique_ptr<base::Value> temp_value;

  switch (type) {
    case TYPE_BOOLEAN:
      if (!value->IsType(base::Value::TYPE_BOOLEAN)) {
        NOTREACHED();
        return;
      }
      break;
    case TYPE_INTEGER: {
      // In JS all numbers are doubles.
      double double_value;
      if (!value->GetAsDouble(&double_value)) {
        NOTREACHED();
        return;
      }
      int int_value = static_cast<int>(double_value);
      temp_value.reset(new base::FundamentalValue(int_value));
      value = temp_value.get();
      break;
    }
    case TYPE_DOUBLE:
      if (!value->IsType(base::Value::TYPE_DOUBLE)) {
        NOTREACHED();
        return;
      }
      break;
    case TYPE_STRING:
      if (!value->IsType(base::Value::TYPE_STRING)) {
        NOTREACHED();
        return;
      }
      break;
    case TYPE_URL: {
      std::string original;
      if (!value->GetAsString(&original)) {
        NOTREACHED();
        return;
      }
      GURL fixed = url_formatter::FixupURL(original, std::string());
      temp_value.reset(new base::StringValue(fixed.spec()));
      value = temp_value.get();
      break;
    }
    case TYPE_LIST: {
      // In case we have a List pref we got a JSON string.
      std::string json_string;
      if (!value->GetAsString(&json_string)) {
        NOTREACHED();
        return;
      }
      temp_value = base::JSONReader::Read(json_string);
      value = temp_value.get();
      if (!value || !value->IsType(base::Value::TYPE_LIST)) {
        NOTREACHED();
        return;
      }
      break;
    }
    default:
      NOTREACHED();
  }

  std::string metric;
  if (args->GetSize() > 2 && !args->GetString(2, &metric))
    LOG(WARNING) << "Invalid metric parameter: " << pref_name;
  SetPref(pref_name, value, metric);
}

...

URLはweb_ui()->RegisterMessageCallback("setURLPref",base::Bind(&CoreOptionsHandler::HandleSetURLPref,base::Unretained(this)));で関数HandleSetURLPrefの引数として渡され、
HandleSetURTPref内、HandleSetPref(args, TYPE_URL);で関数HandleSetPrefの引数として渡され、
HandleSetPref内、SetPref(pref_name, value, metric);によって登録されます。(pref_nameはsrc/chrome/common/pref_names.ccにおいて登録されているconst char kHomePage[] = “homepage”;を参照していると思われます。)

ホームページ設定画面で入力したURLがどのようにホームページとして新たに登録されるか、ここまで辿ってようやく判明しましたので、次回これを利用して「新しいタブページを設定画面から変更できるようにする」方法について具体的に見ていくことになります。

次回予告

printfデバッグによってURLの流れを理解した一行はいよいよソースコードを書き換えることに挑戦する。正しいと思われた選択もいざコンパイルするとエラーの山。容赦なく近づいてくる締切。最期の力を振り絞ったエンターキーが導くのは希望か、それとも――次回、「終わりよければ全て良し」

#4はこちら

Chromiumを手探った#2 - コードに当たりを付けよう

#1はこちら

前回ビルドに成功したChromiumの大量のソースコードから、今回弄るべき対象となる部分を見つけるためのアレコレです。

 

GDBを使ったデバッグ(基本編)

タブを新しく作成した際にどのようなソースコードが読まれ、どのような動作をしているかを調べるためにはデバッガを使用します。今回はGDBEmacs上から扱いました。

M-x gud-gdb
gdb --fullname chrome

 を(事前にM-x shellで...src/out/Defaultまで移動した後に)実行すると、

f:id:iuias:20161018134355p:plain

シンボルの読み込みを延々と続け、2〜10分後…

f:id:iuias:20161018134359p:plain

ようやくシンボルの読み込みが終了し、デバッガ上でコードを探っていくことが出来ます。あとはmain関数やその他の怪しい関数にbreakpointを設定してnextやstepで辿っていく…というのが基本です。

問題点

何より重いです。シンボルを読み込むだけで数分何も出来ない時間が生まれてしまい、生きているのが辛くなります。

また、スレッドが多重に立ち上がり、所望のスレッドがどれなのか把握しづらいです。ブラウザは立ち上がった後、基本的に入力待ちをしているため、「とりあえずnextしてみる」に意味がありません。

さらに、「タブを開く」という動作のGUI的側面を管理する.jsファイルをこの方法では追うことが出来ず、あまり本質的ではありませんでした。

 

ソースコードをあたる

GDBを上手く動かすことができなかった僕たちには泥臭くコードを見ていくしか選択肢がありませんでした。

ファイル名(new_tab_ui.ccなど)をもとに幾つかのソースコードを見ていく中で、変数kChromeUINewTabURL[]を発見しました。これが怪しいと睨んだ僕たちはこの文字列がソースコード内に含まれるファイルを検索することになりました。

そこで主に用いたのがgrepコマンドです。

$ grep -rnI kChromeUINewTabURL

 を実行すると、.../src/chrome/common/url_constants.ccなるファイルに、

const char kChromeUINewTabURL[] = "chrome://new_tab";

 として定義されていることが判明しました。また、この文字列を直接変更することで「新しいタブ」ページを任意のページに変更することができました。

後はこの変更を、設定ページ(chrome://settings)から行えるようにすればいいだけだ!とこの時はまだ楽勝ムードが漂っていました。(遠い過去のお話のような気がします…)

これ以降も基本的にはgrepソースコードに当たりをつける作業を何度も行っています。また、変数名で検索することでコードとコードの繋がりや依存関係を推定することもできます。

 

僕たちはgrepが端末で行えて気楽だったのでこればかり使っていましたが、ChromiumにはCode Searchという優秀なコード検索ツールが用意されています。こちらを利用するのも非常に有効です。

 

GDBを使ったデバッグ(上級編)

ライブラリをGDBで辿る方法、シンボルを全て読み込む機能をオフにする方法などが実験内で紹介されました。非常にわかりやすい説明が成されていますので、ここではリンクを貼るに留めておきます。

自分がコンパイルしていないライブラリの中をGDBで追跡する

ちなみに、これが紹介されたタイミングでは僕たちはコードに目星をつけ切っていましたので、正直あんまりこの中に書かれていることは活用できてないです…。(やってはみましたが)

 

次回予告

grepという武器を入手し、意気揚々とソースコードの山に挑戦する一行だったが、立ち塞がるのは訳の分からない変数ばかり。前も後ろも何も見えない正しく五里霧中の状況で差し伸べられた一筋の光とは!?――次回「printfデバッグは神」

#3はこちら

Chromiumを手探った#1 - Chromiumをビルドしよう

はじめに

この記事は大学での実験「大規模ソフトウェアを手探る」において無謀にもChromiumを改良してやろうと挑戦した人間たちの辛く苦しい記録です。

Chromiumについて詳しくは公式ページ先生を参照いただけると幸いです。めんどくさい!という方は軽量版Google Chromeだと思っておいて貰えれば大丈夫です。

この実験では実験レポートとしてブログ記事を提出することが許されていますので、当記事はその体で作成されます。

後世Chromiumに手を出す人の助けに少しでもなれたら、との思いで僕達がこの実験で何をできたのかを何回かに分けてまとめていきます。間違いや疑問点などありましたらコメントで指摘して頂けると幸いです。

 

環境

OS: Ubuntu 14.04 LTS 64bit

 

なぜChromiumを選んだのか

特にこれといって扱いたいソフトウェアがなかった僕達は、実験ホームページ中の「どんなソフトをいじればいいかのヒント」を参考にすることにしました。

どうせ扱うなら真に”大規模”と言えるソフトウェアがいい、みんなが知ってるソフトウェアがいい…など様々な思いが渦巻き、結果としてGoogle ChromeOperaといった有名ブラウザのベースであり、オープンソースであるChromiumを扱うこととなりました。(実験中この判断を何度後悔したかわかりませんが…)

 

やろうと思ったこと

Chromiumでは(Google Chromeなどでもそうですが)新しくタブを立ち上げたときに開くページを設定することができません。開くのは無機質な「新しいタブ」というページです。ホームページを設定することはできますが、これを表示するためにはタブを立ち上げてからホームページボタンを押さなければならず、スマートではありませんでした。

そこで、新しいタブを開いたときに表示されるページを自分好みにカスタマイズできるようにすることが今回の実験のゴールとして設定されました。

 

はじめの一歩はビルドから

目標を設定して一番はじめに僕達が行ったのはChromiumのビルド作業です。

一般のオープンソースのソフトウェアでは「ソースコードを公式サイトなどからダウンロード→makeなどでコンパイル」を行うだけなのですが、Chromiumの場合はその手順がやや複雑です。下のページを参照しました。

www.chromium.org

depot_toolsの準備を行う

depot_toolsはChromiumソースコードのダウンロード・コンパイル作業に用いるツールで、自力でChromiumをビルドする際には必要になります。

$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

を端末において実行するとdepot_toolsというディレクトリが作成されます。続いて、

$ export PATH=$PATH:(depot_toolsまでのパス)

を実行すればdepot_toolsの準備は完了です。なお、(depot_toolsまでのパス)は、例えばホームディレクトリ内のexperimentというディレクトリにdepot_toolsが存在する場合、”~/experiment/depot_tools”となります。

Chromiumソースコードを取得する

depot_toolsの"fetch"というツールを用いてChromiumソースコードを取得します。

$ mkdir chromium
$ cd chromium
$ fetch --no-history chromium
$ cd src

 chromiumというディレクトリを作成し、そのディレクトリに移動し、そこでfetchを行います。fetchコマンドの--no-historyは通常不要(かつ巨大)なレポジトリ・履歴を取得しないオプションです。このオプションをつけることでコードの取得が20分程度で終了します。(つけないと倍以上の時間がかかり得ます)

ソースコードはsrcというディレクトリとして取得されますので、最後にそこへ移動します。以降の動作やコードの探索はすべてこのsrc内で行われます。

ソースコードコンパイルする

ソースコードが取得出来たので、必要なファイルをインストールし、コンパイルを実行します。

$ ./build/install-build-deps.sh
$ git rebase-update
$ gclient sync
$ gn gen out/Default
$ ninja -C out/Default chrome

 初め3行がファイルのインストールとアップデートです。gn genで作成したディレクトリ内に最終行(ninjaコマンド*1)でコンパイルしています。なおこのコンパイルには2時間程度かかるため、時間と気持ちに余裕のあるときに行いましょう。

最後にout/Default/chromeを実行すれば…

f:id:iuias:20161017162408p:plain

やりました。これにてビルド完了です。

 

次回予告

なんとかビルドを完了した僕達を待ち受けていたのは膨大なソースコードの山だった!どこを弄ればいいかの見当もつかず、とりあえずgdbChromiumを実行してみた僕達を非情な現実が襲う――次回「デバッグの度に10分待機させられたら作業にならないんですッ!」

#2はこちら

*1:depot_tools内にあるコンパイラ。apt-getで"ninja"は単体で取得出来ますが、こちらだと上手くいきません。