Chromium Mojo C++ Bindings API中文

概述

本文档为binding API的用法提供详细的指引,并附有示例代码段。详细的API文献请参考//mojo/public/cpp/bindings的头文件。
针对 Chromium开发者的简化指引,请看此链接

起航

当Mojom IDL文件被bindings生成器处理时,c++代码产出了根据.mojom文件命名的一系列.h和.cc文件。假设我们在//services/db/public/mojom/db.mojom创建了以下Mojom文件:

module db.mojom;

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

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

并在==//services/db/public/mojom/BUILD.gn==有一个生成bindings的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++ bindings有关。其中的两个:

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 {
  // ...
};

本文档为C++开发覆盖了由Mojom IDL生成的各种不同的定义,并且他们如何有效的用于消息管道的通信。

接口

Mojom IDL接口被翻译成相应的C++(纯虚)类接口。在内部也有为消息的序列化和反序列化而生成的代码,不过详情是对bindings的消费者隐藏的。

基本用法

让我们来看一个新的//sample/logger.mojom,它定义了client用于log简单字符串消息的一个简单logging接口:

module sample.mojom;

interface Logger {
  Log(string message);
};

bindings生成器将会生产一个有如下定义的logger.mojom.h(隐藏了不重要的细节):

namespace sample {
namespace mojom {

class Logger {
  virtual ~Logger() {}

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

}  // namespace mojom
}  // namespace sample

Remote and PendingReceiver

在Mojo bindings库中,这些是有效的强类型消息管道端点。如果一个Remote< T>绑定了消息管道的一个端点,那么它能够通过解引用调用T的接口。这些调用立即序列化它们的参数(通过生成的代码),并将相关消息写入管道中。
PendingReceiver< T>一个持有Remote< T>管道另一端的类型容器–也就是接收端–它将被路由到一些绑定它的实现中去。PendingReceiver< T>除了持有管道的一端和一些有效的编译时类型信息外,不会做任何事情。

在这里插入图片描述


所以我们怎么创建一个强类型的消息管道呢?

创建接口管道

一个创建接口管道的办法就是通过手动地创建一个管道,然后将每一端用一个强类型对象封装:

#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++ Bindings库提供了一个更方便的办法来完成这件事。remote.h定义了一个BindNewPipeAndPassReceive方法:

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

第二个代码段和第一个是完全相同的。

注意:在第一个例子中你可能注意到mojo::PendingRemote< Logger>的使用。它和PendingReceiver< T>是一样的,都只持有一个管道handle,并且不在管道做实际的读写。这个类型和PendingReceiver< T>一样是安全的,当从一个序列移动到另一个序列时。一个绑定的Remote< T>被绑定于一个单独序列。
Remote< T>可能通过Unbind()方法来解除绑定,这个方法还返回一个新的PendingRemote< T>。反之,Remote< T>也可能绑定(并拥有所有权)PendingRemote< T>,这样就能在管道中进行接口调用。
==Remote< T>序列-绑定的特性,对于安全地分发消息响应和连接错误提醒是非常有必要的。

一旦PendingRemote< T>被绑定,我们可以立即开始调用Logger接口方法,它将会立刻将消息写入管道。这些消息将会在管道接收端排好序,直到接收端被绑定并且开始读消息。

logger->Log("Hello!");

这实际上写了一则Log消息给管道。

在这里插入图片描述


如上所述,PendingReceiver实际没有做任何事情,所以消息将会一直保留在管道中。我们需要一个办法来将消息从管道的另一端读出来并且分发它们。我们必须绑定pending receiver

绑定一个PendingReceiver

在bindings库中有非常多不同的helper类用来绑定消息管道的接收端。它们中最原始的是mojo::Receiver< T>mojo::Receiver< T>T的实现和一个单一绑定消息管道端点连接起来(通过PendingRemote< T>),它将持续监控管道的可读状态。
任何时候只要被绑定的管道变成可读状态,Receiver将会调度一个task来读、反序列化(运用生成的代码)、将所有可用的消息分发给绑定的T的实现。以下是Logger接口的一个简单实现。需要注意的是,实现自身拥有mojo::Receiver。这是一个通用的模板,因为一个绑定的实现生命必须比绑定在它上面的mojo::Receiver长。

#include "base/logging.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(const Logger&) = delete;
  Logger& operator=(const Logger&) = delete;
  ~Logger() override {}

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

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

现在我们通过我们的LoggerImpl构造一个LoggerImpl,之前被排列好的Log消息将会被立即分发给LoggerImpl的序列:

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

以下的示意图说明了事件的顺序,都是由上述代码行启动的:

  1. LoggerImpl构造器被调用,将PendingReceiver< Logger>传递给Receiver
  2. Receiver拥有PendingReceiver< Logger>管道的端点,开始监控它是否可读。当管道是可读的,立即调度一个task来读管道中挂起的Log消息。
  3. Log消息被读取并且反序列化,造成Receiver触发它绑定的LoggerImplLogger::Log方法。

    在这里插入图片描述


    我们的implementation通过LOG(ERROR)最终log了client的"Hello!"消息。

注意:管道的消息被读取和分发,只在绑定它的对象(也就是以上的例子中的mojo::Receiver)活着的时候进行。

接收响应

一些Mojom 接口方法期望得到一个响应。假设我们修改Logger接口,最后一行log就可以这样获得:

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

所有client和接口的implementation都使用GetTail方法相同的签名:implementation用callback参数来响应请求,clients传递callback参数来异步接收响应。传递给GetTailGetTailCallback参数是有序的。它必须在GetTail被调用的相同序列中触发。client的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_;
};

更新之后的client调用:

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

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

在幕后,implementation侧的callback实际上序列化了响应的参数,并且将它们写入管道回传给client。与此同时,client侧的callback被某个内部逻辑触发,这个逻辑监控传入的响应消息,一旦消息到达就读取它并反序列化,并用反序列化的参数触发callback。

连接错误

如果一个管道断开连接,两个端点都将观察到连接错误(除非是关闭/销毁一个端点造成的断开连接,端点将不会收到这样的提示)。如果在断开连接的时候端点还保留有消息未读,连接错误将不会被触发直到消息排出。
管道断开连接可能是这样造成的:

  • Mojo系统层造成的:进程终端,资源耗尽等
  • 因为处理收到的消息时有验证错误,bindings关闭了管道
  • 另一侧的端点被关闭了。比如,remote侧绑定了一个mojo::Remote< T>,它被销毁了。
    当连接错误出现在了一个接收端点,这个端点的disconnect handler(如果设置了)被触发。这个handle是一个简单的base::OnceClosure,它在端点绑定的相同管道只被触发一次。
    通常clients和implementations使用这个handle去做一些清理操作–特别是当这个错误是不可预料的–创建一个新的管道,尝试建立一个新的连接。
    所有消息管道绑定的C++对象(比如mojo::Receiver< T>,mojo::Remote< T>等)支持通过set_disconnect_handler方法设置断开连接的handler。
    我们做另一个端到端的Logger例子来展示断开连接handler调用。假设LoggerImpl已经在构造器中设置了如下断开连接handler:
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消息。像所有其他接收端的callbacks一样,一个断开连接handler将不会被触发,当它相关联的接收端对象已经被销毁了。
base::Unretained的使用是安全的,因为错误handler将不会超越receiver_的生命周期被触发,并且this拥有receiver_

关于端点的生命周期和Callbacks

一旦mojo::Remote< T>被销毁,挂起的callbacks和连接错误handler(如果有注册)将不会被调用。
一旦mojo::Receiver< T>被销毁,不会再有方法调用分发给implementation,而且连接错误handler(如果有注册)将不会被调用。

处理运行中crash和callback的最佳实践

一个通常的情况是:当调用含有回调的mojo接口的时候,调用者想要知道是否另一端已经被销毁(比如因为crash)。在这种情况下,消费者通常想知道是否响应的回调不会被运行。解决这个问题有不同的办法,取决于==Remote< T>==是如何被持有的:

  1. 消费者拥有Remote< T>:使用了set_disconnect_handler
  2. 消费者不拥有Remote< T>:根据调用者的行为这里有两种办法。如果调用者想要确保错误handler被运行,那么可以使用mojo::WrapCallbackWithDropHandler来解决。如果调用者想要回调运行,那么可以使用mojo::WrapCallbackWithDefaultInvokeIfNotRun来解决。不管是哪种方法,都应该遵循回调注意事项,以确保在消费者被销毁后callbacks不会运行(因为==Remote< T>==的拥有者比消费者生命更长)。值得一提的是,在Remote被重置或者销毁的时候,callbacks也将同步运行。

关于排序

如前所述,关闭管道的一段将在另一端触发连接错误。然而需要注意的是,这个事件本身是相对于管道中的其他事件来排序的。
这意味着可以安全地写一些人为的东西:

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超出scope,它将立刻关掉消息管道中它这一端,但是impl侧将不会被提示,直到它接收到发送的Log消息。因此上述impl将会先log我们的消息,然后看到一个连接错误,然后退出run loop。

类型

Enums

Mojom enums直接转换成等价的强类型C++11 enum类,其底层类型为int32_t。Mojom和C++之间的类型名和值名是相同的。Mojo同时定义了一个特殊的enumerator kMaxValue,它共享最大enumerator的值:这使得以直方图形式记录Mojo enums并与遗留的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

Structs

Mojom structs用于将字段的逻辑分组定义为一个新的复合类型。每个Mojom struct都会生成一个具有相同名称的代表性C++类,其中包含相应C++类型的具有相同名称的public字段和几个有用的public方法。
举个例子,思考以下Mojom struct:

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中的字段时,struct类型由move-only的mojo::StructPtr helper封装,它大致相当于带有一些附加实用程序方法的std::unique_ptr。这允许struct值可以为空并且struct类型可能是自引用的。
每个生成的结构类都有一个静态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 ~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));

Unions

与struct类似,带标记的unions生成一个具有相同名称的代表性C++类,该类通常封装在mojo::StructPtr< T>中。
与struct不同,所有生成的union字段都是私有的,必须使用访问器进行检索和操作。字段foo可以通过get_foo()访问,并且可以通过set_foo()设置。每个字段还有一个布尔值is_foo(),它指示union当前是否采用字段foo的值以排除所有其他union字段。
最后,每个生成的union类还有一个嵌套的Tag enum类,它枚举所有命名的union字段。Mojom union值的当前类型可以通过调用返回Tag的 which() 方法来确定。
举个例子,思考以下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::NewIntValue(42);
CHECK(value->is_int_value());
CHECK_EQ(value->which(), Value::Tag::kIntValue);

value->set_float_value(42);
CHECK(value->is_float_value());
CHECK_EQ(value->which(), Value::Tag::kFloatValue);

value->set_string_value("bananas");
CHECK(value->is_string_value());
CHECK_EQ(value->which(), Value::Tag::kStringValue);

最后,请注意,如果union值当前未被给定字段占用,则尝试访问该字段将 DCHECK:

ValuePtr value = Value::NewIntValue(42);
LOG(INFO) << "Value is " << value->string_value();  // DCHECK!

通过接口发送接口

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

发送挂起的Receivers
考虑==//sample/db.mojom==中的新Mojom例子:

module db.mojom;

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

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

正如 Mojom IDL 文档中所指出的,pending_receiver< Table>语法与上面部分讨论的PendingReceiver< T>类型精确对应,实际上为这些接口生成的代码大致为:

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

我们现在可以将所有这些与TableDatabase的实现放在一起:

#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_;
};

很简单。AddTable的 Mojom 参数pending_receiver< Table>转换为一个C++的mojo::PendingReceiver< db::mojom::Table>,我们知道它只是一个强类型的消息管道handler。当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::PendingReceiver< db::mojom::Table>端点仍在传输中。

发送Remotes
当然我们也能发送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));

其他接口绑定类型

上面的接口部分涵盖了最常见的绑定对象类型:RemotePendingReceiver,以及Receiver的基本用法。虽然这些类型可能是实践中最常用的,但还有其他几种绑定client端和implementation端接口管道的方法。

Self-owned Receivers

Self-owned Receivers作为单例对象存在,该对象拥有其接口实现,并在其绑定的接口端点检测到错误时自动清理自己。 MakeSelfOwnedReceiver 函数用于创建这样的Receiver。。

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将保持活动状态。

Receiver Sets

有时与多个客户端共享单个实现实例很有用。ReceiverSet 使这很容易。思考一下这样的Mojom:

module system.mojom;

interface Logger {
  Log(string message);
};

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

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

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_;
};

Remote Sets

与上面的ReceiverSet类似,有时维护一组Remotes很有用,例如对于一组观察某个事件的client。RemoteSet 可以为您提供帮助。 Mojom为例:

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_;
};

Associated接口

关联接口是有如下特性的接口:

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

Mojom

为remote/receiver字段引入了新类型pending_associated_remotepending_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 将使用相同的消息管道进行通信,关联的remote/receiver通过该消息管道传递。

在 C++ 中使用Associated接口

生成C++绑定时,Bar的pending_associated_remote映射到mojo::PendingAssociatedRemote< Bar>; pending_associated_receiver映射到 mojo::PendingAssociatedReceiver< Bar>

// 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;
  ...
};

传递pending associated receivers
假设你已经有一个 Remote< Foo> 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_receiverpending_bar)必须通过另一个接口发送。这就是接口与现有消息管道相关联的方式。

需要注意的是,在传递bar_receiver之前不能调用bar->DoSomething()。这是 FIFO-ness 保证所要求的:在接收方,当DoSomething调用的消息到达时,我们希望在处理任何后续消息之前将其分派给相应的 AssociatedReceiver< Bar>。 如果bar_receiver在后续消息中,则消息调度会陷入死锁。另一方面,一旦发送bar_receiverbar 就可以使用。无需等到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< Bar>
当底层消息管道断开连接(例如,foo或foo_receiver_被破坏)时,所有关联的接口端点(例如,barbar_receiver_)将收到断开连接错误。

传递pending associated remotes
同样,假设您已经有一个Remote< Foo> 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。这将创建有效的关联接口端点,这些端点实际上不与其他任何东西相关联。

更多详情

同步调用

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

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

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

Mojom changes

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

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

这表示调用==SomeSyncCall()==时,调用线程的控制流被阻塞,直到收到响应为止。
不允许将此属性与没有响应的函数一起使用。如果需要等到服务端处理完调用,可以使用一个空的响应参数列表:

[Sync]
SomeSyncCallWithNoResult() => ();

生成的bindings (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);
};

如你所见,客户端和服务端使用不同的签名。在客户端,response映射到输出参数,boolean返回值表示操作是否成功。 (返回 false 通常意味着发生了连接错误。)
在服务端,使用带有回调的签名。原因是在某些情况下,实现可能需要做一些同步方法的结果所依赖的异步工作。

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

Re-entrancy

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

在这里插入图片描述


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

避免死锁

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

在这里插入图片描述

更多详情

Design Proposal: Mojo Sync Methods

类型映射

版本考量