概要
Mojo C++ バインディング API は、C++ システム API を拡張し、メッセージパイプ上での通信をより自然な形で行えるようにするプリミティブを提供します。Mojom IDL とバインディングジェネレータが生成するコードと組み合わせることで、プロセス内・プロセス間を問わず、インターフェースのクライアントと実装を容易に接続できます。
本ドキュメントでは、コード例を用いてバインディング API の利用方法を詳細に解説します。詳細な API リファレンスについては、//mojo/public/cpp/bindings 以下のヘッダーファイルを参照してください。
はじめに
Mojom IDL ファイルをバインディングジェネレータで処理すると、入力となる .mojom ファイルに基づいた名前の .h ファイルと .cc ファイルが生成されます。以下のような //services/db/public/mojom/db.mojom ファイルを作成したとします。
module db.mojom;
interface Table {
AddRow(int32 key, string data);
};
interface Database {
CreateTable(Table& table);
};
そして、//services/db/public/mojom/BUILD.gn でバインディングを生成する GN ターゲットを定義します。
import("//mojo/public/tools/bindings/mojom.gni")
mojom("mojom") {
sources = [
"db.mojom",
]
}
このインターフェースを必要とするターゲットは、以下のように依存関係を記述してください。
deps += [ '//services/db/public/mojom' ]
ビルドを実行すると、
ninja -C out/r services/db/public/mojom
C++ バインディングに関連する以下のようなソースファイルが生成されます。
out/gen/services/db/public/mojom/db.mojom.cc
out/gen/services/db/public/mojom/db.mojom.h
生成されたヘッダーをインクルードすることで、定義を利用できます。
#include "services/business/public/mojom/factory.mojom.h"
class TableImpl : public db::mojom::Table {
// ...
};
以降では、Mojom IDL から生成される C++ 向けの様々な定義と、それらをメッセージパイプを介した通信に効果的に利用する方法について説明します。
注意: Blink コード内で C++ バインディングを使用する場合、特別な制約が適用され、異なる生成ヘッダーを使用する必要があります。詳細は Blink タイプマッピングを参照してください。
インターフェース
Mojom IDL のインターフェース定義は、生成されるヘッダーファイル内で C++ の(純粋仮想)クラス定義に変換されます。インターフェース上の各リクエストメッセージに対して、1つのメソッドシグネチャが生成されます。メッセージのシリアライズやデシリアライズのコードも内部的に生成されますが、バインディング利用者は意識する必要はありません。
基本的な使い方
簡単なロギングインターフェースを定義する //sample/logger.mojom を考えます。
module sample.mojom;
interface Logger {
Log(string message);
};
これをバインディングジェネレータで処理すると、logging.mojom.h に以下のような定義が生成されます(詳細は省略)。
namespace sample {
namespace mojom {
class Logger {
virtual ~Logger() {}
virtual void Log(const std::string& message) = 0;
};
using LoggerPtr = mojo::InterfacePtr<Logger>;
using LoggerRequest = mojo::InterfaceRequest<Logger>;
} // namespace mojom
} // namespace sample
最後の型エイリアスに注目してください。
InterfacePtr と InterfaceRequest
LoggerPtr と LoggerRequest は、C++ バインディングライブラリの最も基本的なテンプレート型である InterfacePtr<T> と InterfaceRequest<T> のエイリアスです。
Mojo バインディングライブラリにおいて、これらは厳密に型付けされたメッセージパイプの端点です。InterfacePtr<T> がメッセージパイプ端点にバインドされている場合、それをデリファレンスして不透明な T インターフェースのメソッドを呼び出すことができます。これらの呼び出しは、引数を(生成されたコードを使用して)即座にシリアライズし、対応するメッセージをパイプに書き込みます。
InterfaceRequest<T> は、InterfacePtr<T> のパイプの反対側(受信側)を、実装にバインドされるまで保持するための型付きコンテナです。InterfaceRequest<T> 自体はパイプ端点を保持し、コンパイル時に有用な型情報を伝達する以外には何もしません。
インターフェースパイプの作成
パイプを手動で作成し、各端点を型付きオブジェクトでラップする方法もあります。
#include "sample/logger.mojom.h"
mojo::MessagePipe pipe;
sample::mojom::LoggerPtr logger(
sample::mojom::LoggerPtrInfo(std::move(pipe.handle0), 0));
sample::mojom::LoggerRequest request(std::move(pipe.handle1));
これは冗長ですが、C++ バインディングライブラリはより便利な方法として MakeRequest 関数を提供します。
sample::mojom::LoggerPtr logger;
auto request = mojo::MakeRequest(&logger);
この2番目のスニペットは最初のものと同等です。
注意: 最初の例で
LoggerPtrInfo型を使用していますが、これはmojo::InterfacePtrInfo<Logger>のエイリアスです。これはInterfaceRequest<T>と同様にパイプハンドルを保持するだけで、メッセージの読み書きはできません。これらの型はシーケンス間を自由に移動できますが、バインドされたInterfacePtr<T>は単一のシーケンスにバインドされます。
InterfacePtr<T>はPassInterface()メソッドを呼び出すことでバインドを解除でき、新しいInterfacePtrInfo<T>が返ります。逆に、InterfacePtr<T>はInterfacePtrInfo<T>をバインド(所有権を取得)することで、パイプ上でインターフェース呼び出しを行えるようになります。
InterfacePtr<T>がシーケンスにバインドされる性質は、メッセージ応答や接続エラー通知の安全なディスパッチをサポートするために必要です。
LoggerPtr がバインドされると、すぐに Logger インターフェースのメソッド呼び出しを開始でき、呼び出しは即座にメッセージをパイプに書き込みます。これらのメッセージは、誰かがパイプの受信側にバインドして読み取りを開始するまでキューに残ります。
logger->Log("Hello!");
これにより、Log メッセージがパイプに書き込まれます。
しかし、InterfaceRequest 自体は何もしないため、このメッセージはパイプ上に残り続けます。パイプのもう一方の端からメッセージを読み取り、ディスパッチする必要があります。つまり、インターフェースリクエストをバインドする必要があります。
インターフェースリクエストのバインド
メッセージパイプの受信側をバインドするためのヘルパークラスはいくつかあります。最も基本的なものは mojo::Binding<T> です。mojo::Binding<T> は、T の実装を、バインドされたメッセージパイプ端点(mojo::InterfaceRequest<T> 経由)と橋渡しし、読み取り可能かどうかを継続的に監視します。
バインドされたパイプが読み取り可能になるたびに、Binding はタスクをスケジュールして、利用可能なすべてのメッセージを読み取り、デシリアライズし(生成されたコードを使用)、バインドされた T の実装にディスパッチします。以下は Logger インターフェースの実装例です。実装自体が mojo::Binding を所有していることに注意してください。これは一般的なパターンです。バインドされた実装は、それをバインドする mojo::Binding よりも長生きする必要があるためです。
#include "base/logging.h"
#include "base/macros.h"
#include "sample/logger.mojom.h"
class LoggerImpl : public sample::mojom::Logger {
public:
// 注意: クライアントごとに1つのインスタンスを持つインターフェース実装では、
// コンストラクタで InterfaceRequest を受け取るのが一般的です。
explicit LoggerImpl(sample::mojom::LoggerRequest request)
: binding_(this, std::move(request)) {}
~LoggerImpl() override {}
// sample::mojom::Logger:
void Log(const std::string& message) override {
LOG(ERROR) << "[Logger] " << message;
}
private:
mojo::Binding<sample::mojom::Logger> binding_;
DISALLOW_COPY_AND_ASSIGN(LoggerImpl);
};
これで、保留中の LoggerRequest に対して LoggerImpl を構築でき、以前にキューイングされた Log メッセージは LoggerImpl のシーケンス上で可能な限り早くディスパッチされます。
LoggerImpl impl(std::move(request));
以下の図は、上記のコード行によって引き起こされる一連のイベントを示しています。
LoggerImplコンストラクタが呼び出され、LoggerRequestがBindingに渡されます。BindingはLoggerRequestのパイプ端点の所有権を取得し、読み取り可能かどうかの監視を開始します。パイプはすぐに読み取り可能になるため、保留中のLogメッセージを可能な限り早く読み取るタスクがスケジュールされます。Logメッセージが読み取られ、デシリアライズされ、BindingがバインドされたLoggerImplのLogger::Log実装を呼び出します。
結果として、実装はクライアントの "Hello!" メッセージを LOG(ERROR) で記録します。
注意: メッセージは、パイプをバインドするオブジェクト(上記の例では
mojo::Binding)が存在する間のみ、パイプから読み取られ、ディスパッチされます。
応答の受信
一部の Mojom インターフェースメソッドは応答を期待します。Logger インターフェースを変更して、最後に記録された行を照会できるようにしてみましょう。
module sample.mojom;
interface Logger {
Log(string message);
GetTail() => (string message);
};
生成される C++ インターフェースは以下のようになります。
namespace sample {
namespace mojom {
class Logger {
public:
virtual ~Logger() {}
virtual void Log(const std::string& message) = 0;
using GetTailCallback = base::OnceCallback<void(const std::string& message)>;
virtual void GetTail(GetTailCallback callback) = 0;
}
} // namespace mojom
} // namespace sample
これまでと同様に、このインターフェースのクライアントと実装は両方とも GetTail メソッドに同じシグネチャを使用します。実装は callback 引数を使用してリクエストに応答し、クライアントは callback 引数を渡して非同期的に応答を受信します。以下は更新された実装です。
class LoggerImpl : public sample::mojom::Logger {
public:
explicit LoggerImpl(sample::mojom::LoggerRequest request)
: binding_(this, std::move(request)) {}
~LoggerImpl() override {}
// sample::mojom::Logger:
void Log(const std::string& message) override {
LOG(ERROR) << "[Logger] " << message;
lines_.push_back(message);
}
void GetTail(GetTailCallback callback) override {
std::move(callback).Run(lines_.back());
}
private:
mojo::Binding<sample::mojom::Logger> binding_;
std::vector<std::string> lines_;
};
そして、更新されたクライアント呼び出しは以下のようになります。
void OnGetTail(const std::string& message) {
LOG(ERROR) << "Tail was: " << message;
}
logger->GetTail(base::BindOnce(&OnGetTail));
内部的には、実装側のコールバックは応答引数をシリアライズし、パイプに書き込んでクライアントに返送します。一方、クライアント側のコールバックは、パイプ上の着信応答メッセージを監視し、到着次第読み取ってデシリアライズし、デシリアライズされたパラメータでコールバックを呼び出す内部ロジックによって呼び出されます。
接続エラー
パイプが切断されると、両方の端点が接続エラーを観測できます(切断が端点のクローズ/破棄による場合、その端点は通知を受け取れません)。切断時に端点に残っている受信メッセージがある場合、それらのメッセージが排出されるまで接続エラーはトリガーされません。
パイプ切断の原因としては以下が考えられます。
- Mojo システムレベルの原因: プロセス終了、リソース枯渇など。
- 受信メッセージ処理時の検証エラーにより、バインディングがパイプを閉じる。
- ピア端点がクローズされる。例えば、リモート側がバインドされた
mojo::InterfacePtr<T>で、それが破棄される。
原因に関わらず、バインディング端点で接続エラーが発生すると、その端点の 接続エラーハンドラ(設定されている場合)が呼び出されます。このハンドラは単純な base::Closure であり、端点が同じパイプにバインドされている限り1回だけ呼び出されます。通常、クライアントと実装はこのハンドラを使用してクリーンアップを行ったり、エラーが予期しないものであった場合に新しいパイプを作成して再接続を試みたりします。
すべてのメッセージパイプバインディング C++ オブジェクト(mojo::Binding<T>、mojo::InterfacePtr<T> など)は、set_connection_error_handler メソッドを介して接続エラーハンドラの設定をサポートしています。
別のエンドツーエンドの Logger 例でエラーハンドラの呼び出しを示します。
sample::mojom::LoggerPtr logger;
LoggerImpl impl(mojo::MakeRequest(&logger));
impl.set_connection_error_handler(base::BindOnce([] { LOG(ERROR) << "Bye."; }));
logger->Log("OK cool");
logger.reset(); // クライアント側を閉じる。
impl がここで生き続ける限り、最終的に Log メッセージを受信し、その直後に "Bye." を出力するバインドされたコールバックの呼び出しを受け取ります。他のすべてのバインディングコールバックと同様に、接続エラーハンドラは、対応するバインディングオブジェクトが破棄された後は決して呼び出されません。
実際、代わりに LoggerImpl がコンストラクタ内で以下のようなエラーハンドラを設定していたとします。
LoggerImpl::LoggerImpl(sample::mojom::LoggerRequest request)
: binding_(this, std::move(request)) {
binding_.set_connection_error_handler(
base::BindOnce(&LoggerImpl::OnError, base::Unretained(this)));
}
void LoggerImpl::OnError() {
LOG(ERROR) << "Client disconnected! Purging log lines.";
lines_.clear();
}
base::Unretained の使用は安全です。なぜなら、エラーハンドラは binding_ の有効期間を超えて呼び出されることはなく、this が binding_ を所有しているからです。
端点の有効期間とコールバックに関する注意
mojo::InterfacePtr<T> が破棄されると、保留中のコールバックおよび(登録されている場合)接続エラーハンドラが呼び出されないことが保証されます。
mojo::Binding<T> が破棄されると、それ以上メソッド呼び出しが実装にディスパッチされず、(登録されている場合)接続エラーハンドラが呼び出されないことが保証されます。
プロセスクラッシュとコールバックに関するベストプラクティス
コールバックを受け取る mojo インターフェースメソッドを呼び出す際の一般的な状況は、呼び出し元がもう一方の端点が(例えばクラッシュにより)破棄されたかどうかを知りたい場合です。その場合、消費者は通常、応答コールバックが実行されないかどうかを知りたいとします。この問題には、InterfacePtr<T> の保持方法に応じて、異なる解決策があります。
- 消費者が
InterfacePtr<T>を所有する場合:set_connection_error_handlerを使用する必要があります。 - 消費者が
InterfacePtr<T>を所有しない場合: 呼び出し元が望む動作に応じて2つのヘルパーがあります。呼び出し元がエラーハンドラが確実に実行されることを望む場合は、mojo::WrapCallbackWithDropHandlerを使用する必要があります。呼び出し元がコールバックが常に実行されることを望む場合は、mojo::WrapCallbackWithDefaultInvokeIfNotRunヘルパーを使用する必要があります。これらのヘルパーの両方で、通常のコールバック注意事項に従い、コールバックが消費者の破棄後に実行されないようにする必要があります(例えば、InterfacePtr<T>の所有者が消費者よりも長生きする場合)。これには、base::WeakPtrまたはbase::RefCountedの使用が含まれます。また、これらのヘルパーを使用すると、InterfacePtr がリセットまたは破棄されている間に、コールバックが同期的に実行される可能性があることにも注意してください。
順序に関する注意
前述のセクションで述べたように、パイプの一端を閉じると、最終的にもう一方の端で接続エラーがトリガーされます。ただし、このイベント自体は、パイプ上の他のイベント(メッセージの書き込みなど)に対して順序付けられていることに注意することが重要です。
つまり、以下のような工夫されたコードを書くことは安全です。
void GoBindALogger(sample::mojom::LoggerRequest request) {
LoggerImpl impl(std::move(request));
base::RunLoop loop;
impl.set_connection_error_handler(loop.QuitClosure());
loop.Run();
}
void LogSomething() {
sample::mojom::LoggerPtr logger;
bg_thread->task_runner()->PostTask(
FROM_HERE, base::BindOnce(&GoBindALogger, mojo::MakeRequest(&logger)));
logger->Log("OK Computer");
}
logger がスコープ外になると、メッセージパイプの自端を即座に閉じますが、実装側は送信された Log メッセージを受信するまでこれに気付きません。したがって、上記の impl は最初にメッセージを記録し、その後 接続エラーを確認してランループを終了します。
型
列挙型
Mojom の列挙型は、int32_t を基底型とする、同等の厳密に型付けされた C++11 enum クラスに直接変換されます。型名と値の名前は Mojom と C++ で同じです。Mojo は常に、最大の列挙子と同じ値を持つ特別な列挙子 kMaxValue を定義します。これにより、Mojo の列挙型をヒストグラムに記録したり、従来の IPC と相互運用したりすることが容易になります。
例えば、以下の Mojom 定義を考えます。
module business.mojom;
enum Department {
kEngineering,
kMarketing,
kSales,
};
これは以下の C++ 定義に変換されます。
namespace business {
namespace mojom {
enum class Department : int32_t {
kEngineering,
kMarketing,
kSales,
kMaxValue = kSales,
};
} // namespace mojom
} // namespace business
構造体
Mojom の構造体は、フィールドを論理的にグループ化して新しい複合型を定義するために使用できます。各 Mojom 構造体は、同名の代表的な C++ クラスを生成し、同名のパブリックフィールド(対応する C++ 型)といくつかの便利なパブリックメソッドを持ちます。
例えば、以下の Mojom 構造体を考えます。
module business.mojom;
struct Employee {
int64 id;
string username;
Department department;
};
これは以下のような C++ クラスを生成します。
namespace business {
namespace mojom {
class Employee;
using EmployeePtr = mojo::StructPtr<Employee>;
class Employee {
public:
// デフォルトコンストラクタ - デフォルト値(Mojom 内で明示的に指定された可能性がある)を適用します。
Employee();
// 値コンストラクタ - 構造体の各フィールドに対して、Mojom 定義の辞書順で明示的な引数を受け取ります。
Employee(int64_t id, const std::string& username, Department department);
// この構造体の新しいコピーを作成します。
EmployeePtr Clone();
// 同じ型の別の構造体と等しいかどうかをテストします。
bool Equals(const Employee& other);
// Mojom と同じ名前のパブリックフィールド。
int64_t id;
std::string username;
Department department;
};
} // namespace mojom
} // namespace business
メッセージパラメータとして、または別の Mojom 構造体内のフィールドとして使用される場合、struct 型は move-only の mojo::StructPtr ヘルパーでラップされます。これは std::unique_ptr とほぼ同等で、いくつかの追加ユーティリティメソッドを備えています。これにより、構造体の値を null 可能にしたり、構造体型を自己参照可能にしたりできます。
生成された各構造体クラスには、New の引数を転送して構築されたクラスの新しいインスタンスをラップする、新しい mojo::StructPtr<T> を返す静的 New() メソッドがあります。例えば、
mojom::EmployeePtr e1 = mojom::Employee::New();
e1->id = 42;
e1->username = "mojo";
e1->department = mojom::Department::kEngineering;
は以下と同等です。
auto e1 = mojom::Employee::New(42, "mojo", mojom::Department::kEngineering);
ここで、以下のようなインターフェースを定義すると、
interface EmployeeManager {
AddEmployee(Employee e);
};
実装する C++ インターフェースは以下のようになります。
class EmployeeManager {
public:
virtual ~EmployeeManager() {}
virtual void AddEmployee(EmployeePtr e) = 0;
};
そして、C++ コードから以下のようにメッセージを送信できます。
mojom::EmployeeManagerPtr manager = ...;
manager->AddEmployee(
Employee::New(42, "mojo", mojom::Department::kEngineering));
// または
auto e = Employee::New(42, "mojo", mojom::Department::kEngineering);
manager->AddEmployee(std::move(e));
共用体
構造体と同様に、タグ付き共用体は同名の代表的な C++ クラスを生成し、通常は mojo::StructPtr<T> でラップされます。
構造体とは異なり、生成された共用体のすべてのフィールドはプライベートであり、アクセサを使用して取得および設定する必要があります。フィールド foo には foo() でアクセスし、set_foo() で設定します。また、各フィールドには is_foo() というブール値メソッドがあり、共用体が現在他のすべての共用体フィールドを除外してフィールド foo の値を取っているかどうかを示します。
最後に、生成された各共用体クラスには、名前付きの共用体フィールドをすべて列挙するネストされた Tag enum クラスがあります。Mojom 共用体の値の現在の型は、which() メソッドを呼び出すことで判断でき、Tag が返ります。
例えば、以下の Mojom 定義を考えます。
union Value {
int64 int_value;
float float_value;
string string_value;
};
interface Dictionary {
AddValue(string key, Value value);
};
これは以下の C++ インターフェースを生成します。
class Value {
public:
~Value() {}
};
class Dictionary {
public:
virtual ~Dictionary() {}
virtual void AddValue(const std::string& key, ValuePtr value) = 0;
};
そして、以下のように使用できます。
ValuePtr value = Value::New();
value->set_int_value(42);
CHECK(value->is_int_value());
CHECK_EQ(value->which(), Value::Tag::INT_VALUE);
value->set_float_value(42);
CHECK(value->is_float_value());
CHECK_EQ(value->which(), Value::Tag::FLOAT_VALUE);
value->set_string_value("bananas");
CHECK(value->is_string_value());
CHECK_EQ(value->which(), Value::Tag::STRING_VALUE);
最後に、共用体の値が特定のフィールドで現在占有されていない場合、そのフィールドへのアクセスを試みると DCHECK が発生することに注意してください。
ValuePtr value = Value::New();
value->set_int_value(42);
LOG(INFO) << "Value is " << value->string_value(); // DCHECK!
インターフェースを介したインターフェースの送信
インターフェースパイプの作成方法と、Ptr および Request 端点の使用方法を学びました。これだけでは興味深い IPC にはなりません。Mojo IPC の基本は、インターフェース端点を他のインターフェースを介して転送する機能です。その方法を見てみましょう。
インターフェースリクエストの送信
//sample/db.mojom に新しい Mojom の例を考えます。
module db.mojom;
interface Table {
void AddRow(int32 key, string data);
};
interface Database {
AddTable(Table& table);
};
Mojom IDL ドキュメントで述べられているように、Table& 構文は Table インターフェースリクエストを示します。これは、前述のセクションで説明した InterfaceRequest<T> 型に正確に対応し、実際、これらのインターフェースの生成コードはおおよそ以下のようになります。
namespace db {
namespace mojom {
class Table {
public:
virtual ~Table() {}
virtual void AddRow(int32_t key, const std::string& data) = 0;
}
using TablePtr = mojo::InterfacePtr<Table>;
using TableRequest = mojo::InterfaceRequest<Table>;
class Database {
public:
virtual ~Database() {}
virtual void AddTable(TableRequest table);
};
using DatabasePtr = mojo::InterfacePtr<Database>;
using DatabaseRequest = mojo::InterfaceRequest<Database>;
} // namespace mojom
} // namespace db
これらすべてを Table と Database の実装でまとめてみましょう。
#include "sample/db.mojom.h"
class TableImpl : public db::mojom::Table {
public:
explicit TableImpl(db::mojom::TableRequest request)
: binding_(this, std::move(request)) {}
~TableImpl() override {}
// db::mojom::Table:
void AddRow(int32_t key, const std::string& data) override {
rows_.insert({key, data});
}
private:
mojo::Binding<db::mojom::Table> binding_;
std::map<int32_t, std::string> rows_;
};
class DatabaseImpl : public db::mojom::Database {
public:
explicit DatabaseImpl(db::mojom::DatabaseRequest request)
: binding_(this, std::move(request)) {}
~DatabaseImpl() override {}
// db::mojom::Database:
void AddTable(db::mojom::TableRequest table) {
tables_.emplace_back(std::make_unique<TableImpl>(std::move(table)));
}
private:
mojo::Binding<db::mojom::Database> binding_;
std::vector<std::unique_ptr<TableImpl>> tables_;
};
非常にわかりやすいです。AddTable への Table& Mojom パラメータは、C++ の db::mojom::TableRequest(mojo::InterfaceRequest<db::mojom::Table> のエイリアス)に変換されます。これは、厳密に型付けされたメッセージパイプハンドルにすぎません。DatabaseImpl が AddTable 呼び出しを受け取ると、新しい TableImpl を構築し、受信した TableRequest にバインドします。
使用方法を見てみましょう。
db::mojom::DatabasePtr database;
DatabaseImpl db_impl(mojo::MakeRequest(&database));
db::mojom::TablePtr table1, table2;
database->AddTable(mojo::MakeRequest(&table1));
database->AddTable(mojo::MakeRequest(&table2));
table1->AddRow(1, "hiiiiiiii");
table2->AddRow(2, "heyyyyyy");
TableRequest 端点がまだ転送中であっても、新しい Table パイプをすぐに使用開始できることに注意してください。
InterfacePtr の送信
もちろん、InterfacePtr を送信することもできます。
interface TableListener {
OnRowAdded(int32 key, string data);
};
interface Table {
AddRow(int32 key, string data);
AddListener(TableListener listener);
};
これにより、以下のような Table::AddListener シグネチャが生成されます。
virtual void AddListener(TableListenerPtr listener) = 0;
そして、以下のように使用できます。
db::mojom::TableListenerPtr listener;
TableListenerImpl impl(mojo::MakeRequest(&listener));
table->AddListener(std::move(listener));
その他のインターフェースバインディング型
「インターフェース」のセクションでは、最も一般的なバインディングオブジェクト型である InterfacePtr、InterfaceRequest、Binding の基本的な使用方法について説明しました。これらの型はおそらく実際に最もよく使用されますが、クライアント側と実装側の両方でインターフェースパイプをバインドする方法は他にもいくつかあります。
強力なバインディング
強力なバインディング は、インターフェースの実装を所有し、バインドされたインターフェース端点がエラーを検出すると自動的にクリーンアップするスタンドアロンオブジェクトとして存在します。このようなバインディングを作成するには、MakeStrongBinding 関数を使用します。
class LoggerImpl : public sample::mojom::Logger {
public:
LoggerImpl() {}
~LoggerImpl() override {}
// sample::mojom::Logger:
void Log(const std::string& message) override {
LOG(ERROR) << "[Logger] " << message;
}
private:
// 注意: これは Binding オブジェクトを所有しません!
};
sample::mojom::LoggerPtr logger;
mojo::MakeStrongBinding(std::make_unique<LoggerImpl>(),
mojo::MakeRequest(&logger));
logger->Log("NOM NOM NOM MESSAGES");
これで、logger がシステム内のどこかで開かれている限り、反対側のバインドされた LoggerImpl は生き続けます。
バインディングセット
単一の実装インスタンスを複数のクライアントで共有すると便利な場合があります。BindingSet を使用すると、これを簡単に行えます。Mojom を考えます。
module system.mojom;
interface Logger {
Log(string message);
};
interface LoggerProvider {
GetLogger(Logger& logger);
};
BindingSet を使用して、複数の Logger リクエストを単一の実装インスタンスにバインドできます。
class LogManager : public system::mojom::LoggerProvider,
public system::mojom::Logger {
public:
explicit LogManager(system::mojom::LoggerProviderRequest request)
: provider_binding_(this, std::move(request)) {}
~LogManager() {}
// system::mojom::LoggerProvider:
void GetLogger(LoggerRequest request) override {
logger_bindings_.AddBinding(this, std::move(request));
}
// system::mojom::Logger:
void Log(const std::string& message) override {
LOG(ERROR) << "[Logger] " << message;
}
private:
mojo::Binding<system::mojom::LoggerProvider> provider_binding_;
mojo::BindingSet<system::mojom::Logger> logger_bindings_;
};
InterfacePtr セット
上記の BindingSet と同様に、イベントを監視するクライアントのセットなどのために、InterfacePtr のセットを維持すると便利な場合があります。InterfacePtrSet が役立ちます。Mojom を例に挙げます。
module db.mojom;
interface TableListener {
OnRowAdded(int32 key, string data);
};
interface Table {
AddRow(int32 key, string data);
AddListener(TableListener listener);
};
Table の実装は次のようになります。
class TableImpl : public db::mojom::Table {
public:
TableImpl() {}
~TableImpl() override {}
// db::mojom::Table:
void AddRow(int32_t key, const std::string& data) override {
rows_.insert({key, data});
listeners_.ForEach([key, &data](db::mojom::TableListener* listener) {
listener->OnRowAdded(key, data);
});
}
void AddListener(db::mojom::TableListenerPtr listener) {
listeners_.AddPtr(std::move(listener));
}
private:
mojo::InterfacePtrSet<db::mojom::TableListener> listeners_;
std::map<int32_t, std::string> rows_;
};
関連インターフェース
関連インターフェースは以下のようなインターフェースです。
- メッセージの順序を維持しながら、単一のメッセージパイプ上で複数のインターフェースを実行できるようにします。
- バインディングが複数のシーケンスから単一のメッセージパイプにアクセスできるようにします。
Mojom
インターフェースポインタ/リクエストフィールドに新しいキーワード associated が導入されました。例えば、
interface Bar {};
struct Qux {
associated Bar bar3;
};
interface Foo {
// 関連インターフェースポインタを使用します。
SetBar(associated Bar bar1);
// 関連インターフェースリクエストを使用します。
GetBar(associated Bar& bar2);
// 関連インターフェースポインタを持つ構造体を渡します。
PassQux(Qux qux);
// コールバックで関連インターフェースポインタを使用します。
AsyncGetBar() => (associated Bar bar4);
};
これは、インターフェースの実装/クライアントが、関連インターフェースポインタ/リクエストが渡されるのと同じメッセージパイプを使用して通信することを意味します。
C++ での関連インターフェースの使用
C++ バインディングを生成する場合、Bar の関連インターフェースポインタは BarAssociatedPtrInfo(mojo::AssociatedInterfacePtrInfo<Bar> のエイリアス)にマッピングされ、関連インターフェースリクエストは BarAssociatedRequest(mojo::AssociatedInterfaceRequest<Bar> のエイリアス)にマッピングされます。
// Mojom 内:
interface Foo {
...
SetBar(associated Bar bar1);
GetBar(associated Bar& bar2);
...
};
// C++ 内:
class Foo {
...
virtual void SetBar(BarAssociatedPtrInfo bar1) = 0;
virtual void GetBar(BarAssociatedRequest bar2) = 0;
...
};
関連インターフェースリクエストの受け渡し
すでに InterfacePtr<Foo> foo_ptr を取得しており、それに対して GetBar() を呼び出したいとします。以下のように行います。
BarAssociatedPtrInfo bar_ptr_info;
BarAssociatedRequest bar_request = MakeRequest(&bar_ptr_info);
foo_ptr->GetBar(std::move(bar_request));
// BarAssociatedPtr は AssociatedInterfacePtr<Bar> のエイリアスです。
BarAssociatedPtr bar_ptr;
bar_ptr.Bind(std::move(bar_ptr_info));
bar_ptr->DoSomething();
最初に、コードは Bar 型の関連インターフェースを作成します。これは、関連付けられていないインターフェースをセットアップする場合と非常によく似ています。重要な違いは、2つの関連端点のうちの1つ(bar_request または bar_ptr_info)を別のインターフェースを介して送信する必要があることです。これにより、インターフェースが既存のメッセージパイプに関連付けられます。
bar_request を渡す前に bar_ptr->DoSomething() を呼び出すことはできないことに注意してください。これは FIFO 性の保証に必要です。受信側で、DoSomething 呼び出しのメッセージが到着したときに、後続のメッセージを処理する前に対応する AssociatedBinding<Bar> にディスパッチしたいと考えています。もし bar_request が後続のメッセージにある場合、メッセージディスパッチはデッドロックに陥ります。一方、bar_request が送信されるとすぐに bar_ptr は使用可能になります。bar_request がリモート側の実装にバインドされるのを待つ必要はありません。
コードを少し短くするために、AssociatedInterfacePtrInfo ポインタの代わりに AssociatedInterfacePtr ポインタを受け取る MakeRequest オーバーロードが提供されています。次のコードも同じ目的を達成します。
BarAssociatedPtr bar_ptr;
foo_ptr->GetBar(MakeRequest(&bar_ptr));
bar_ptr->DoSomething();
Foo の実装は次のようになります。
class FooImpl : public Foo {
...
void GetBar(BarAssociatedRequest bar2) override {
bar_binding_.Bind(std::move(bar2));
...
}
...
Binding<Foo> foo_binding_;
AssociatedBinding<Bar> bar_binding_;
};
この例では、bar_binding_ の有効期間は FooImpl に関連付けられています。ただし、そうする必要はありません。例えば、bar2 を別のシーケンスに渡して、そこで AssociatedBinding<Bar> にバインドすることもできます。
基になるメッセージパイプが切断されると(例えば、foo_ptr または foo_binding_ が破棄されると)、すべての関連インターフェース端点(例えば、bar_ptr と bar_binding_)は接続エラーを受け取ります。
関連インターフェースポインタの受け渡し
同様に、すでに InterfacePtr<Foo> foo_ptr を取得しており、それに対して SetBar() を呼び出したいとします。以下のように行います。
AssociatedBinding<Bar> bar_binding(some_bar_impl);
BarAssociatedPtrInfo bar_ptr_info;
BarAssociatedRequest bar_request = MakeRequest(&bar_ptr_info);
foo_ptr->SetBar(std::move(bar_ptr_info));
bar_binding.Bind(std::move(bar_request));
次のコードも同じ目的を達成します。
AssociatedBinding<Bar> bar_binding(some_bar_impl);
BarAssociatedPtrInfo bar_ptr_info;
bar_binding.Bind(&bar_ptr_info);
foo_ptr->SetBar(std::move(bar_ptr_info));
パフォーマンスに関する考慮事項
関連インターフェースをマスターインターフェース(マスターインターフェースが存在するシーケンス)とは異なるシーケンスで使用する場合:
- メッセージの送信:送信は呼び出し側のシーケンスで直接行われます。そのため、シーケンスのホッピングはありません。
- メッセージの受信:マスターインターフェースとは異なるシーケンスにバインドされた関連インターフェースは、ディスパッチ中に余分なシーケンスホップが発生します。
したがって、パフォーマンスの観点から、関連インターフェースはメッセージの受信がマスターシーケンスで行われるシナリオに適しています。
テスト
関連インターフェースは、使用可能になる前にマスターインターフェースに関連付ける必要があります。つまり、関連インターフェースの一方の端をマスターインターフェースの一方の端を介して送信するか、または既にマスターインターフェースを持つ別の関連インターフェースの一方の端を介して送信する必要があります。
最初に関連付けずに関連インターフェース端点をテストしたい場合は、mojo::MakeIsolatedRequest() を使用できます。これにより、実際には他の何にも関連付けられていない、機能する関連インターフェース端点が作成されます。
同期呼び出し
別のドキュメントを参照してください(TODO: 上記のドキュメントをリポジトリのマークダウンドキュメントに移動します)。
型マッピング
多くの場合、生成された C++ バインディングがインターフェースメソッド内で特定の Mojom 型を表すためにより自然な型を使用できるようにしたい場合があります。例えば、以下の Rect のような Mojom 構造体を考えます。
module gfx.mojom;
struct Rect {
int32 x;
int32 y;
int32 width;
int32 height;
};
interface Canvas {
void FillRect(Rect rect);
};
Canvas Mojom インターフェースは通常、以下のような C++ インターフェースを生成します。
class Canvas {
public:
virtual void FillRect(RectPtr rect) = 0;
};
しかし、Chromium ツリーにはすでに意味的に同等で、便利なヘルパーメソッドを持つネイティブの gfx::Rect が定義されています。メッセージ境界ごとに gfx::Rect と Mojom 生成の RectPtr を手動で変換する代わりに、Mojom バインディングジェネレータが代わりに以下を生成できれば便利です。
class Canvas {
public:
virtual void FillRect(const gfx::Rect& rect) = 0;
}
正解は「はい、それは素晴らしいでしょう!」です。そして幸いなことに、それが可能です!
グローバル設定
この機能は非常に強力ですが、ビルドシステムに避けられない複雑さをもたらします。これは、型マッピングが本質的にウイルス的な概念であるという事実に起因します。gfx::mojom::Rect がどこかで gfx::Rect にマッピングされている場合、そのマッピングはすべての場所に適用する必要があります。
このため、chromium_bindings_configuration.gni と blink_bindings_configuration.gni で定義されたいくつかのグローバルタイプマップ設定があります。これらは、リポジトリ内の Mojom 生成バインディングの2つのサポートされているバリアントを設定します。詳細については、後続のセクションを参照してください。
ここでは、gfx::mojom::Rect から gfx::Rect へのマッピングを表現する方法を見てみましょう。
StructTraits の定義
生成されたバインディングコードに、任意のネイティブ型 T を任意の Mojom 型 mojom::U としてシリアライズする方法を教えるためには、mojo::StructTraits テンプレートの適切な特殊化を定義する必要があります。
StructTraits の有効な特殊化は、以下の静的メソッドを定義する必要があります。
- Mojom 構造体のフィールドごとに1つの静的アクセサ。名前は構造体フィールドと完全に同じでなければなりません。これらのアクセサはすべて、ネイティブ型のオブジェクトへの const 参照を受け取り、Mojom 構造体フィールドの型と互換性のある値を返す必要があります。これは、メッセージのシリアライズ中に、ネイティブ型からデータを安全かつ一貫して抽出し、余分なコピーコストをかけないために使用されます。
- Mojom 構造体のシリアライズ表現が与えられた場合に、ネイティブ型のインスタンスを初期化する単一の静的
Readメソッド。Readメソッドは、受信データが受け入れられるかどうかを示すboolを返す必要があります(trueは受け入れ、falseは拒否)。
StructTraits の特殊化が、あまり一般的でない要件を満たすために定義できる他のメソッドもあります。詳細については、高度な StructTraits の使用法を参照してください。
gfx::Rect のマッピングを定義するには、以下の StructTraits の特殊化が必要です。これは //ui/gfx/geometry/mojo/geometry_struct_traits.h に定義します。
#include "mojo/public/cpp/bindings/struct_traits.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/mojo/geometry.mojom.h"
namespace mojo {
template <>
class StructTraits<gfx::mojom::RectDataView, gfx::Rect> {
public:
static int32_t x(const gfx::Rect& r) { return r.x(); }
static int32_t y(const gfx::Rect& r) { return r.y(); }
static int32_t width(const gfx::Rect& r) { return r.width(); }
static int32_t height(const gfx::Rect& r) { return r.height(); }
static bool Read(gfx::mojom::RectDataView data, gfx::Rect* out_rect);
};
} // namespace mojo
そして、//ui/gfx/geometry/mojo/geometry_struct_traits.cc では、
#include "ui/gfx/geometry/mojo/geometry_struct_traits.h"
namespace mojo {
// static
template <>
bool StructTraits<gfx::mojom::RectDataView, gfx::Rect>::Read(
gfx::mojom::RectDataView data,
gfx::Rect* out_rect) {
if (data.width() < 0 || data.height() < 0)
return false;
out_rect->SetRect(data.x(), data.y(), data.width(), data.height());
return true;
};
} // namespace mojo
Read() メソッドは、受信した width または height フィールドが負の場合に false を返すことに注意してください。これはデシリアライズ中の検証ステップとして機能します。クライアントが負の幅または高さを持つ gfx::Rect を送信すると、そのメッセージは拒否され、パイプは閉じられます。このように、型マッピングは、呼び出しサイトやインターフェースの実装をより便利にするだけでなく、カスタム検証ロジックを有効にするためにも使用できます。
新しい型マッピングの有効化
必要な StructTraits を定義しましたが、バインディングジェネレータ(つまりビルドシステム)にマッピングについて教える必要があります。これを行うには、typemap ファイルを作成する必要があります。このファイルは、使い慣れた GN 構文を使用して、新しい型マッピングを記述します。
この geometry.typemap ファイルを Mojom ファイルと同じ場所に配置しましょう。
mojom = "//ui/gfx/geometry/mojo/geometry.mojom"
public_headers = [ "//ui/gfx/geometry/rect.h" ]
traits_headers = [ "//ui/gfx/geometry/mojo/geometry_struct_traits.h" ]
sources = [
"//ui/gfx/geometry/mojo/geometry_struct_traits.cc",
"//ui/gfx/geometry/mojo/geometry_struct_traits.h",
]
public_deps = [ "//ui/gfx/geometry" ]
type_mappings = [
"gfx.mojom.Rect=gfx::Rect",
]
上記の各変数を見てみましょう。
mojom: typemap が適用されるmojomファイルを指定します。複数の typemap が同じmojomファイルに適用される可能性がありますが、特定の typemap は単一のmojomファイルにのみ適用できます。public_headers: typemap が適用されたgfx.mojom.Rectの Mojom 定義に依存するコードが必要とする追加のヘッダー。ネイティブターゲット型の定義に必要なヘッダーはここにリストする必要があります。traits_headers: このファイルで説明されている型マッピングの関連するStructTraits特殊化を含むヘッダー。sources:StructTraits定義に必要な実装ソースとヘッダー。これらのソースは、この typemap を適用するmojomファイルの生成された C++ バインディングターゲットに直接コンパイルされます。public_deps:public_headersとtraits_headersによって公開されるターゲット依存関係。deps:sourcesによって公開されるが、public_depsではまだカバーされていないターゲット依存関係。type_mappings: この typemap に適用される型マッピングのリスト。このリストの文字列は"MojomType=CppType"の形式です。MojomTypeは完全修飾の Mojom 型名、CppTypeは完全修飾の C++ 型名である必要があります。CppTypeの後に角括弧で囲んで追加の属性を指定できます。move_only:CppTypeは move-only であり、生成されたメソッドシグネチャで値渡しする必要があります。move_onlyは推移的であるため、MojomTypeのコンテナは値渡しされるCppTypeのコンテナに変換されます。copyable_pass_by_value:CppTypeの値を移動せずに値渡しするように強制します。move_onlyとは異なり、これは推移的ではありません。nullable_is_same_type: デフォルトでは、null 不可能なMojomTypeはCppTypeにマッピングされ、null 可能なMojomType?はbase::Optional<CppType>にマッピングされます。この属性が設定されている場合、null 可能なMojomType?値に対してbase::Optionalラッパーは省略されますが、この型マッピングのStructTraits定義は追加のIsNullおよびSetToNullメソッドを定義する必要があります。以下の「Null 可能性の特殊化」を参照してください。force_serialize: typemap は遅延シリアライズと互換性がありません(例えば、base::StringPieceへの typemap を考えてみてください。コピーを保持するのは安全ではありません)。この型を運ぶメッセージは、即時シリアライズパスに強制されます。
typemap ファイルができたので、それをグローバル設定に追加できるローカルの typemap リストに追加する必要があります。以下の内容で新しい //ui/gfx/typemaps.gni ファイルを作成します。
typemaps = [
"//ui/gfx/geometry/mojo/geometry.typemap",
]
最後に、chromium_bindings_configuration.gni の _typemap_imports に追加することで、このファイルをグローバルデフォルト(Chromium)バインディング設定で参照できます。
_typemap_imports = [
...,
"//ui/gfx/typemaps.gni",
...,
]
バージョン管理に関する考慮事項
Mojom IDL でのバージョン管理の一般的なドキュメントについては、バージョン管理を参照してください。
このセクションでは、バージョン管理された Mojom 型に関連する C++ 固有の考慮事項について簡単に説明します。
インターフェースバージョンの問い合わせ
InterfacePtr は、リモートインターフェースのバージョンを問い合わせたり、アサートしたりするための以下のメソッドを定義します。
void QueryVersion(const base::Callback<void(uint32_t)>& callback);
これは、リモートエンドポイントにそのバインディングのバージョン番号を問い合わせます。応答を受信すると、callback がリモートバージョン番号で呼び出されます。この値は、冗長な問い合わせを避けるために InterfacePtr インスタンスによってキャッシュされることに注意してください。
void RequireVersion(uint32_t version);
リモートエンドポイントに、クライアントが最低限 version のバージョンを必要とすることを通知します。リモートエンドポイントがそのバージョンをサポートできない場合、パイプの自端を即座に閉じ、他のリクエストを受信できなくなります。
バージョン管理された列挙型
便利なように、すべての拡張可能な列挙型には、受信した列挙値が実装の現在のバージョンの列挙型定義で認識されているかどうかを判断するための生成されたヘルパー関数があります。例えば、
[Extensible]
enum Department {
SALES,
DEV,
RESEARCH,
};
は、生成された C++ 列挙型と同じ名前空間に関数を生成します。
inline bool IsKnownEnumValue(Department value);