Mojo C++ 绑定 API

Mojo C++ 绑定 API

官方文档:https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/mojo/README.md

本文档是Mojo 文档的子集。

概述

Mojo C++ 绑定 API 利用C++ 系统 API提供一组更自然的原语,用于通过 Mojo 消息管道进行通信。结合从Mojom IDL 和绑定生成器生成的代码,用户可以轻松地跨任意进程内和进程间边界连接接口客户端和实现。

本文档通过示例代码片段提供了有关绑定 API 使用的详细指南。有关详细的 API 参考,请参阅//mojo/public/cpp/bindings 中的标头。

有关针对 Chromium 开发人员的简化指南,请参阅此链接

入门

当Mojom IDL文件由绑定发生器处理,C ++代码被发射的一系列.h和.cc文件与基于输入的名称.mojom的文件。假设我们在以下位置创建了以下 Mojom 文件//services/db/public/mojom/db.mojom:

module db.mojom;

interface Table {
  AddRow(int32 key, string data);
};

interface Database {
  CreateTable(Table& table);
};

以及生成绑定的 GN 目标//services/db/public/mojom/BUILD.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++(纯虚拟​​)类接口定义,由接口上每个请求消息的单个生成方法签名组成。在内部,还有用于消息序列化和反序列化的生成代码,但这个细节对绑定消费者是隐藏的。

基本用法

让我们考虑一个新//sample/logger.mojom的定义一个简单的日志记录接口,客户端可以使用它来记录简单的字符串消息:

module sample.mojom;

interface Logger {
  Log(string message);
};

通过绑定生成器运行此操作将生成logger.mojom.h具有以下定义(模不重要的细节):

namespace sample {
namespace mojom {

class Logger {
  virtual ~Logger() {}

  virtual void Log(const std::string& message) = 0;
};

}  // namespace mojom
}  // namespace sample

远程和 PendingReceiver

在 Mojo 绑定库的世界中,这些是有效的强类型消息管道端点。如果 aRemote绑定到消息管道端点,则可以取消引用它以在不透明T接口上进行调用。这些调用会立即序列化它们的参数(使用生成的代码)并将相应的消息写入管道。

APendingReceiver本质上只是一个类型化的容器,用于保存 aRemote管道的另一端——接收端——直到它可以路由到将绑定它的某个实现。除了保持管道端点并携带有用的编译时类型信息之外,PendingReceiver它实际上不做任何事情。
m001
那么我们如何创建一个强类型的消息管道呢?

创建接口管道

一种方法是手动创建管道并用强类型对象包装每一端:

#include "sample/logger.mojom.h"

mojo::MessagePipe pipe;
mojo::Remote<sample::mojom::Logger> logger(
    mojo::PendingRemote<sample::mojom::Logger>(std::move(pipe.handle0), 0));
mojo::PendingReceiver<sample::mojom::Logger> receiver(std::move(pipe.handle1));

这很冗长,但 C++ 绑定库提供了一种更方便的方法来完成同样的事情。remote.h定义了一个BindNewPipeAndPassReceiver方法:

mojo::Remote<sample::mojom::Logger> logger;
auto receiver = logger.BindNewPipeAndPassReceiver();

第二个片段相当于第一个片段。

注意:在上面的第一个示例中,您可能会注意到mojo::PendingRemote. 这与 a 类似PendingReceiver,因为它仅持有管道句柄,而实际上不能在管道上读取或写入消息。这种类型和PendingReceiver在序列之间自由移动都是安全的,而边界Remote绑定到单个序列。

ARemote可以通过调用其Unbind()方法解除绑定,该方法返回一个新的PendingRemote。相反, anRemote可以绑定(并因此获得所有权) anPendingRemote以便可以在管道上进行接口调用。

的序列绑定性质Remote对于支持其消息响应和连接错误通知的安全分派是必要的。

一旦PendingRemote被绑定,我们就可以立即开始调用Logger它的接口方法,这将立即将消息写入管道。这些消息将在管道的接收端排队,直到有人绑定到它并开始读取它们。

logger->Log("Hello!");

这实际上将一条Log消息写入管道。
m0002
但是如上所述,PendingReceiver 实际上并没有做任何事情,因此该消息将永远停留在管道上。我们需要一种方法来读取管道另一端的消息并发送它们。我们必须绑定挂起的接收者。

绑定挂起的接收器

绑定库中有许多不同的帮助器类,用于绑定消息管道的接收端。其中最原始的是mojo::Receiver. Amojo::Receiver桥接具有T单个绑定消息管道端点(通过 a mojo::PendingReceiver)的实现,它不断监视可读性。

任何时候绑定管道变得可读时,Receiver都会安排一个任务来读取、反序列化(使用生成的代码),并将所有可用消息分派到绑定T实现。下面是该Logger接口的示例实现。请注意,实现本身拥有一个mojo::Receiver. 这是一种常见的模式,因为绑定实现必须比任何mojo::Receiver绑定它的实现都长。

#include "base/logging.h"
#include "base/macros.h"
#include "sample/logger.mojom.h"

class LoggerImpl : public sample::mojom::Logger {
 public:
  // NOTE: A common pattern for interface implementations which have one
  // instance per client is to take a PendingReceiver in the constructor.

  explicit LoggerImpl(mojo::PendingReceiver<sample::mojom::Logger> receiver)
      : receiver_(this, std::move(receiver)) {}
  ~Logger() override {}
  Logger(const Logger&) = delete;
  Logger& operator=(const Logger&) = delete;

  // sample::mojom::Logger:
  void Log(const std::string& message) override {
    LOG(ERROR) << "[Logger] " << message;
  }

 private:
  mojo::Receiver<sample::mojom::Logger> receiver_;
};

现在我们可以LoggerImpl在我们PendingReceiver的 上构造一个,并且先前排队的Log消息将在LoggerImpl的序列上尽快调度:

LoggerImpl impl(std::move(receiver));

下图说明了以下事件序列,所有这些事件都由上述代码行启动:

  1. 该LoggerImpl构造函数被调用,传递PendingReceiver沿到Receiver。
  2. 在Receiver采取所有权PendingReceiver的管道终点,并开始观察其可读性。管道可以立即读取,因此调度任务以Log尽快从管道读取待处理的消息。
  3. 该Log消息被读取和反序列化,导致Receiver调用Logger::Log其限制的实施LoggerImpl。
    m0003
    因此,我们的实现最终将"Hello!"通过.log 记录客户端的消息LOG(ERROR)。

注意:消息将仅被读取并从管道分派只要其结合它(对象即所述mojo::Receiver在上面的例子)保持活着。

接收回复

一些 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参数来异步receive响应。GetTailCallback传递给实现的参数GetTail是序列仿射。它必须在被调用的相同序列上GetTail被调用。客户端callback运行在它们调用的相同序列上GetTail(它们logger被绑定到的序列)。这是一个更新的实现:

class LoggerImpl : public sample::mojom::Logger {
 public:
  // NOTE: A common pattern for interface implementations which have one
  // instance per client is to take a PendingReceiver in the constructor.

  explicit LoggerImpl(mojo::PendingReceiver<sample::mojom::Logger> receiver)
      : receiver_(this, std::move(receiver)) {}
  ~Logger() override {}
  Logger(const Logger&) = delete;
  Logger& operator=(const Logger&) = delete;

  // 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::Receiver<sample::mojom::Logger> receiver_;
  std::vector<std::string> lines_;
};

以及更新的客户端调用:

void OnGetTail(const std::string& message) {
  LOG(ERROR) << "Tail was: " << message;
}

logger->GetTail(base::BindOnce(&OnGetTail));

在幕后,实现端回调实际上是序列化响应参数并将它们写入管道以传递回客户端。同时,客户端回调由一些内部逻辑调用,该逻辑监视管道以获取传入的响应消息,一旦它到达就读取并反序列化,然后使用反序列化的参数调用回调。

连接错误

如果管道断开连接,则两个端点都将能够观察到连接错误(除非断开连接是由关闭/销毁端点引起的,在这种情况下,该端点将不会收到此类通知)。如果断开连接时端点还有剩余的传入消息,则在消息耗尽之前不会触发连接错误。

管道断开可能由以下原因引起:

  • Mojo 系统级原因:进程终止、资源耗尽等。
  • 由于在处理收到的消息时出现验证错误,绑定会关闭管道。
  • 对等端点已关闭。例如,远程端是一个边界mojo::Remote,它被破坏了。

不管根本原因是什么,当在接收方端点上遇到连接错误时,将调用该端点的断开连接处理程序(如果已设置)。这个处理程序很简单base::OnceClosure,只要端点绑定到同一个管道,就只能调用一次。通常,客户端和实现使用此处理程序进行某种清理,或者——特别是如果错误是意外的——创建一个新管道并尝试与其建立新连接。

所有消息管结合C ++对象(例如,mojo::Receiver,mojo::Remote,等等)支持经由设置它们的断开处理程序set_disconnect_handler方法。

我们可以设置另一个端到端Logger示例来演示断开连接处理程序调用。假设LoggerImpl在其构造函数中设置了以下断开连接处理程序:

LoggerImpl::LoggerImpl(mojo::PendingReceiver<sample::mojom::Logger> receiver)
    : receiver_(this, std::move(receiver)) {
  receiver_.set_disconnect_handler(
      base::BindOnce(&LoggerImpl::OnError, base::Unretained(this)));
}

void LoggerImpl::OnError() {
  LOG(ERROR) << "Client disconnected! Purging log lines.";
  lines_.clear();
}

mojo::Remote<sample::mojom::Logger> logger;
LoggerImpl impl(logger.BindNewPipeAndPassReceiver());
logger->Log("OK cool");
logger.reset();  // Closes the client end.

只要在impl这里保持活动状态,它最终会收到Log消息,然后立即调用输出"Client disconnected! Purging log lines.". 与所有其他接收器回调一样,一旦其相应的接收器对象被销毁,断开连接处理程序将永远不会被调用。

使用base::Unretained是安全的,因为错误处理程序永远不会在receiver_和this拥有的生命周期之后被调用receiver_。

关于端点生命周期和回调的说明

一旦 amojo::Remote被销毁,就可以保证不会调用挂起的回调以及连接错误处理程序(如果已注册)。

一旦 amojo::Receiver被销毁,就可以保证不会再向实现分派更多的方法调用,并且不会调用连接错误处理程序(如果已注册)。

处理流程崩溃和回调的最佳实践

调用采用回调的 mojo 接口方法时的一种常见情况是调用者想知道另一个端点是否被拆除(例如,由于崩溃)。在这种情况下,消费者通常想知道响应回调是否不会运行。这个问题有不同的解决方案,这取决于它是如何Remote保持的:

  1. 消费者拥有Remote:set_disconnect_handler应该使用。
  2. 消费者不拥有Remote:根据调用者想要的行为,有两个助手。如果调用者想要确保运行错误处理程序,mojo::WrapCallbackWithDropHandler则应使用。如果调用者希望回调始终运行,mojo::WrapCallbackWithDefaultInvokeIfNotRun则应使用 helper。对于这两个助手,应遵循通常的回调注意事项,以确保回调不会在消费者被破坏后运行(例如,因为消费者的所有者比Remote消费者更长寿)。这包括使用base::WeakPtr或base::RefCounted。还应该注意的是,使用这些帮助程序,可以在重置或销毁 Remote 时同步运行回调。

关于订购的注意事项

如上一节所述,关闭管道的一端最终会在另一端触发连接错误。然而,重要的是要注意这个事件本身是相对于管道上的任何其他事件(例如写入消息)进行排序的。

这意味着编写一些人为的东西是安全的:

LoggerImpl::LoggerImpl(mojo::PendingReceiver<sample::mojom::Logger> receiver,
      base::OnceClosure disconnect_handler)
    : receiver_(this, std::move(receiver)) {
  receiver_.set_disconnect_handler(std::move(disconnect_handler));
}

void GoBindALogger(mojo::PendingReceiver<sample::mojom::Logger> receiver) {
  base::RunLoop loop;
  LoggerImpl impl(std::move(receiver), loop.QuitClosure());
  loop.Run();
}

void LogSomething() {
  mojo::Remote<sample::mojom::Logger> logger;
  bg_thread->task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&GoBindALogger, logger.BindNewPipeAndPassReceiver()));
  logger->Log("OK Computer");
}

当logger超出范围时,它会立即关闭消息管道的末端,但 impl 端在收到发送的Log消息之前不会注意到这一点。因此,impl上面将首先记录我们的消息,然后看到连接错误并跳出运行循环。

类型

枚举

Mojom 枚举直接转换为等效的强类型 C++11 枚举类,并int32_t作为底层类型。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:
  // Default constructor - applies default values, potentially ones specified
  // explicitly within the Mojom.
  Employee();

  // Value constructor - an explicit argument for every field in the struct, in
  // lexical Mojom definition order.
  Employee(int64_t id, const std::string& username, Department department);

  // Creates a new copy of this struct value
  EmployeePtr Clone();

  // Tests for equality with another struct value of the same type.
  bool Equals(const Employee& other);

  // Equivalent public fields with names identical to the Mojom.
  int64_t id;
  std::string username;
  Department department;
};

}  // namespace mojom
}  // namespace business

请注意,当用作消息参数或另一个 Mojom 结构中的字段时,struct类型由仅移动mojo::StructPtr助手包装,这大致相当于std::unique_ptr带有一些附加实用程序方法的 a。这允许结构值可以为空,并且结构类型可能是自引用的。

每个生成的 struct 类都有一个静态New()方法,该方法返回一个新的mojo::StructPtr包装类的新实例,该类是通过转发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 ~EmployeManager() {}

  virtual void AddEmployee(EmployeePtr e) = 0;
};

我们可以从 C++ 代码发送这条消息,如下所示:

mojom::EmployeManagerPtr manager = ...;
manager->AddEmployee(
    Employee::New(42, "mojo", mojom::Department::kEngineering));

// or
auto e = Employee::New(42, "mojo", mojom::Department::kEngineering);
manager->AddEmployee(std::move(e));

联合

structs类似,标记联合生成一个同名的、具有代表性的 C++ 类,该类通常包装在mojo::StructPtr.

与结构不同,所有生成的联合字段都是私有的,必须使用访问器进行检索和操作。字段foo可由 访问get_foo()和设置set_foo()。is_foo()每个字段还有一个布尔值,指示联合当前是否正在采用foo排除所有其他联合字段的字段值。

最后,每个生成的联合类还有一个嵌套的Tag枚举类,它枚举所有命名的联合字段。Mojom 联合值的当前类型可以通过调用which()返回 a的方法来确定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!

通过接口发送接口

我们知道如何创建接口管道并以一些有趣的方式使用它们的 Remote 和 PendingReceiver 端点。这仍然不能成为有趣的 IPC!Mojo IPC 的基本功能是在其他接口之间传输接口端点的能力,所以让我们看看如何实现这一点。

发送待处理的接收者
考虑一个新的示例 Mojom //sample/db.mojom:

module db.mojom;

interface Table {
  void AddRow(int32 key, string data);
};

interface Database {
  AddTable(pending_receiver<Table> table);
};

正如Mojom IDL 文档 中所指出的,pending_receiver

语法与PendingReceiver上面部分中讨论的类型完全对应,实际上这些接口的生成代码大约是:

namespace db {
namespace mojom {

class Table {
 public:
  virtual ~Table() {}

  virtual void AddRow(int32_t key, const std::string& data) = 0;
}

class Database {
 public:
  virtual ~Database() {}

  virtual void AddTable(mojo::PendingReceiver<Table> table);
};

}  // namespace mojom
}  // namespace db

我们现在可以通过Tableand的实现将所有这些放在一起Database:

#include "sample/db.mojom.h"

class TableImpl : public db::mojom:Table {
 public:
  explicit TableImpl(mojo::PendingReceiver<db::mojom::Table> receiver)
      : receiver_(this, std::move(receiver)) {}
  ~TableImpl() override {}

  // db::mojom::Table:
  void AddRow(int32_t key, const std::string& data) override {
    rows_.insert({key, data});
  }

 private:
  mojo::Receiver<db::mojom::Table> receiver_;
  std::map<int32_t, std::string> rows_;
};

class DatabaseImpl : public db::mojom::Database {
 public:
  explicit DatabaseImpl(mojo::PendingReceiver<db::mojom::Database> receiver)
      : receiver_(this, std::move(receiver)) {}
  ~DatabaseImpl() override {}

  // db::mojom::Database:
  void AddTable(mojo::PendingReceiver<db::mojom::Table> table) {
    tables_.emplace_back(std::make_unique<TableImpl>(std::move(table)));
  }

 private:
  mojo::Receiver<db::mojom::Database> receiver_;
  std::vector<std::unique_ptr<TableImpl>> tables_;
};

很简单。该pending_receiver

Mojom参数AddTable将转换为C ++ mojo::PendingReceiver db::mojom::Table,我们知道这仅仅是一个强类型的消息管道句柄。当DatabaseImpl接到AddTable电话时,它构造一个新的TableImpl并将其绑定到接收到的mojo::PendingReceiver db::mojom::Table.

让我们看看如何使用它。

mojo::Remote<db::mojom::Database> database;
DatabaseImpl db_impl(database.BindNewPipeAndPassReceiver());

mojo::Remote<db::mojom::Table> table1, table2;
database->AddTable(table1.BindNewPipeAndPassReceiver());
database->AddTable(table2.BindNewPipeAndPassReceiver());

table1->AddRow(1, "hiiiiiiii");
table2->AddRow(2, "heyyyyyy");

请注意,我们可以再次Table立即开始使用新管道,即使它们的mojo::PendingReceiverdb::mojom::Table端点仍在传输中。

发送遥控器
当然我们也可以发送Remotes:

interface TableListener {
  OnRowAdded(int32 key, string data);
};

interface Table {
  AddRow(int32 key, string data);

  AddListener(pending_remote<TableListener> listener);
};

这将生成如下Table::AddListener签名:

virtual void AddListener(mojo::PendingRemote<TableListener> listener) = 0;

这可以像这样使用:

mojo::PendingRemote<db::mojom::TableListener> listener;
TableListenerImpl impl(listener.InitWithNewPipeAndPassReceiver());
table->AddListener(std::move(listener));

其他接口绑定类型

接口上的常用绑定盖基本使用部分对象类型:Remote,PendingReceiver,和Receiver。虽然这些类型可能是实践中最常用的类型,但还有其他几种绑定客户端和实现端接口管道的方法。

自有接收器

甲自主接收机存在作为拥有其接口实现独立的对象,并自动当其绑定的接口的端点检测到错误清除自己的身体。该MakeSelfOwnedReceiver函数用于创建这样的接收器。.

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:
  // NOTE: This doesn't own any Receiver object!
};

mojo::Remote<db::mojom::Logger> logger;
mojo::MakeSelfOwnedReceiver(std::make_unique<LoggerImpl>(),
                        logger.BindNewPipeAndPassReceiver());

logger->Log("NOM NOM NOM MESSAGES");

现在只要logger系统中某处保持打开状态,LoggerImpl另一端的边界就会保持活动状态。

接收器组

有时与多个客户端共享单个实现实例很有用。ReceiverSet使这变得容易。考虑 Mojom:

module system.mojom;

interface Logger {
  Log(string message);
};

interface LoggerProvider {
  GetLogger(Logger& logger);
};

我们可以使用ReceiverSet将多个Logger挂起的接收器绑定到单个实现实例:

class LogManager : public system::mojom::LoggerProvider,
                   public system::mojom::Logger {
 public:
  explicit LogManager(mojo::PendingReceiver<system::mojom::LoggerProvider> receiver)
      : provider_receiver_(this, std::move(receiver)) {}
  ~LogManager() {}

  // system::mojom::LoggerProvider:
  void GetLogger(mojo::PendingReceiver<Logger> receiver) override {
    logger_receivers_.Add(this, std::move(receiver));
  }

  // system::mojom::Logger:
  void Log(const std::string& message) override {
    LOG(ERROR) << "[Logger] " << message;
  }

 private:
  mojo::Receiver<system::mojom::LoggerProvider> provider_receiver_;
  mojo::ReceiverSet<system::mojom::Logger> logger_receivers_;
};

远程设置

在类似ReceiverSet上述的,有时是非常有用的维护一组RemoteS表示例如一个客户观察某些事件的集合。RemoteSet是来帮忙的。服用魔力:

module db.mojom;

interface TableListener {
  OnRowAdded(int32 key, string data);
};

interface Table {
  AddRow(int32 key, string data);
  AddListener(pending_remote<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(mojo::PendingRemote<db::mojom::TableListener> listener) {
    listeners_.Add(std::move(listener));
  }

 private:
  mojo::RemoteSet<db::mojom::Table> listeners_;
  std::map<int32_t, std::string> rows_;
};

相关接口

关联的接口是以下接口:

允许在单个消息管道上运行多个接口,同时保留消息排序。
使接收方可以从多个序列访问单个消息管道。

Mojom

为远程/接收器字段引入了新类型pending_associated_remote和新类型pending_associated_receiver。例如:

interface Bar {};

struct Qux {
  pending_associated_remote<Bar> bar;
};

interface Foo {
  // Uses associated remote.
  PassBarRemote(pending_associated_remote<Bar> bar);
  // Uses associated receiver.
  PassBarReceiver(pending_associated_receiver<Bar> bar);
  // Passes a struct with associated interface pointer.
  PassQux(Qux qux);
  // Uses associated interface pointer in callback.
  AsyncGetBar() => (pending_associated_remote<Bar> bar);
};

在每种情况下,接口 impl/client 将使用相同的消息管道进行通信,关联的远程/接收器通过该管道传递。

在 C++ 中使用关联的接口

生成 C++ 绑定时,pending_associated_remoteBar被映射到mojo::PendingAssociatedRemote; 挂起_关联_接收器到mojo::PendingAssociatedReceiver.

// In mojom:
interface Foo {
  ...
  PassBarRemote(pending_associated_remote<Bar> bar);
  PassBarReceiver(pending_associated_receiver<Bar> bar);
  ...
};

// In C++:
class Foo {
  ...
  virtual void PassBarRemote(mojo::PendingAssociatedRemote<Bar> bar) = 0;
  virtual void PassBarReceiver(mojo::PendingAssociatedReceiver<Bar> bar) = 0;
  ...
};

传递挂起的关联接收器
假设您已经有一个Remote foo,并且您想调用PassBarReceiver()它。你可以做:

mojo::PendingAssociatedRemote<Bar> pending_bar;
mojo::PendingAssociatedReceiver<Bar> bar_receiver = pending_bar.InitWithNewEndpointAndPassReceiver();
foo->PassBarReceiver(std::move(bar_receiver));

mojo::AssociatedRemote<Bar> bar;
bar.Bind(std::move(pending_bar));
bar->DoSomething();

首先,代码创建一个类型为 的关联接口Bar。它看起来与设置非关联接口的操作非常相似。一个重要的区别是两个关联端点之一(bar_receiver或pending_bar)必须通过另一个接口发送。这就是接口与现有消息管道关联的方式。

需要注意的是bar->DoSomething(),通过之前不能调用bar_receiver。这是FIFO-ness保证所要求的:在接收方,当DoSomethingcall的消息到达时,我们希望AssociatedReceiver在处理任何后续消息之前将其分派给相应的消息。如果bar_receiver在后续消息中,则消息调度会陷入死锁。另一方面,一旦bar_receiver发送,bar就可以使用。无需等到bar_receiver绑定到远程端的实现。

AssociatedRemote提供了BindNewEndpointAndPassReceiver一种使代码更短的方法。以下代码实现了相同的目的:

mojo::AssociatedRemote<Bar> bar;
foo->PassBarReceiver(bar.BindNewEndpointAndPassReceiver());
bar->DoSomething();

的实现Foo如下所示:

class FooImpl : public Foo {
  ...
  void PassBarReceiver(mojo::AssociatedReceiver<Bar> bar) override {
    bar_receiver_.Bind(std::move(bar));
    ...
  }
  ...

  Receiver<Foo> foo_receiver_;
  AssociatedReceiver<Bar> bar_receiver_;
};

在此示例中,bar_receiver_的生命周期与 的生命周期相关FooImpl。但你不必这样做。例如,您可以传递bar2到另一个序列以绑定到AssociatedReceiver那里。

当底层消息管道断开连接(例如,foo或被foo_receiver_破坏)时,所有关联的接口端点(例如,bar和bar_receiver_)将收到断开连接错误。

传递关联的遥控器
同样,假设您已经有一个Remote foo,并且您想调用PassBarRemote()它。你可以做:

mojo::AssociatedReceiver<Bar> bar_receiver(some_bar_impl);
mojo::PendingAssociatedRemote<Bar> bar;
mojo::PendingAssociatedReceiver<Bar> bar_pending_receiver = bar.InitWithNewEndpointAndPassReceiver();
foo->PassBarRemote(std::move(bar));
bar_receiver.Bind(std::move(bar_pending_receiver));

以下代码实现了相同的目的:

mojo::AssociatedReceiver<Bar> bar_receiver(some_bar_impl);
mojo::PendingAssociatedRemote<Bar> bar;
bar_receiver.Bind(bar.InitWithNewPipeAndPassReceiver());
foo->PassBarRemote(std::move(bar));

性能注意事项

在与主序列(主接口所在的位置)不同的序列上使用关联接口时:

发送消息:发送直接发生在调用序列上。所以没有序列跳跃。
接收消息:关联接口绑定在与主接口不同的序列上,在调度期间会产生额外的序列跳跃。
因此,性能相关的接口更适合于消息接收发生在主序列上的场景。

测试

关联接口需要先与主接口关联后才能使用。这意味着关联接口的一端必须通过主接口的一端发送,或者通过另一个本身已经具有主接口的关联接口的一端发送。

如果要在不先关联的情况下测试关联的接口端点,可以使用AssociatedRemote::BindNewEndpointAndPassDedicatedReceiver. 这将创建工作关联的接口端点,这些端点实际上与其他任何东西都没有关联。

阅读更多

设计:Mojo 相关接口

同步调用

在决定使用同步调用之前仔细考虑

尽管同步调用很方便,但在非绝对必要时应避免使用它们:

同步调用会损害并行性,因此会损害性能。
重入会更改消息顺序并产生您在编码时可能从未想过的调用堆栈。这一直是一个巨大的痛苦。
同步调用可能会导致死锁。

Mojom 变化

为方法引入了一个新属性[Sync](或[Sync=true])。例如:

interface Foo {
  [Sync]
  SomeSyncCall() => (Bar result);
};

表示当SomeSyncCall()被调用时,调用线程的控制流被阻塞,直到收到响应。

不允许将此属性用于没有响应的函数。如果只需要等待服务端处理完调用,可以使用空的响应参数列表:

[Sync]
SomeSyncCallWithNoResult() => ();

生成的绑定 (C++)

上面Foo接口生成的C++接口为:

class Foo {
 public:
  // The service side implements this signature. The client side can
  // also use this signature if it wants to call the method asynchronously.
  virtual void SomeSyncCall(SomeSyncCallCallback callback) = 0;

  // The client side uses this signature to call the method synchronously.
  virtual bool SomeSyncCall(BarPtr* result);
};

如您所见,客户端和服务端使用不同的签名。在客户端,响应被映射到输出参数,布尔返回值指示操作是否成功。(返回 false 通常意味着发生了连接错误。)

在服务端,使用带有回调的签名。原因是在某些情况下,实现可能需要做一些异步工作,同步方法的结果取决于这些工作。

注意:您也可以在客户端使用带有回调的签名来异步调用该方法。

重入

在等待同步方法调用的响应时,调用线程会发生什么?它继续处理传入的同步请求消息(即同步方法调用);阻止其他消息,包括与正在进行的同步调用不匹配的异步消息和同步响应消息。
m0004

请注意,与正在进行的同步调用不匹配的同步响应消息无法重新输入。那是因为它们对应于调用堆栈中的同步调用。因此,它们需要在堆栈展开时排队和处理。

避免死锁

请注意,重入行为并不能防止涉及异步调用的死锁。您需要避免调用序列,例如:
m0005

阅读更多

设计提案:Mojo 同步方法

类型映射

在许多情况下,您可能更喜欢生成的 C++ 绑定使用更自然的类型来表示接口方法中的某些 Mojom 类型。例如,考虑一个 Mojom 结构,Rect如下所示:

module gfx.mojom;

struct Rect {
  int32 x;
  int32 y;
  int32 width;
  int32 height;
};

interface Canvas {
  void FillRect(Rect rect);
};

所述CanvasMojom界面通常会产生一个C ++接口等:

class Canvas {
 public:
  virtual void FillRect(RectPtr rect) = 0;
};

然而,Chromium 树已经定义了一个原生的gfx::Rect,它的含义是等价的,但也有有用的辅助方法。如果 Mojom 绑定生成器能够生成:而不是在每个消息边界手动在 agfx::Rect和 Mojom 生成之间进行转换RectPtr,岂不是很好:

class Canvas {
 public:
  virtual void FillRect(const gfx::Rect& rect) = 0;
}

正确答案是:“对!那样就好了!” 幸运的是,它可以!

定义 StructTraits

为了教生成的绑定代码如何将任意本机类型序列化为T任意 Mojom 类型mojom::U,我们需要定义mojo::StructTraits模板的适当特化。

一个有效的特化StructTraits必须定义以下静态方法:

Mojom 结构的每个字段的单个静态访问器,与结构字段的名称完全相同。这些访问器都必须接受(最好是 const)对本机类型对象的引用,并且必须返回与 Mojom 结构字段类型兼容的值。这用于在消息序列化期间安全且一致地从本机类型中提取数据,而不会产生额外的复制成本。

Read给定 Mojom 结构的序列化表示,初始化本机类型实例的单个静态方法。该Read方法必须返回 abool以指示传入的数据是被接受 ( true) 还是被拒绝 ( false)。

为了定义 的映射gfx::Rect,我们需要以下特化StructTraits,我们将在 中定义//ui/gfx/geometry/mojo/geometry_mojom_traits.h:

#include "mojo/public/cpp/bindings/mojom_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_mojom_traits.cc:

#include "ui/gfx/geometry/mojo/geometry_mojom_traits.h"

namespace mojo {

// static
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()方法返回。这在反序列化期间充当验证步骤:如果客户端发送具有负宽度或高度的 ,则其消息将被拒绝并且管道将关闭。通过这种方式,除了使调用站点和接口实现更加方便之外,类型映射还可以用于启用自定义验证逻辑。falsewidthheightgfx::Rect

当结构体字段具有非原始类型时,例如字符串或数组,建议在访问器中返回数据的只读视图以避免复制。这是安全的,因为输入对象保证比访问器方法返回的结果的使用时间更长。

以下示例用于StringPiece返回 GURL 数据的视图 ( //url/mojom/url_gurl_mojom_traits.h):

#include "base/strings/string_piece.h"
#include "url/gurl.h"
#include "url/mojom/url.mojom.h"
#include "url/url_constants.h"

namespace mojo {

template <>
struct StructTraits<url::mojom::UrlDataView, GURL> {
  static base::StringPiece url(const GURL& r) {
    if (r.possibly_invalid_spec().length() > url::kMaxURLChars ||
        !r.is_valid()) {
      return base::StringPiece();
    }
    return base::StringPiece(r.possibly_invalid_spec().c_str(),
                             r.possibly_invalid_spec().length());
  }
}  // namespace mojo

启用新类型映射

我们已经定义了StructTraits必要的,但我们仍然需要教绑定生成器(以及构建系统)关于映射。为此,我们必须向mojomGN 中的目标添加更多信息:

# Without a typemap
mojom("mojom") {
  sources = [
    "rect.mojom",
  ]
}

# With a typemap.
mojom("mojom") {
  sources = [
    "rect.mojom",
  ]

  cpp_typemaps = [
    {
      # NOTE: A single typemap entry can list multiple individual type mappings.
      # Each mapping assumes the same values for |traits_headers| etc below.
      #
      # To typemap a type with separate |traits_headers| etc, add a separate
      # entry to |cpp_typemaps|.
      types = [
        {
          mojom = "gfx.mojom.Rect"
          cpp = "::gfx::Rect"
        },
      ]
      traits_headers = [ "//ui/gfx/geometry/mojo/geometry_mojom_traits.h" ]
      traits_sources = [ "//ui/gfx/geometry/mojo/geometry_mojom_traits.cc" ]
      traits_public_deps = [ "//ui/gfx/geometry" ]
    },
  ]
}

有关上述定义和其他支持参数的详细信息,请参阅mojom.gni 中的typemap 文档。

有了这个额外的配置,任何 mojom 引用gfx.mojom.Rect(例如对于方法参数或结构字段)都将是gfx::Rect生成的 C++ 代码中的引用。

对于绑定的 Blink 变体,blink_cpp_typemaps改为添加到列表中。

没有类型映射traits_sources

使用traits_sources在一个类型映射配置装置所列出的来源将被直接烘焙到相应的mojom目标自身的来源。如果您想对 Blink 和非 Blink 绑定使用相同的类型映射,这可能会出现问题。

对于这种情况,建议您component为 typemap trait定义一个单独的目标,并traits_public_deps在 typemap 的 中引用它:

mojom("mojom") {
  sources = [
    "rect.mojom",
  ]

  cpp_typemaps = [
    {
      types = [
        {
          mojom = "gfx.mojom.Rect"
          cpp = "::gfx::Rect"
        },
      ]
      traits_headers = [ "//ui/gfx/geometry/mojo/geometry_mojom_traits.h" ]
      traits_public_deps = [ ":geometry_mojom_traits" ]
    },
  ]
}

component("geometry_mojom_traits") {
  sources = [
    "//ui/gfx/geometry/mojo/geometry_mojom_traits.cc",
    "//ui/gfx/geometry/mojo/geometry_mojom_traits.h",
  ]

  # The header of course needs corresponding COMPONENT_EXPORT() tags.
  defines = [ "IS_GEOMETRY_MOJOM_TRAITS_IMPL" ]
}

结构特征参考

特化的每个StructTraits静态 getter 方法——每个结构字段一个——必须返回一个类型,该类型可以在序列化期间用作该字段的数据源。这是一个快速参考,将 Mojom 字段类型映射到有效的 getter 返回类型:

Mojom 字段类型C++ Getter 返回类型
boolbool
int8int8_t
uint8uint8_t
int16int16_t
uint16uint16_t
int32int32_t
uint32uint32_t
int64int64_t
uint64uint64_t
floatfloat
doubledouble
handlemojo::ScopedHandle
handle<message_pipe>mojo::ScopedMessagePipeHandle
handle<data_pipe_consumer>mojo::ScopedDataPipeConsumerHandle
handle<data_pipe_producer>mojo::ScopedDataPipeProducerHandle
handle<shared_buffer>mojo::ScopedSharedBufferHandle
pending_remotemojo::PendingRemote
pending_receivermojo::PendingReceiver
pending_associated_remotemojo::PendingAssociatedRemote
pending_associated_receiver
string值或对定义T了mojo::StringTraits专门化的任何类型的引用。默认情况下,这包括std::string、base::StringPiece和WTF::String(blink)。
array<T>Value or reference to any type T that has a mojo::ArrayTraits specialization defined. By default this includes std::vector, mojo::CArray, and WTF::Vector (Blink).
map<K, V>Value or reference to any type T that has a mojo::MapTraits specialization defined. By default this includes std::map, mojo::unordered_map, and WTF::HashMap (Blink).
FooEnumValue of any type that has an appropriate EnumTraits specialization defined. By default this inlcudes only the generated FooEnum type.
FooStructValue or reference to any type that has an appropriate StructTraits specialization defined. By default this includes only the generated FooStructPtr type.
FooUnionValue of reference to any type that has an appropriate UnionTraits specialization defined. By default this includes only the generated FooUnionPtr type.
Foo?absl::optional, where CppType is the value type defined by the appropriate traits class specialization (e.g. StructTraits, mojo::MapTraits, etc.). This may be customized by the typemapping.

使用生成的 DataView 类型

专门化的静态Read方法StructTraits获得一个生成的FooDataView参数(如RectDataView上面的例子中的),它公开了传入消息内容中序列化 Mojom 结构的直接视图。为了使其尽可能易于使用,生成的FooDataView类型具有对应于每个结构字段的生成方法:

  • 对于 POD
    字段类型(例如bool、浮点数、整数),这些是名称与字段名称相同的简单访问器方法。因此,在Rect示例中,我们可以访问data.x()和data.width()。返回类型与上表中StructTraits Reference下列出的映射完全对应。

  • 对于句柄和接口类型(例如 handle或pending_remote),它们被命名TakeFieldName(对于名为
    的字段field_name)并且它们按值返回适当的仅移动句柄类型。返回类型与上表中StructTraits
    Reference下列出的映射完全对应。

  • 对于所有其他字段类型(例如,枚举、字符串、数组、映射、结构),它们被命名ReadFieldName(对于名为
    的字段field_name)并返回 a
    bool(指示读取成功或失败)。成功后,他们用反序列化的字段值填充输出参数。输出参数可以是指向任何类型的指针,该类型StructTraits定义了适当的专业化,如上表所述,在StructTraits
    Reference 下。

一个例子在这里很有用。假设我们引入了一个新的 Mojom 结构:

struct RectPair {
  Rect left;
  Rect right;
};

和相应的 C++ 类型:

class RectPair {
 public:
  RectPair() {}

  const gfx::Rect& left() const { return left_; }
  const gfx::Rect& right() const { return right_; }

  void Set(const gfx::Rect& left, const gfx::Rect& right) {
    left_ = left;
    right_ = right;
  }

  // ... some other stuff

 private:
  gfx::Rect left_;
  gfx::Rect right_;
};

我们要映射gfx::mojom::RectPair到的特征gfx::RectPair可能如下所示:

namespace mojo {

template <>
class StructTraits
 public:
  static const gfx::Rect& left(const gfx::RectPair& pair) {
    return pair.left();
  }

  static const gfx::Rect& right(const gfx::RectPair& pair) {
    return pair.right();
  }

  static bool Read(gfx::mojom::RectPairDataView data, gfx::RectPair* out_pair) {
    gfx::Rect left, right;
    if (!data.ReadLeft(&left) || !data.ReadRight(&right))
      return false;
    out_pair->Set(left, right);
    return true;
  }
}  // namespace mojo

生成的ReadFoo方法总是将multi_word_field_name字段转换为ReadMultiWordFieldName方法。

变体

现在您可能已经注意到,在处理 Mojom 时会生成额外的 C++ 源代码。这些存在是由于类型映射,我们在整个文档中引用的源文件(即foo.mojom.cc和foo.mojom.h)实际上只是给定 Mojom 文件的 C++ 绑定的一个变体(默认或铬变体)。

当前在树中定义的唯一其他变体是blink变体,它会生成一些附加文件:

out/gen/sample/db.mojom-blink.cc
out/gen/sample/db.mojom-blink.h

这些文件反映了默认变体中的定义,但使用不同的 C++ 类型代替某些内置字段和参数类型。例如,Mojom 字符串由WTF::String而不是 表示std::string。为了避免符号冲突,变体的符号嵌套在一个额外的内部命名空间中,因此接口的 Blink 使用者可能会编写如下内容:

#include "sample/db.mojom-blink.h"

class TableImpl : public db::mojom::blink::Table {
 public:
  void AddRow(int32_t key, const WTF::String& data) override {
    // ...
  }
};

除了对内置字符串、数组和映射使用不同的 C++ 类型外,应用于 Blink 绑定的自定义类型映射与常规绑定分开管理。

mojom目标blink_cpp_typemaps除了常规的cpp_typemaps. 这列出了应用于 Blink 绑定的类型映射。

要专门依赖生成的 Blink 绑定,请参考${target_name}_blink. 例如,使用定义:

# In //foo/mojom
mojom("mojom") {
  sources = [
    "db.mojom",
  ]
}

C++ 源可以通过依赖于 Blink 绑定来依赖"//foo/mojom:mojom_blink".

最后请注意,这两个绑定变体共享一些通用定义,这些定义不受类型映射配置差异的影响(如枚举和描述序列化对象格式的结构)。这些定义在共享源中生成:

out/gen/sample/db.mojom-shared.cc
out/gen/sample/db.mojom-shared.h
out/gen/sample/db.mojom-shared-internal.h

包括任何一个变体的标头 (db.mojom.h或db.mojom-blink.h) 都隐含地包括共享标头,但在某些情况下可能希望仅包括共享标头。

C++ 源可以仅依赖于共享源,通过引用"${target_name}_shared"目标,例如"//foo/mojom:mojom_shared"在上面的示例中。

版本注意事项

有关 Mojom IDL 中版本控制的一般文档,请参阅版本控制。

本节简要讨论与版本化 Mojom 类型相关的一些特定于 C++ 的注意事项。

查询接口版本

Remote 定义了以下方法来查询或断言远程接口版本:

void QueryVersion(base::OnceCallback<void(uint32_t)> callback);

这将查询远程端点以获取其绑定的版本号。当接收到响应时callback,使用远程版本号调用。请注意,此值由Remote实例缓存以避免冗余查询。

void RequireVersion(uint32_t version);

通知远程端点version客户端需要的最低版本。如果远程端点不支持该版本,它将立即关闭管道的末端,防止接收任何其他请求。

版本化枚举

所有可扩展的枚举都应该有一个使用[Default]属性指定为默认值的枚举器值。当 Mojo 反序列化当前版本的枚举定义中未定义的枚举值时,该值将透明地映射到[Default]枚举器值。实现可以使用此枚举器值的存在来正确处理版本偏差。

[Extensible]
enum Department {
  [Default] kUnknown,
  kSales,
  kDev,
  kResearch,
};

在 Chrome 中使用 Mojo 绑定

请参阅将旧版 Chrome IPC 转换为 Mojo。

附加文件

Calling Mojo From Blink:简要概述在 Blink 代码中使用 Mojom C++ 绑定的方式。

其他参考

Chromium Mojo & IPC

Intro to Mojo & Services

Servicifying Chromium Features

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值