大規模ソフトウェア(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の後輩のみなさんは頑張ってください :)