大規模ソフトウェア(Chromium)を手探る callbackハンドラを追加する・全体の感想

elechoくんのブログでChromeでWebUI Interfaceを追加するときの大まかな流れがわかったと思います。 Chromeに新しくWebUI Interfacesを追加するときの手順は公式に簡潔なガイドラインがあります。

www.chromium.org

Creating a Chrome WebUI interface is simple yet involves changing a number of files.

とあるように、ちょっとした画面(今回の場合は設定画面のセクション一つ)追加するにも割とたくさんのファイルを変更する必要があります… 知ってたらUIの大きな変更が必要な機能は追加しなかった気がする…...

このブログでは特にcallback Handkerを追加するときの手順にフォーカスして紹介したいと思います。 ユーザーがキーワードを設定したときにコールバック関数を読んでprefs::kRestrictedKeywordでストアされるキーワードの値を更新するためには、JavaScriptの世界(WebUI)からC++の世界(Chromeのコア)へ情報を送る必要があります。こういった場合、一番手っ取り早いのがcallback handllerを追加する方法です。 f:id:akaringo030402:20171025114952p:plain

AddRestrictedKeywordのためのcallbackハンドラを追加する

Chromeでcallbackハンドラを追加する手順は以下のようになります。

  1. src/chrome/browser/ui/webui/以下に新しくhandlerクラス(h, cppファイル)を追加する。
  2. MessageCallbackを登録し、JavaScriptで呼ばれる関数名と対応するC++プログラムでの関数名を決める
  3. 実際に呼ばれるC++プログラムの関数の動作を定義する
  4. 追加したハンドラを新しくビルドターゲットに追加する

1. 新しくRestrictedKeywordHandlerクラスを追加する。

まず新しくHandlerクラスをchrome/browser/ui/webui/settings/以下に作成します。 settings_restricted_keyword_pages_handler.hsettings_restricted_keyword_pages_handler.cppというファイルを追加しました。 まずは同じくwebui/settings/以下のsettings_startup_pages_handler.{h,cpp}を参考にそれっぽく書いて見ます。

  • settings_restricted_keyword_pages_handler.h
#ifndef CHROME_BROWSER_UI_WEBUI_SETTINGS_SETTINGS_RESTRICTED_KEYWORD_HANDLER_H_
#define CHROME_BROWSER_UI_WEBUI_SETTINGS_SETTINGS_RESTRICTED_KEYWORD_HANDLER_H_
#include "base/macros.h"
#include "chrome/browser/ui/webui/settings/custom_home_pages_table_model.h"
#include "chrome/browser/ui/webui/settings/settings_page_ui_handler.h"
#include "components/prefs/pref_change_registrar.h"
#include "ui/base/models/table_model_observer.h"

namespace base {
class ListValue;
}

namespace content {
class WebUI;
}

namespace settings {
// Chrome browser startup settings handler.
class RestrictedKeywordHandler : public SettingsPageUIHandler,
                            public ui::TableModelObserver {
 public:
  explicit RestrictedKeywordHandler(content::WebUI* webui);
  ~RestrictedKeywordHandler() override;

  // SettingsPageUIHandler:
  void RegisterMessages() override;
  void OnJavascriptAllowed() override;
  void OnJavascriptDisallowed() override;

  // ui::TableModelObserver:
  void OnModelChanged() override;
  void OnItemsChanged(int start, int length) override;
  void OnItemsAdded(int start, int length) override;
  void OnItemsRemoved(int start, int length) override;

 private:
  PrefChangeRegistrar pref_change_registrar_;
  CustomHomePagesTableModel restricted_custom_page_table_model_;
  DISALLOW_COPY_AND_ASSIGN(RestrictedKeywordHandler);
 };

} //namespace settings

#endif // CHROME_BROWSER_UI_WEBUI_SETTINGS_SETTINGS_RESTRICTED_KEYWORD_HANDLER_H_
  • settings_restricted_keyword_pages_handler.cpp
#include "chrome/browser/ui/webui/settings/settings_restricted_keyword_pages_handler.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "chrome/browser/prefs/session_startup_pref.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/webui/settings_utils.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/web_ui.h"
#include "url/gurl.h"

namespace settings {
RestrictedKeywordHandler::RestrictedKeywordHandler(content::WebUI* webui)
    : restricted_custom_page_table_model_(Profile::FromWebUI(webui)) {
}

RestrictedKeywordHandler::~RestrictedKeywordHandler() {
}

void RestrictedKeywordHandler::RegisterMessages() {
}

void RestrictedKeywordHandler::OnJavascriptAllowed() {
  restricted_custom_page_table_model_.SetObserver(this);
  PrefService* prefService = Profile::FromWebUI(web_ui())->GetPrefs();
  SessionStartupPref pref = SessionStartupPref::GetStartupPref(prefService);
  pref_change_registrar_.Init(prefService);
}

void RestrictedKeywordHandler::OnJavascriptDisallowed() {
  restricted_custom_page_table_model_.SetObserver(nullptr);
  pref_change_registrar_.RemoveAll();
}

void RestrictedKeywordHandler::OnModelChanged() {
  base::ListValue startup_pages;
  int page_count = restricted_custom_page_table_model_.RowCount();
  std::vector<GURL> urls = restricted_custom_page_table_model_.GetURLs();
  for (int i = 0; i < page_count; ++i) {
    std::unique_ptr<base::DictionaryValue> entry(new base::DictionaryValue());
    entry->SetString("title", restricted_custom_page_table_model_.GetText(i, 0));
    entry->SetString("url", urls[i].spec());
    entry->SetString("tooltip",
                     restricted_custom_page_table_model_.GetTooltip(i));
    entry->SetInteger("modelIndex", i);
    startup_pages.Append(std::move(entry));
  }
  FireWebUIListener("update-startup-pages", startup_pages);
}

void RestrictedKeywordHandler::OnItemsChanged(int start, int length) {
  OnModelChanged();
}

void RestrictedKeywordHandler::OnItemsAdded(int start, int length) {
  OnModelChanged();
}

void RestrictedKeywordHandler::OnItemsRemoved(int start, int length) {
  OnModelChanged();
}

} // namespace settings

2. callback関数を登録する。

C++で書かれた関数を名前と一緒にそれとなくregisterすることで、JavaScriptから指定した関数名で呼んであげればregisterした関数が呼ばれるようになります。また、その際にJavaScriptからcallbackを渡してあげれば、C++からそのcallbackを呼ぶことも可能です。
今回はprefs::kRestrictedKeywordを追加する関数setRestrictedKeyword、またユーザーが設定画面を開いた時に今の設定値を見えるようにJavaScriptに情報を渡すための関数getRestrictedKeywordを追加します。 メッセージコールバックの追加のためには、先ほど作成したsettings_restricted_keyword_pages_handler.cppRestrictedKeywordHandler::RegisterMessages()で登録を行います。

void RestrictedKeywordHandler::RegisterMessages() {
  if (Profile::FromWebUI(web_ui())->IsOffTheRecord())
    return;
  web_ui()->RegisterMessageCallback("addRestrictedKeyword",
                 base::Bind(&RestrictedKeywordHandler::HandleAddRestrictedKeyword,
                            base::Unretained(this)));
  web_ui()->RegisterMessageCallback("getRestrictedKeyword",
                            base::Bind(&RestrictedKeywordHandler::HandleGetRestrictedKeyword,
                                       base::Unretained(this)));
}

3. 実際に呼ばれる関数を宣言、定義する

登録した関数を実際に宣言、定義します。

  • settings_restricted_keyword_pages_handler.h
 public:
  explicit RestrictedKeywordHandler(content::WebUI* webui);
  ~RestrictedKeywordHandler() override;
  // 追加ここから
  void HandleAddRestrictedKeyword(const base::ListValue* args);
  void HandleGetRestrictedKeyword(const base::ListValue* args);
  std::string GetRestrictedKeyword(void);
  // 追加ここまで

cpp側で追加する関数を定義します。

  • settings_restricted_keyword_pages_handler.cpp
// 追加ここから
void RestrictedKeywordHandler::HandleAddRestrictedKeyword(const base::ListValue* args) {
  std::string pref_name;
  args->GetString(0, &pref_name);
  const base::Value* value;
  args->Get(1, &value);
  PrefService* prefs = Profile::FromWebUI(web_ui())->GetPrefs();
  prefs->SetString(prefs::kRestrictedKeyword, value->GetString());
}
void RestrictedKeywordHandler::HandleGetRestrictedKeyword(const base::ListValue* args) {
  CHECK_EQ(1U, args->GetSize());
  const base::Value* callback_id;
  CHECK(args->Get(0, &callback_id));
  AllowJavascript();
  ResolveJavascriptCallback(*callback_id, base::Value(GetRestrictedKeyword()));
}
std::string RestrictedKeywordHandler::GetRestrictedKeyword() {
  std::string RestrictedKeyword;
  PrefService* prefs = Profile::FromWebUI(web_ui())->GetPrefs();
  RestrictedKeyword = prefs->GetString(prefs::kRestrictedKeyword);
  return RestrictedKeyword;
}
// 追加ここまで

4. 追加したハンドラを新しくビルドターゲットに追加する

新しくsourcesを追加する時はgnファイルに追加したファイルへのパスを登録をする必要があります。 chrome/browser/ui/BUILD.gnファイルの他のsettings関連のハンドラが記述されている所の下に以下のように追加します。

      "webui/settings/settings_restricted_keyword_pages_handler.cc",
      "webui/settings/settings_restricted_keyword_pages_handler.h",

また、今回の変更の場合はこれだけでなく、SettingPagesHandlerとして新しく登録しないと正しくHandlerといて登録されないのでそちらも追加します。 chrome/browser/ui/webui/settings/md_settings_ui.ccを以下のように変更します。

// 変更ここから
#include "chrome/browser/ui/webui/settings/settings_restricted_keyword_pages_handler.h"
// 変更ここまで

MdSettingsUI::MdSettingsUI(content::WebUI* web_ui)
    : content::WebUIController(web_ui),
      WebContentsObserver(web_ui->GetWebContents()) {
  ...
  AddSettingsPageUIHandler(base::MakeUnique<OnStartupHandler>(profile));
  AddSettingsPageUIHandler(base::MakeUnique<PeopleHandler>(profile));
  AddSettingsPageUIHandler(base::MakeUnique<ProfileInfoHandler>(profile));
  AddSettingsPageUIHandler(base::MakeUnique<ProtocolHandlersHandler>());
  AddSettingsPageUIHandler(
      base::MakeUnique<SafeBrowsingHandler>(profile->GetPrefs()));
  AddSettingsPageUIHandler(base::MakeUnique<SearchEnginesHandler>(profile));
  AddSettingsPageUIHandler(base::MakeUnique<SiteSettingsHandler>(profile));
  AddSettingsPageUIHandler(base::MakeUnique<StartupPagesHandler>(web_ui));
  // 変更ここから
  AddSettingsPageUIHandler(base::MakeUnique<RestrictedKeywordHandler>(web_ui));
  // 変更ここまで
}

elechoくんの変更と合わせて、以上で設定画面から一つないし複数のキーワードを設定し、 検索ボックスにそれらの非表示キーワードを含む自動補完候補を表示させない機能の実装が完了しました!!

全体の感想

実装した機能自体はかなりネタ的に決めたものでした。

ですが手探るに当たって、Chromium Browserの大きな部品の一つであるOmniboxがどう実装されているのか、また自動補完のデータはどういうデータフローで供給されてきているのか、JavaScriptで記述されたWebUIとブラウザのコアの部分の間で情報がどうやり取りされているか、JavaScriptで呼ばれたcallback関数がC++側でどう実行されるかなど、多くのことを知ることができとても興味深かったです。

超大規模ソフトウェアならではの苦労も色々ありましたが(ビルドが終わらない、変更すべき箇所がどこかわからない等)、そういった経験もとても勉強になりました。

また私の適当な思いつきに付き合ってくれた同じチームのお二人にはとても感謝しています。本当に手探り状態でちゃんと期限内に終わるか不安だった時期もありましたが、二人と色々議論しながらこういう実装にしようここはこうしようと話すのがとても楽しかったです。

来年以降Chromiumを手探ってくれるEEICの後輩のみなさんは頑張ってください :)

大規模ソフトウェア(Chromium)を手探る user profileに設定を追加する

前回までのブログで任意のキーワードを含む検索結果に基づく自動補完を除くことができるようになりました。 今回はChromeのUI側で変更されたキーワードをuser profileで保存し、C++側のOmniboxの自動補完を行なっているクラスでこのデータを呼べるように変更します。
昨年(2016年度)の「大規模ソフトウェアを手探る」でChromiumを題材にしていた方がuser profileへの追加をした際のステップを丁寧に説明してくださっており、実装の際にとても参考になりました。

fizzy-yuya.hatenablog.com

Preferenceに表示したくないキーワードのデータを追加する

実際にキーワードフィルターをかける場合にはChromeの設定画面でユーザーのよって登録されたキーワードのデータを取ってきてフィルターをかける必要があります。ユーザーの登録したChromiumの設定はUser Profileに保存され、それぞれに固有のkey(pref_name)によって識別されます。

preferecesに任意の設定について追加したい場合には、

  1. PrefNamesクラスで新しく設定したいpreferenceのnameとデフォルト値を登録する。
  2. ProfileImplクラスのRegisterProfilePrefsd()という関数でこのpreferenceを登録する。

というステップでProfileにこの新しいpreferenceの値がストアされるようになり、PrefServiceを呼ぶことで、C++のプログラムから参照できるようになります。

詳しい説明は公式を参照してください。

まずchrome/common/pref_names.hに追加したいpreferenceの項目を追加します。今回はとりあえず全てのpreferenceの最後に新しくkRestrictedKeywordという項目を追加しました。

namespace prefs {
// Profile prefs. Please add Local State prefs below instead.
extern const char kChildAccountStatusKnown[];
...
// 変更ここから
extern const char kRestrictedKeyword[];
// 変更ここまで
}  // namespace prefs

これで宣言はできたので、cpp側でUI(JavaScript)側からこのpreferenceを参照する時に必要なpref_nameを登録します。ここに格納される値はデフォルト値ではなくてキーとして使われる識別子であることに注意してください。1

namespace prefs {
// *************** PROFILE PREFS ***************
// These are attached to the user profile
...
// 変更ここから
const char kRestrictedKeyword[] = "RestrictedKeyword";
// 変更ここまで
}  // namespace prefs

上のドキュメントにも書いてある通り、 pref_names.{h,cc}で定義された新たなpreferenceはchrome /browser/profiles/profile_impl.ccで登録される必要があります。

 // RegisterProfilePrefsd()というvoid関数でpreferenceの登録が行われている
void ProfileImpl::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterBooleanPref(prefs::kSavingBrowserHistoryDisabled, false);
...
 // 変更ここから
  registry->RegisterStringPref(prefs::kRestrictedKeyword, std::string());
// 変更ここまで
}

デフォルトの値を設定したい場合にはRegisterStringPref()の二つ目の引数で設定ができ、例えばregistry->RegisterStringPref(prefs::kRestrictedKeyword, "foobarhoge");とすればユーザーによって制限したキーワードが設定されるまで、"foobarhoge"がkRestrictedKeywordのデフォルト値として、foobarhogeを含む検索結果が表示されないようになります。

これでuser profileに新しいpreferenceを追加することができたので、いよいよC++側からpreferenceの情報を取得して自動補完候補にフィルターをかける機能を実装しようと思います。

AutocompleteControllerからUser profileを参照する

ユーザーのpreferencesを取得するためには、PrefServiceをservice clientから呼ぶ必要があります。 前回のブログの実装では実質的に自動補完の候補をソートし、重複を除く関数であるSortAndDedup()というAutocompleteResultクラスの関数の中でフィルター機能を実装しました。

ただこの実装方法を用いると、AutocompleteResultクラスがservice clientに順ずるものにアクセスできないため、この関数を呼び出しているAutocompleteController側から何らかの方法でkRestrictedKeywordの情報を渡す必要があります。 AutocompleteControllerはSortAndCull()という関数を経由して間接的にSortAndDedup()関数を読んでいます。

SortAndCull()関数がconst string kRestrictedKeywordという引数を追加で取るように変更することも考えたのですが、Call Hierarchyをチェックしたところ 15 occurrencesだった(つまり15箇所で呼ばれている)ため、 全部変更するのちょっとめんどくさいな...ということで、新たにSortAndCullWithKeyword()という関数をAutocompleteResultクラスに追加し、keywordが存在する場合にはこちらの関数が呼ばれるように変更しました。

まず、関数の宣言をヘッダーファイルに追加します。

  void SortAndCull(const AutocompleteInput& input,
                   TemplateURLService* template_url_service);
  // 変更ここから
  void SortAndCullWithKeyword(const AutocompleteInput& input,
                              TemplateURLService* template_url_service,
                              const std::string& keyword);
  // 変更ここまで

次に、新たに追加したSortAndCullWithKeyword()をcppファイルで定義します。基本的にはオリジナルのSortAndCull()関数を内部で呼びつつ、 keywordに従ってフィルターをかけています。フィルターをかけるに当たって、入力文字列とマッチした自動補完候補のベクターであるmatchesの先頭がdefault_matchとして保持されていなくてはいけないので、最後にdefault_matchを設定し直しています。

// SortAndCull()関数の次にSortAndCullWithKeyword()関数を追加する。
// 変更ここから
void AutocompleteResult::SortAndCullWithKeyword(
    const AutocompleteInput& input,
    TemplateURLService* template_url_service,
    const std::string& keyword) {
  // デフォルトのSortAndCullを呼ぶ
  SortAndCull(input, template_url_service);

  // キーワードフィルターをかける
  for (auto itr = matches_.begin(); itr != matches_.end();) {
    bool be_removed = itr->type == AutocompleteMatchType::HISTORY_TITLE ||
                      itr->type == AutocompleteMatchType::HISTORY_BODY ||
                      itr->type == AutocompleteMatchType::SEARCH_HISTORY ||
                      itr->type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
    if (be_removed && base::EqualsASCII(itr->contents, keyword) == true) {
      itr = matches_.erase(itr);
    } else {
      itr++;
    }
  }

  // default_match_を設定し直す
  default_match_ = matches_.begin();
}
// 変更ここまで

最後に、AutocompleteController側からSortAndCullWIth()の代わりにSortAndCullWithKeyword()を呼び、preferencesにストアされているkRestrictedKeywordを渡すように書き換えます。

void AutocompleteController::UpdateResult(
    bool regenerate_result,
    bool force_notify_default_match_changed) {
  TRACE_EVENT0("omnibox", "AutocompleteController::UpdateResult");
  const bool last_default_was_valid = result_.default_match() != result_.end();
  // The following three variables are only set and used if
  // |last_default_was_valid|.
  base::string16 last_default_fill_into_edit, last_default_keyword,
      last_default_associated_keyword;
  if (last_default_was_valid) {
    last_default_fill_into_edit = result_.default_match()->fill_into_edit;
    last_default_keyword = result_.default_match()->keyword;
    if (result_.default_match()->associated_keyword) {
      last_default_associated_keyword =
          result_.default_match()->associated_keyword->keyword;
    }
  }

  if (regenerate_result)
    result_.Reset();

  AutocompleteResult last_result;
  last_result.Swap(&result_);

  for (Providers::const_iterator i(providers_.begin());
       i != providers_.end(); ++i)
    result_.AppendMatches(input_, (*i)->matches());

  // 変更ここから 
  PrefService* prefs = provider_client_->GetPrefs();
  const std::string keyword = prefs->GetString(prefs::kRestrictedKeyword);
  // 非表示キーワードが設定されていた時はSortAndCullWithKeyword()を、
  // そうでない時は通常のSortAndCull()を呼ぶ。
  if (keyword) {
    result_.SortAndCullWithKeyword(input_, template_url_service_, keyword);
  } else {
    result_.SortAndCull(input_, template_url_service_);
  }
  // 変更ここまで

これでuser profile preferenceからユーザが設定したキーワードの情報を読み込み、Omniboxの自動補完候補に出したくないものを単語でフィルタリングできるようになりました。やったね。

ただこれだと、単一のキーワードに対してのみしかフィルタリング機能を使えないことになります。やっぱり複数のキーワードに対してフィルタリング機能を使いたいよね、ということになったので、キーワードをカンマで区切られた文字列で渡してもらい、それをtokenizeして複数のキーワードに対してフィルター機能を実装をするようにしました。

// 変更ここから
// tokenizeを行う関数を追加する
std::vector<std::string> TokenizesKeywordsStringToKeywordsVec(
    const std::string& keyword) {
  std::vector<std::string> tokens;
  std::size_t start = 0, end = 0;
  while ((end = keyword.find(',', start)) != std::string::npos) {
    tokens.push_back(keyword.substr(start, end - start));
    start = end + 1;
  }
  tokens.push_back(keyword.substr(start));
  return tokens;
}

void AutocompleteResult::SortAndCullWithKeyword(
    const AutocompleteInput& input,
    TemplateURLService* template_url_service,
    const std::string& keyword) {
  SortAndCull(input, template_url_service);

  std::vector<std::string> tokens =
      TokenizesKeywordsStringToKeywordsVec(keyword);
  for (auto i = tokens.begin(); i != tokens.end(); i++) {
    LOG(INFO) << *i << ' ';
    for (auto itr = matches_.begin(); itr != matches_.end();) {
      bool be_removed = itr->type == AutocompleteMatchType::HISTORY_TITLE ||
                        itr->type == AutocompleteMatchType::HISTORY_BODY ||
                        itr->type == AutocompleteMatchType::SEARCH_HISTORY ||
                        itr->type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
      if (be_removed && base::EqualsASCII(itr->contents, *i) == true) {
        itr = matches_.erase(itr);
      } else {
        itr++;
      }
    }
  }
  default_match_ = matches_.begin();
}
// 変更ここまで

また、この実装だとまだ「完全に一致キーワードと一致する検索履歴」しかフィルターがかけられず、また特に英語の場合は大文字小文字の区別がされてしまって 結果的にフィルターがうまくかからないケースもありえます。 そこで最後に以下のように変更を加えました。 またついでにmatches_から消去して良いか、matchTypeから判別する部分についても関数として切り出し、Tokenizeを行う関数、全てのアルファベットの入力文字列を小文字化する関数とともに無名名前空間に入れます。

// 変更ここまで
namespace { 

base::string16 NormalizeRestrictedKeyword(std::string s) {
  return base::ToLowerASCII(base::UTF8ToUTF16(std::move(s)));
}

std::vector<base::string16> TokenizesKeywordsStringToKeywordsVec(
    const std::string& keyword) {

std::vector<std::string> TokenizesKeywordsStringToKeywordsVec(
    const std::string& keyword) {
  std::vector<base::string16> tokens;
  std::size_t start = 0, end;
  while ((end = keyword.find(',', start)) != std::string::npos) {
  std::vector<base::string16> tokens;
  std::size_t start = 0, end;
  while ((end = keyword.find(',', start)) != std::string::npos) {
    tokens.push_back(
        NormalizeRestrictedKeyword(keyword.substr(start, end - start)));
    start = end + 1;
  }

  tokens.push_back(NormalizeRestrictedKeyword(keyword.substr(start)));
  return tokens;
}

    
bool IsRemovableTypeFromMatch(AutocompleteMatchType::Type type) {
  return type == AutocompleteMatchType::HISTORY_TITLE ||
         type == AutocompleteMatchType::HISTORY_BODY ||
         type == AutocompleteMatchType::SEARCH_HISTORY ||
         type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
}

}  // namespace

void AutocompleteResult::SortAndCullWithKeyword(
    const AutocompleteInput& input,
    TemplateURLService* template_url_service,
    const std::string& keyword) {
  std::vector<base::string16> restricted_keywords =
      TokenizesKeywordsStringToKeywordsVec(keyword);
  matches_.erase(
      std::remove_if(
          matches_.begin(), matches_.end(),
          [&restricted_keywords](const AutocompleteMatch& match) {
            bool removable = IsRemovableTypeFromMatch(match.type);
            if (removable) {
              base::string16 match_text = base::ToLowerASCII(match.contents);
              for (auto& restricted_keyword : restricted_keywords) {
                if (match_text.find(restricted_keyword) !=
                        base::string16::npos) {
                  return true;
                }
              }
            }
            return false;
          }),
      matches_.end());

  SortAndCull(input, template_url_service);
}
// 変更ここまで

ただこの変更を加えて実行してみると、キーワードフィルタリング自体はできるのですが、なぜか「キーワードと入力文字列が完全一致した時にcrashする」という現象が発生するようになります…なぜだ… まずは王道、crashした時のback traceをみてみます。

Check failed: input.text().empty() != default_match_->
    allowed_to_be_default_match (0 vs. 0)fill_into_edit=ポケモンgo, provider=Search, input=pokemongo
#0 0x7f6fac40677d base::debug::StackTrace::StackTrace()
#1 0x7f6fac404b4c base::debug::StackTrace::StackTrace()
#2 0x7f6fac4962ba logging::LogMessage::~LogMessage()
#3 0x558c51544aee AutocompleteResult::SortAndCull()
#4 0x558c51540e13 AutocompleteResult::CopyOldMatches()
#5 0x558c538379e0 AutocompleteController::UpdateResult()
#6 0x558c53836882 AutocompleteController::Start()

この時はOmniboxの入力している最中に落ちているのでinput.text().empty() == falseとなっています。
そのため、本来ならばdefault_match_-> allowed_to_be_default_match == trueにならなくてはいのですが
default_match_->allowed_to_be_default_match == falseになっているためにDCHECK()で落ちているようです。 この謎のdefault_match_allowed_to_be_default_matchについて、少し調べてみます。

default_match_とallowed_to_be_default_matchについて調べる

default_matchについて、AutocompleteResultクラスには特にコメント等で詳細な記述がないため、 default_matchはAutocompleteMatchというクラスのインスタンスになるため、このクラスを定義するautocomplete_match.cppファイルでallowed_to_be_default_matchについての めぼしい記述がないか探してみます。 するとallowed_to_be_default_matchについて以下のような記述を発見しました。

  // If false, the omnibox should prevent this match from being the
  // default match.  Providers should set this to true only if the
  // user's input, plus any inline autocompletion on this match, would
  // lead the user to expect a navigation to this match's destination.
  // For example, with input "foo", a search for "bar" or navigation
  // to "bar.com" should not set this flag; a navigation to "foo.com"
  // should only set this flag if ".com" will be inline autocompleted;
  // and a navigation to "foo/" (an intranet host) or search for "foo"
  // should set this flag.
  bool allowed_to_be_default_match;

allowed_to_be_default_matchは様々なProviderから返される自動補完候補に対して、それがdefault_matchになりうるかなり得ないかを表しているようです。 またallowed_to_be_default_match == trueになるのは、ユーザーの入力そのもの、もしくは入力に対しインライン自動補完を加えた候補に対してのみらしいですね。 そのためfooとユーザーが入力する時にbarbar.comdefault_matchにはなり得ないものの、 foo.comfoodefault_matchとなり得るようです。

つまり、先ほどのバグの原因は
「入力文字列と表示されたくないキーワードが完全に一致するもしくは入力文字列がキーワードを含む時、ユーザーの入力をそのまま候補としてサジェストするmatch(基本的にはこれが一番優先度の高いautocomplete_matchとしてdefault_match_にセットされている)がキーワードフィルタリングによって除かれる。その結果、優先度が二番目以降の候補がSortAndCull()が呼ばれた後default_match_にセットされるが、優先度が二番目以降の候補はallowed_to_be_default_match == trueとなる条件を満たしていないことが多いため2、クラッシュする」
ことだったようです。
元々「Search For...」と呼ばれる、ユーザーの文字列をそのまま検索候補として表示するものについてはbool IsRemovableTypeFromMatch(AutocompleteMatchType::Type type)関数でチェックをかけ、除去されないようにしていたはずでした。
しかし、おそらく同じキーワードの候補重複を排除する際に、他のProviderから返される別の候補で上書きされた結果除去されてしまったようです。

そこで入力文字列と一致する自動補完候補についてはmatches_から除去しないよう、以下のようにコードを修正しました。

// 変更ここから
namespace {
base::string16 NormalizeRestrictedKeyword(std::string s) {
  return base::ToLowerASCII(base::UTF8ToUTF16(std::move(s)));
}

std::vector<base::string16> TokenizesKeywordsStringToKeywordsVec(
    const std::string& keyword) {
  std::vector<base::string16> tokens;
  std::size_t start = 0, end;
  while ((end = keyword.find(',', start)) != std::string::npos) {
  std::vector<base::string16> tokens;
  std::size_t start = 0, end;
  while ((end = keyword.find(',', start)) != std::string::npos) {
    tokens.push_back(
        NormalizeRestrictedKeyword(keyword.substr(start, end - start)));
    start = end + 1;
  }

  tokens.push_back(NormalizeRestrictedKeyword(keyword.substr(start)));
  return tokens;
}

    
bool IsRemovableTypeFromMatch(AutocompleteMatchType::Type type) {
  return type == AutocompleteMatchType::HISTORY_TITLE ||
         type == AutocompleteMatchType::HISTORY_BODY ||
         type == AutocompleteMatchType::SEARCH_HISTORY ||
         type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
}

}  // namespace

void AutocompleteResult::SortAndCullWithKeyword(
    const AutocompleteInput& input,
    TemplateURLService* template_url_service,
    const std::string& keyword) {
  std::vector<base::string16> restricted_keywords =
      TokenizesKeywordsStringToKeywordsVec(keyword);
  const base::string16& input_text = input.text();
 // ラムダ式にOmniboxの入力ボックス内の文字列 input_textも一緒に渡す
  matches_.erase(
      std::remove_if(
          matches_.begin(), matches_.end(),
          [&restricted_keywords, &input_text](const AutocompleteMatch& match) {
            bool removable = IsRemovableTypeFromMatch(match.type);
            if (removable) {
              base::string16 match_text = base::ToLowerASCII(match.contents);
              for (auto& restricted_keyword : restricted_keywords) {
                // Omniboxの入力文字列と一致しないかつ表示したくないキーワードを含む検索候補を除くように書き換える。
                if (input_text != match_text &&
                    match_text.find(restricted_keyword) !=
                        base::string16::npos) {
                  return true;
                }
              }
            }
            return false;
          }),
      matches_.end());

  SortAndCull(input, template_url_service);
}
// 変更ここまで

これでユーザーの設定した複数キーワードに対しての検索履歴のフィルタリング機能の実装が完了しました。 あとはこのキーワードを設定する設定画面の追加及びJavaScriptの世界とC++の世界の間で情報をやり取りするためのHandlerクラスの追加等を行う必要があります。 これは次回以降のブログでまた説明します。

フィルタリング部分の変更まとめ

これまでに行なって変更を一覧にして載せておきます。

  • components/omnibox/browser/autocomplete_result.h
// 変更ここから
  void SortAndCullWithKeyword(const AutocompleteInput& input,
                              TemplateURLService* template_url_service,
                              const std::string& keyword);
// 変更ここまで
  • components/omnibox/browser/autocomplete_result.cpp
// 変更ここから
namespace {
base::string16 NormalizeRestrictedKeyword(std::string s) {
  return base::ToLowerASCII(base::UTF8ToUTF16(std::move(s)));
}

std::vector<base::string16> TokenizesKeywordsStringToKeywordsVec(
    const std::string& keyword) {
  std::vector<base::string16> tokens;
  std::size_t start = 0, end;
  while ((end = keyword.find(',', start)) != std::string::npos) {
  std::vector<base::string16> tokens;
  std::size_t start = 0, end;
  while ((end = keyword.find(',', start)) != std::string::npos) {
    tokens.push_back(
        NormalizeRestrictedKeyword(keyword.substr(start, end - start)));
    start = end + 1;
  }

  tokens.push_back(NormalizeRestrictedKeyword(keyword.substr(start)));
  return tokens;
}

    
bool IsRemovableTypeFromMatch(AutocompleteMatchType::Type type) {
  return type == AutocompleteMatchType::HISTORY_TITLE ||
         type == AutocompleteMatchType::HISTORY_BODY ||
         type == AutocompleteMatchType::SEARCH_HISTORY ||
         type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
}

}  // namespace

void AutocompleteResult::SortAndCullWithKeyword(
    const AutocompleteInput& input,
    TemplateURLService* template_url_service,
    const std::string& keyword) {
  std::vector<base::string16> restricted_keywords =
      TokenizesKeywordsStringToKeywordsVec(keyword);
  const base::string16& input_text = input.text();
  matches_.erase(
      std::remove_if(
          matches_.begin(), matches_.end(),
          [&restricted_keywords, &input_text](const AutocompleteMatch& match) {
            bool removable = IsRemovableTypeFromMatch(match.type);
            if (removable) {
              base::string16 match_text = base::ToLowerASCII(match.contents);
              for (auto& restricted_keyword : restricted_keywords) {
                if (input_text != match_text &&
                    match_text.find(restricted_keyword) !=
                        base::string16::npos) {
                  return true;
                }
              }
            }
            return false;
          }),
      matches_.end());

  SortAndCull(input, template_url_service);
}
// 変更ここまで
namespace prefs {
...
// 変更ここから
extern const char kRestrictedKeyword[];
// 変更ここまで
}  // namespace prefs
  • chrome/common/pref_names.cpp
namespace prefs {
...
// 変更ここから
const char kRestrictedKeyword[] = "RestrictedKeyword";
// 変更ここまで
}  // namespace prefs
  • chrome/browser/profiles/profile_impl.cc
void ProfileImpl::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
...
 // 変更ここから
  registry->RegisterStringPref(prefs::kRestrictedKeyword, std::string());
// 変更ここまで
}

  1. 頭の悪い人なので、最初ここにデフォルト値を設定しているんだと思っていました……

  2. (上の例で言うポケモンGOは意味的にはpokemongoと同値であっても、文字列としてはユーザーの入力と一致しないためdefault_matchとはなり得ない)

大規模ソフトウェアを手探る 検索ボックスにおける自動補完・サジェスチョンのデータの流れを追う

前回のブログでは、過去の検索ワードをDBから取ってきて自動補完を行うクラスに返すSearchProviderクラスを見つけてフィルター機能を実装してみたものの、実際の自動補完候補にフィルターしたはずのワードが残ってしまうと言う謎現象に遭遇しました。 今回ではこの問題を解消し、C++側の実装変更を完了するまでをまとめたいと思います。

データの流れを追う

SearchProviderクラスから流れてくるデータではデータのフィルタリングができているのに、Omniboxには補完候補として表れることから、SearchProvider以外のクラスからこのデータが供給されているのではないかと考え、まず自動補完のデータがどういった流れで受け渡されているのかを調べることにしました。

gdbブレークポイントを設定してみたり、エラーのバックトレース結果をみたり、地道にコードをサーチしたりして、Omniboxでユーザーが入力するたびに、AutocompleteControllerというクラスが各Providerからデータを集めて自動補完候補autocomplete_matchesを集め、このAutocompleteResultsクラスにこのマッチしたデータを渡すと、このAutocompleteResultsクラスで関連度に基づいてこのmatchesをソートし、重複を排除してControllerに渡し、AutocompleteControllerがこの結果に基づいてOmniboxのサジェスチョンを更新していることがわかりました。f:id:akaringo030402:20171101194051p:plain 実際には他にもHistoryQuickProviderなどいくつかのproviderからAutocompleteControllerに自動補完候補が提供されているのですが、図では省略しています。

前回の変更ではいくつかのProviderのうち、「検索時のクエリのうち、入力文字列にマッチするものを候補として返す」SearchProviderの結果から禁止したいキーワードとマッチするものを除くようにしていました。 図の中の赤いデータが表示したくないキーワードを含む検索履歴のデータを表しています。SearchProviderでこの特定のキーワードを含む検索履歴を除いても、全てのProviderから供給された結果を集めると、その中に消したい履歴が残り続けています。 f:id:akaringo030402:20171101194132p:plain

一旦、AutocompleteControllerの中で色々なProviderを集めている部分のコードを確認してみます。

AutocompleteController::AutocompleteController(...) {
  provider_types &= ~OmniboxFieldTrial::GetDisabledProviderTypes();

  if (provider_types & AutocompleteProvider::TYPE_BOOKMARK)
    providers_.push_back(new BookmarkProvider(provider_client_.get()));
  if (provider_types & AutocompleteProvider::TYPE_BUILTIN)
    providers_.push_back(new BuiltinProvider(provider_client_.get()));
  if (provider_types & AutocompleteProvider::TYPE_HISTORY_QUICK)
    providers_.push_back(new HistoryQuickProvider(provider_client_.get()));
  if (provider_types & AutocompleteProvider::TYPE_HISTORY_URL) {
    history_url_provider_ =
        new HistoryURLProvider(provider_client_.get(), this);
    providers_.push_back(history_url_provider_);
  }
  if (provider_types & AutocompleteProvider::TYPE_KEYWORD) {
    keyword_provider_ = new KeywordProvider(provider_client_.get(), this);
    providers_.push_back(keyword_provider_);
  }
  if (provider_types & AutocompleteProvider::TYPE_SEARCH) {
    search_provider_ = new SearchProvider(provider_client_.get(), this);
    providers_.push_back(search_provider_);
  }
  if (provider_types & AutocompleteProvider::TYPE_SHORTCUTS)
    providers_.push_back(new ShortcutsProvider(provider_client_.get()));
  if (provider_types & AutocompleteProvider::TYPE_ZERO_SUGGEST) {
    zero_suggest_provider_ = ZeroSuggestProvider::Create(
        provider_client_.get(), history_url_provider_, this);
    if (zero_suggest_provider_)
      providers_.push_back(zero_suggest_provider_);
  }
  ...
}

8種類のProviderからデータを集めていることがわかります。 おそらくこのうちのどれかのProviderがキーワードを含む検索履歴をSearchProviderとは別のProviderから供給されているのではないかと仮説を立て、それぞれのProviderについてどんな結果を返しているのか検証をしてみました。 Chrome Browserのいわゆるprintfデバッグをする際には、個人的にLOG(INFO) <<を使うことがおすすめです。 使用の仕方はC++のstd::coutとだいたい同じです。

真犯人が判明

N時間に渡るデバッグの結果、フィルターをかけたはずの検索ワードを返しているProviderが判明しました。 真犯人は ShortcutProvider でした。

工工工エエエエエエェェェェェェ(゚Д゚)ェェェェェェエエエエエエ工工工

なんと一番無害そうというか関係なさそうなProviderが、実は禁止キーワード文字列をAutocompleteProviderに返していました…... ShortcutProviderクラスのコードをみてみると、

// Provider of recently autocompleted links. Provides autocomplete suggestions
// from previously selected suggestions. The more often a user selects a
// suggestion for a given search term the higher will be that suggestion's
// ranking for future uses of that search term.
class ShortcutsProvider : public AutocompleteProvider,
                          public ShortcutsBackend::ShortcutsBackendObserver {...}

つまり、SearchProviderでキーワードと一致するサジェスチョンを除いても、ShortcutProviderが最近の検索履歴からキーワードと一致しているサジェスチョンをAutocompleteProviderに流していたということです…… f:id:akaringo030402:20171101194213p:plain

え、RecentlySearchedHistoryProviderとかにすればよくね????ShortcutProviderて命名から想像できなくね????ネーミングおかしくね?????

ということで、原因はこのShortcutProviderから、別のDBから最近の検索履歴データが供給されていたというものでした。 チームでの話し合いの結果、SearchProvider、ShortcutProviderに対して個々にキーワードフィルターをかけるより、全てのProviderの結果をまとめ、重複を除いてソートした後にフィルターをかけた方比較回数を減らせるのではないかと考え、 SortAndDedupMatches()というAutocompleteResultクラスの関数でフィルターをかけることにしました。 f:id:akaringo030402:20171101194249p:plain 変更後のコードは以下のようになります。

void AutocompleteResult::SortAndDedupMatches(
    metrics::OmniboxEventProto::PageClassification page_classification,
    ACMatches* matches) {
  // Sort matches such that duplicate matches are consecutive.
  matches->sort(DestinationSort<AutocompleteMatch>(page_classification));
  // Set duplicate_matches for the first match before erasing duplicate
  // matches.
  for (ACMatches::iterator i(matches->begin()); i != matches->end(); ++i) {
    for (auto j = std::next(i);
         j != matches->end() && AutocompleteMatch::DestinationsEqual(*i, *j);
         ++j) {
      AutocompleteMatch& dup_match(*j);
      i->duplicate_matches.insert(i->duplicate_matches.end(),
                                  dup_match.duplicate_matches.begin(),
                                  dup_match.duplicate_matches.end());
      dup_match.duplicate_matches.clear();
      i->duplicate_matches.push_back(dup_match);
    }
  }
  // Erase duplicate matches.
  matches->unique(AutocompleteMatch::DestinationsEqual);

// 変更ここから
  for (auto itr=matches->begin(); itr!=matches->end();){
     bool be_removed = itr->type == AutocompleteMatchType::HISTORY_TITLE ||
            itr->type == AutocompleteMatchType::HISTORY_BODY ||
            itr->type ==  AutocompleteMatchType::SEARCH_HISTORY ||
            itr->type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL; 
     if (be_removed && base::EqualsASCII(itr->contents, "PokemonGo") == true){
        itr = matches->erase(itr);
     } else {
    itr++;
     }
  }
// 変更ここまで
}

単に非表示キーワードを含む全てのサジェスチョンをeraceしてしまうと、普段使いのときに特定のキーワードに関連した検索をしようとしても補完されないので、不便になってしまいますよね。 そこで今回はbe_removedというbool変数を導入して、補完のタイプがSearchHistory及びそれを自動補完したもの(過去のユーザーの検索ワードに基づいたサジェスチョン)、もしくはHistoryBody(過去に閲覧したURL履歴の本文)に 非表示キーワードが含まれる時のみ、 自動補完の候補から除くことにしました。

これでOmniboxのキーワードフィルタリング機能が完成しました!!
ただこれだとC++ソースコードでキーワードをハードコーディングしているだけなので、次回以降、User Profileを利用してユーザーの登録した非表示キーワードを読み込み、 フィルターをかける部分の変更や複数キーワードの登録などについてまとめます。

大規模ソフトウェアを手探る Chromeのソースコードとドキュメントをひたすら漁る

Chromeの膨大なソースコードから変更するクラスを見つける

Chromeをビルドできたところでいよいよ実際にコードの変更を行なっていきます。 今回私たちが追加しようとした機能はChromeの上部にある検索ボックスに表示される検索履歴に単語フィルターをかける(つまり特定の単語が含まれる検索履歴は表示しない)というものでした。 そこで私たちの考えたアイディアとしては「入力に従って検索ボックス情報を更新しているモジュール上で、特定の単語が含まれる検索履歴は更新するデータに追加しないようにする」もしくは「特定の単語が含まれる検索ワードは検索履歴のデータに保存されないように」しようと言うものでした。 最初は検索ボックス内でヒストリー(過去の検索履歴)を読み出してautocompleteの候補を作成しているクラスでちょっとif文追加するだけの簡単なお仕事かな〜と楽観的に考えていたのですが、現実はそこまで甘くはありませんでした…

いきなりChrome code searchの限界に直面する

まず最初は前回紹介したChrome Code Searchで検索をかけます。とりあえず「history search」で検索をかけてみる…

f:id:akaringo030402:20171008085025p:plain
Chrome code searchでの「history search」検索結果
4400件wwwwwww

これを全部見ていくのはしんどそう… とりあえずいきなり適当な単語でcode searchはあんまりよくないと言うことに気がつき(それはそう)、まずどういったモジュールがあの検索ボックスの自動補完に対応しているのか、Chromeのドキュメントから調べていくことにしました。
Chromium (Chrome Browser, Chrome OS)の開発者向けドキュメント (design doc) は公式ホームペーシ、The Chromium ProjectのFor Developers>Design Documentsから検索できます。
Google検索していたらChromeの上部にある検索ボックスはomniboxと呼ばれることがわかったので、このomnibox関連のdesign docを調べていくことにしました。

omnibox関連のドキュメントをとりあえずぐぐりまくる

公式のDesign Docsのうち、まず注目したのはOmnibox: History Providerについてのドキュメントです。

One of the autocomplete providers for the omnibox (the HistoryQuickProvider, HQP for short) serves up autocomplete candidates from the profile's history database. ふむふむ…autocomplete providersなるものが自動補完候補をユーザープロファイルのDBから取得していることがわかります。 またこのautocomplete providersにはいくつか種類があることも読み取れます。

次はDesign Docではないのですが、一般向けのOmniboxの説明をしたドキュメントを見つけました。 これによると、Omniboxの自動補完は以下のタイプに分類できるようです。

  • Search for... This searches for the typed query - this will be the topmost and default item if the input looks like a search string.

  • URL If the input (after auto-complete, if applicable) looks like a URL, this will repeat the user's input and be the default, top-most item.

  • Previous URLs If the input matches a previous URL, those URLs will be shown here.

  • Nav Suggest Uses the user's default search provider to suggest URLs based on the user's input.

  • Search Suggest Uses the user's default search provider to suggest search terms based on the user's input.

  • History Results Shows the number of entries in the user's history that match the given input - selecting this item will take the user to the history results page for their given search term.

これを見た感じ検索ワードの履歴を返しているのはHistory Resultsみたいですね…History Resultに関連してそうなSearchProviderクラスが見つかりました。

とりあえずSearchProviderクラスを変更してみる

SearchHistoryクラスを見ていくと、ScoreHistoryResultsという関数が、過去の検索履歴のうち入力文字列とマッチするHistoryResultsついて、関連度スコアrelevanceをつけてそれに基づいてソートしていることわかりました。ScoreHistoryResults自体はSearchProvider::SearchHistoryResultHelperという別のヘルパー関数を呼んで、実際に自動補完候補のスコア付などを行なっています。

とりあえず、この関数の中でHistoryResultsから、検索文字列があるキーワードと一致する時にscored_resutsに追加しないようにするように書き換えて見ました。

SearchProvider::ScoreHistoryResultsHelper(const HistoryResults& results,
                                          bool base_prevent_inline_autocomplete,
                                          bool input_multiple_words,
                                          const base::string16& input_text,
                                          bool is_keyword) {
  ...
  // resultsは過去の検索履歴のうち入力文字列とマッチするものを全て取り出している。
  for (HistoryResults::const_iterator i(results.begin()); i != results.end();
       ++i) {
   ...
    SearchSuggestionParser::SuggestResult history_suggestion(
        trimmed_suggestion, AutocompleteMatchType::SEARCH_HISTORY, 0,
        trimmed_suggestion, base::string16(), base::string16(),
        base::string16(), base::string16(), nullptr, std::string(),
        std::string(), is_keyword, relevance, false, false, trimmed_input);
    history_suggestion.set_received_after_last_keystroke(false);
    // scored_resultsにキーワードとマッチしていない時のみ追加する。
    // 変更ここから
    if (base::EqualsASCII(history_suggestions.suggestion(), “harry”)  == false){
        scored_results.insert(insertion_position, history_suggestion);
    }
    // 変更ここまで
  }
  ...
}

よしこれでひとまずフィルター機能は実装できたはず…...と思うも、search_providerの返す結果では"harry"が消えているのに表示される結果には"harry"が残っていると言う謎の事態に......

次回のブログではSearchProviderから供給されるデータに対してフィルターをかけるだけではなぜダメだったのか、OmniboxのAutocompleteのデータフローを追いながら原因を解明していきます。

大規模ソフトウェア(Chromium)を手探る 導入・ビルド編

このブログは電気系3年生後期実験の大規模ソフトウェアを手探るの最終報告ブログです。

「大規模ソフトウェアを手探る」とは?

東大工学部電気系3年生は3~6個のテーマの実験を行うことが必修となっており、「大規模ソフトウェアを手探る」とは田浦健二郎先生による「演習レベルの小さなプログラムが作れること」と,「実用規模のプログラムが作れること」のギャップを埋める (ための知識と経験を得ることを目的に、1〜数百万行のオープンソースソフトウェアをソースからビルドし新しい機能を加えたりします。
今回私たちのチームでは、みんなが使う大規模なものをやりたいという気持ちと、個人的にChromeのBlink周りの開発をしたことがあったのでChromiumをテーマに選びました。

そもそもChromiumとは

GoogleのブラウザであるChromeを使っている方も多いのではないでしょうか。ChromiumとはオープンソースソフトウェアとしてChromeを見たときの名称です。

Chromiumクロミウム)はオープンソースのウェブブラウザのプロジェクト名で、Google Chromeはこのソースコードを引き抜いて開発されたものである。 -- Chromium - Wikipedia

ちなみにChromiumがどのくらい大規模かというと、 英語圏の質問投稿サイトQuoraの質問How many lines of code is Google Chrome?によると、2012年の時点で400~500万行のコードが存在しているみたいです。

4,490,488 lines of code, 5,448,668 lines with comments included, spread over 21,367 unique files.

現在の正確なChromiumソースコード行数については見つけられていないのですが、この数字だけでも私たちが普段使っているブラウザがどれほど大きく複雑なものかわかります。

このブログ記事について

この「大規模ソフトウェアを手探る」実験では、一般的な実験レポートの代わりにこういったブログ形式でのレポートの提出が推奨さえています。
私たちもChromiumという(超)大規模ソフトウェアをて探るにあたって、かなり昨年の方の丁寧なブログレポートが参考になったため、できるだけ詳細に どういった作業を行っていったのか、何に途中つまづいたのかをまとめています。
Chromiumのソースをいじってみたいという方以外だともしかしたら少し冗長かもしれませんが、今後「大規模ソフトウェアを手探る」でChromiumをやりたい方、 もしくは趣味でChromiumをゴニョゴニョしたい方の参考になれば嬉しいです。

今回の実験で実装しようと思った機能

今回の実装しようと思った機能はChromeの上部にでる検索ボックス上に入力に応じて現れる過去の履歴やサーチエンジンの検索サジェスチョンから、指定したキーワードを含む過去の検索履歴を表示しないようにする機能です。 この検索ボックスのことはOmniboxというChromium BrowserのComponentです。

スライドを投影している時やペアプロの時にChrome検索を開いた時に表示したくないような検索履歴がサジェストされることがあるという話を聞いた(見た)のと、 なんとなく面白そうだからというこれまた雑な理由で決めたのですが、結果的にBrowserのコアの部分からフロントエンドまで、またC++からJavaScriptの世界でどうやりとりがされているか等を知ることができ、 とても勉強になりました。

大まかな作業スケジュールとブログ記事リスト

以下が大まかな作業スケジュールになります。できるだけ来年以降大規模ソフトウェアを手探る人に参考になるように細かく書いたら長くなったのでブログ記事を分割しました。

まずはビルドから

とはいうもののまずビルドができないと話になりません。 この記事では開発を始める前のChromiumソースコードを引っ張ってきて手元のPCでビルドするまでをまとめたいと思います。 今回のチームメンバーはみんなLinux(Ubuntu)を使用していたので、ChromiumのLinux向けコンパイル、ビルドのイントラクションを参考にしました。 基本的には順に従って行けばいいのですが、いくつかハマりポイントがあったのでそれも含めて追っていきます。

depot_toolsをインストールする

depot_toolsはソースコードのチェックアウトや依存パッケージのダウンロードなどの、Chromiumのビルドやコントリビュートに必要な機能を提供してくれます。

$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
$ export PATH="$PATH:/path/to/depot_tools"

Chromiumソースコードを取ってくる

Chromiumソースコードを取ってきます。まず適当な場所にChromium作業用のディレクトリを作りましょう。 今回は~/crというディレクトリ以下ににChromiumソースを落としてくることを想定しています。

$ mkdir ~/cr && cd cr

次に、先ほどインストールしてきたdepot_toolsのfetchというツールを使ってコードと依存ライブラリをチェックアウトします(ローカルに持ってきます)。

$ fetch --nohooks--no-history chromium

基本的には--no-historyオプションをつけることをオススメします。--no-historyをつけないと、レポジトリの全ての履歴を取ってくるので時間もストレージも倍くらいかかります… --no-historyをつけた場合はだいたい30分程度でコードがチェックアウトできます。

hookを走らせる

フェッチが完了したらChromium特有のhooksを走らせて必要な追加バイナリをインストールします。 ただUbuntuの場合、この前にフェッチしてきたsrc/以下でbuild/install-build-deps.shを走らせる必要があります。これ公式だとあまり大きく書いてないので見落としがちですが、先にこちらを走らせないと「dependenciesがないよ!」って怒られます。

$ cd src
$ build/install-build-deps.sh
$ gclient runhooks

ビルドのためのセットアップをする

必要なダウンロードが終わりました…!いよいよビルドしていきましょう。まず、セットアップをします。

$ gn gen out/Default

次にいよいよninjaでコンパイルし、実行ファイルをRunします。

$ ninja -C out/Default chrome
$ out/Default/chrome

学科の8コアのマシンでだいたい5時間程度で完了しました…さすが大規模ソフトウェア... 電気系3年生は実験の前にビルドまで終らせてから来ような、初回にやることがなくなるからな ここまででChromeのビルドが完了しました。ここまでは割とトラブルなくできると思います。 次回からは実際に検索フィルター機能を追加するための手順を順にまとめていきます。

Chrome開発に当たって便利なもの

  • Chrome code search
    Chromiumのコードの検索ができる。個人的にはhistory, xsearch(referenceやcall hierarchy)がすごい便利だと思いました。
  • The Chromium Project For Developers Design Documents
    Chromeの様々な機能や全体のアーキテクチャのdesign documents(設計書、機能書)が集まっています。500万行以上あるコードから自分の変更したい機能に関わる部分をcode searchのみで発掘するのはなかなか厳しいので、こういったdesign documentsを見て関連するモジュールの実装等を参考にするといいかもしれません。