概述
本文档为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));
以下的示意图说明了事件的顺序,都是由上述代码行启动的:
- LoggerImpl构造器被调用,将PendingReceiver< Logger>传递给Receiver。
- Receiver拥有PendingReceiver< Logger>管道的端点,开始监控它是否可读。当管道是可读的,立即调度一个task来读管道中挂起的Log消息。
-
Log消息被读取并且反序列化,造成Receiver触发它绑定的LoggerImpl的Logger::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参数来异步接收响应。传递给GetTail的GetTailCallback参数是有序的。它必须在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>==是如何被持有的:
- 消费者拥有Remote< T>:使用了set_disconnect_handler。
- 消费者不拥有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
我们现在可以将所有这些与Table和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_;
};
很简单。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));
其他接口绑定类型
上面的接口部分涵盖了最常见的绑定对象类型:Remote,PendingReceiver,以及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_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 将使用相同的消息管道进行通信,关联的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_receiver或pending_bar)必须通过另一个接口发送。这就是接口与现有消息管道相关联的方式。
需要注意的是,在传递bar_receiver之前不能调用bar->DoSomething()。这是 FIFO-ness 保证所要求的:在接收方,当DoSomething调用的消息到达时,我们希望在处理任何后续消息之前将其分派给相应的 AssociatedReceiver< Bar>。 如果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< Bar>。
当底层消息管道断开连接(例如,foo或foo_receiver_被破坏)时,所有关联的接口端点(例如,bar和 bar_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