Mojo C ++绑定API(转译)
本文档是Mojo文档的子集
原文路径:https://chromium.googlesource.com/chromium/src.git/+/master/mojo/public/cpp/bindings/README.md
Contents
总览
Mojo C ++绑定API利用C ++ System API提供了一组更自然的原语,用于通过Mojo消息管道进行通信。 结合Mojom IDL和绑定生成器生成的代码,用户可以轻松跨越任意内部和进程间边界连接接口客户端和实现。
本文档提供了有关使用示例代码段绑定API的详细指南。 有关详细的API参考,请查阅// mojo / public / cpp / bindings中的标题。
有关面向Chromium开发人员的简化指南,请参见此链接。
入门
当绑定生成器处理Mojom IDL文件时,C ++代码在一系列.h和.cc文件中发出,其名称基于输入的.mojom文件。 假设我们在//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中生成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
远程和待定接收器
在Mojo绑定库的世界中,这些实际上是强类型的消息管道端点。 如果将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 ++绑定库提供了一种更方便的方法来完成同一件事。 remote.h定义了BindNewPipeAndPassReceiver方法:
mojo::Remote<sample::mojom::Logger> logger; auto receiver = logger.BindNewPipeAndPassReceiver());
第二个片段与第一个片段功能相同。
注意:在上面的第一个示例中,您可能会注意到使用了mojo :: PendingRemote <Logger>。 这与PendingReceiver <T>相似,因为它仅保持在管道句柄上,实际上无法在管道上读取或写入消息。 此类型和PendingReceiver <T>都可以安全地在序列之间自由移动,而绑定的Remote <T>绑定到单个序列。
可以通过调用其Unbind()方法来解除对Remote <T>的绑定,该方法将返回一个新的PendingRemote <T>。 相反,Remote <T>可以绑定PendingRemote <T>(并因此拥有所有权),以便可以在管道上进行接口调用。
Remote <T>的顺序绑定性质对于支持安全地发送其消息响应和连接错误通知是必需的。
一旦绑定了PendingRemote <Logger>,我们可以立即开始在其上调用Logger接口方法,该方法将立即将消息写入管道。 这些消息将在管道的接收端排队,直到有人绑定到它并开始读取它们为止。
logger->Log("Hello!");
这实际上在管道中写入了Log消息。
但是如上所述,PendingReceiver实际上不执行任何操作,因此该消息将永远存在于管道中。 我们需要一种从管道的另一端读取消息并进行分发的方法。 我们必须绑定待处理的接收者。
绑定待处理的接收者
绑定库中有许多不同的帮助程序类,用于绑定消息管道的接收端。 其中最原始的是mojo :: Receiver <T>。 mojo :: Receiver <T>通过单个绑定的消息管道端点(通过mojo :: PendingReceiver <T>)桥接T的实现,它会持续监视其可读性。
每当绑定管道可读时,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 {} // sample::mojom::Logger: void Log(const std::string& message) override { LOG(ERROR) << "[Logger] " << message; } private: mojo::Receiver<sample::mojom::Logger> receiver_; DISALLOW_COPY_AND_ASSIGN(LoggerImpl); };
现在我们可以在我们的PendingReceiver <Logger>上构造一个LoggerImpl,并且先前排队的Log消息将按照LoggerImpl的序列尽快分派:
LoggerImpl impl(std::move(receiver));
下图说明了以下事件序列,所有这些事件都是由以上代码行设置的:
- 调用LoggerImpl构造函数,并将PendingReceiver <Logger>一起传递给Receiver。
- 他的接收者拥有PendingReceiver <Logger>的管道端点的所有权,并开始查看其可读性。 该管道可立即读取,因此安排了一个任务来尽快从管道读取未决的Log消息
- 日志消息被读取并反序列化,导致接收方在其绑定的LoggerImpl上调用Logger :: Log实现。
结果,我们的实现最终将记录客户的“ Hello!”。 消息通过LOG(ERROR)。
注意:仅在绑定消息的对象(即上例中的mojo :: Receiver)保持活动的情况下,才会从管道读取和发送消息。
收到回应
某些Mojom接口方法需要响应。 假设我们修改了Logger接口,这样可以查询最后一个记录的行:
module sample.mojom; interface Logger { Log(string message); GetTail() => (string message); };
The generated C++ interface will now look like:
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方法使用相同的签名:实现使用回调参数来响应请求,而客户端则传递回调参数以异步接收响应。 客户端的回调以与调用GetTail相同的顺序运行(与记录器绑定的顺序)。 这是更新的实现:
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 {} // 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_; DISALLOW_COPY_AND_ASSIGN(LoggerImpl); };
And an updated client call:
void OnGetTail(const std::string& message) { LOG(ERROR) << "Tail was: " << message; } logger->GetTail(base::BindOnce(&OnGetTail));
在后台,实现回调实际上是对响应参数进行序列化并将其写入管道中,以传递回客户端。 同时,客户端回调由一些内部逻辑调用,该逻辑监视管道是否有传入的响应消息,一旦到达就读取并反序列化该消息,然后使用反序列化的参数调用该回调。
连接错误
如果管道断开连接,则两个端点都将能够观察到连接错误(除非断开是由于关闭/销毁端点引起的,在这种情况下端点将不会收到此类通知)。 如果断开连接时还有剩余的传入消息要发送给端点,则在消息耗尽之前不会触发连接错误。
管道断开可能是由于:
- Mojo系统级原因:进程终止,资源耗尽等。
- 在处理收到的消息时,由于验证错误,绑定关闭了管道。
- 对等端点已关闭。 例如,远程端是绑定的mojo :: Remote <T>并被销毁.
无论是什么根本原因,当在接收方端点上遇到连接错误时,都会调用该端点的断开处理程序(如果已设置)。 该处理程序是一个简单的base :: OnceClosure,仅在端点绑定到同一管道时才可以调用。 通常,客户端和实现使用此处理程序进行某种清理,或者-特别是如果错误是意外的-创建新管道并尝试与其建立新连接。
所有消息管道绑定C ++对象(例如mojo :: Receiver <T>,mojo :: Remote <T>等)都支持通过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消息,然后立即调用绑定的回调,该回调将输出“客户端已断开连接!清除日志行”。 像所有其他接收者回调一样,一旦其对应的接收者对象被销毁,将永远不会调用断开处理程序。
使用base :: Unretained是安全的,因为错误处理程序永远不会在receiver_的生存期之外被调用,并且拥有拥有者Receiver_。
有关端点生命期和回调的注意事项
一旦mojo :: Remote <T>被销毁,可以确保不会调用挂起的回调以及连接错误处理程序(如果已注册)。
一旦mojo :: Receiver <T>被销毁,可以确保不再将任何方法调用分配给实现,并且不会调用连接错误处理程序(如果已注册)。
处理进程崩溃和回调的最佳实践
调用带有回调的mojo接口方法时,常见的情况是调用方想知道另一个端点是否已拆除(例如,由于崩溃)。 在那种情况下,使用者通常想知道是否不会运行响应回调。 对于此问题有不同的解决方案,具体取决于如何保存Remote <T>:
- 使用者拥有Remote <T>:应该使用set_disconnect_handler。
- 使用者不拥有Remote <T>:根据调用者想要的行为,有两个助手。 如果调用方要确保运行错误处理程序,则应使用mojo :: WrapCallbackWithDropHandler。 如果调用者希望回调始终运行,则应使用mojo :: WrapCallbackWithDefaultInvokeIfNotRun帮助器。 使用这两个帮助程序时,应遵循常规的回调注意事项,以确保在销毁使用者后销毁回调不会运行(例如,因为Remote <T>的所有者超过了使用者)。 这包括使用base :: WeakPtr或base :: RefCounted。 还应注意,使用这些帮助器,可以在重置或销毁远程服务器时同步运行回调。
关于订购的注意事项
如上一节所述,关闭管道的一端最终将在另一端触发连接错误。 但是,请务必注意,此事件本身是相对于管道上的任何其他事件(例如,编写消息)进行排序的。
这意味着编写类似以下内容的内容是安全的:
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超出范围时,它将立即关闭其消息管道的末端,但是在收到已发送的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结构中的字段时,结构类型由仅移动的mojo :: StructPtr帮助程序包装,该辅助程序大致等效于std :: unique_ptr和一些其他实用程序方法。 这允许结构值可以为空,并且结构类型可以是潜在的自引用。
每个生成的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));
共用体
与结构类似,带标记的联合会生成一个名称相同的代表性C ++类,该类通常包装在mojo :: StructPtr <T>中。
与结构不同,所有生成的联合字段都是私有的,必须使用访问器进行检索和操作。 字段foo可通过get_foo()访问,并可通过set_foo()设置。 每个字段还有一个布尔值is_foo(),指示联合是否当前正在采用字段foo的值,但排除在所有其他联合字段之外。
最后,每个生成的联合类还具有一个嵌套的Tag枚举类,该类枚举所有已命名的联合字段。 Mojom联合值的当前类型可以通过调用返回一个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; };
And we can use it like so:
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的主要功能是能够跨其他接口传输接口端点,因此让我们看一下如何实现这一点。
发送待处理的收件人
参照在//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_; };
我们现在可以通过Table和DatabPretty的简单实现将所有内容整合在一起。 AddTable的pending_receiver <Table> Mojom参数转换为C ++ mojo :: PendingReceiver <db :: mojom :: Table>,我们知道这只是一个强类型的消息管道句柄。 当DatabaseImpl收到一个AddTable调用时,它将构造一个新的TableImpl并将其绑定到接收到的mojo :: PendingReceiver <db :: mojom :: Table> .ase:
让我们看看如何使用它。
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");
请注意,即使它们的mojo :: PendingReceiver <db :: mojom :: Table>端点仍在传输中,我们也可以立即立即开始使用新的Table管道。
发送远端消息
当然我们也可以发送远端消息,例如:
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));
其他接口绑定类型
上面的``接口''部分介绍了最常见的绑定对象类型的基本用法:远程,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在这里为您提供帮助。 以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_; };
相关接口
关联的接口是以下接口:
- 允许在单个消息管道上运行多个接口,同时保留消息顺序。
- 使接收者可以从多个序列访问单个消息管道。
Mojom
为远程/接收器字段引入了一个新的关键字关联。 例如:
interface Bar {}; struct Qux { pending_associated_remote<Bar> bar3; }; interface Foo { // Uses associated remote. SetBar(pending_associated_remote<Bar> bar1); // Uses associated receiver. GetBar(pending_associated_receiver<Bar> bar2); // Passes a struct with associated interface pointer. PassQux(Qux qux); // Uses associated interface pointer in callback. AsyncGetBar() => (pending_associated_remote<Bar> bar4); };
这意味着接口impl /客户端将使用与关联的远程/接收器相同的消息管道进行通信。
在C ++中使用关联的接口
当生成C ++绑定时,Bar的前处理的_associated_remote映射到mojo :: PendingAssociatedRemote <Bar>; 即将到mojo :: PendingAssociatedReceiver <Bar>的未决_关联的接收器。
// In mojom: interface Foo { ... SetBar(pending_associated_remote<Bar> bar1); GetBar(pending_associated_receiver<Bar> bar2); ... }; // In C++: class Foo { ... virtual void SetBar(mojo::PendingAssociatedRemote<Bar> bar1) = 0; virtual void GetBar(mojo::PendingAssociatedReceiver<Bar> bar2) = 0; ... };
传递挂起的关联接收者
假设您已经有一个Remote <Foo> foo,并且您想在其上调用GetBar()。 你可以做:
mojo::PendingAssociatedRemote<Bar> pending_bar; mojo::PendingAssociatedReceiver<Bar> bar_receiver = pending_bar.InitWithNewEndpointAndPassReceiver(); foo->GetBar(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保证的要求:在接收方,当DoSomething调用的消息到达时,我们希望在处理任何后续消息之前将其分派到相应的AssociatedReceiver <Bar>。 如果bar_receiver在后续消息中,则消息调度将陷入死锁。 另一方面,一旦发送了bar_receiver,bar就可以使用了。 无需等待bar_receiver绑定到远程端的实现即可。
AssociatedRemote提供了一种BindNewEndpointAndPassReceiver方法来使代码短一些。 以下代码实现了相同的目的:
mojo::AssociatedRemote<Bar> bar; foo->GetBar(bar.BindNewEndpointAndPassReceiver()); bar->DoSomething();
Foo的实现如下所示:
class FooImpl : public Foo { ... void GetBar(mojo::AssociatedReceiver<Bar> bar2) override { bar_receiver_.Bind(std::move(bar2)); ... } ... Receiver<Foo> foo_receiver_; AssociatedReceiver<Bar> bar_receiver_; };
在此示例中,bar_receiver_的寿命与FooImpl的寿命相关。 但是您不必这样做。 例如,您可以将bar2传递到另一个序列以绑定到那里的AssociatedReceiver <Bar>。
当基础消息管道断开连接时(例如foo或foo_receiver_被销毁),所有关联的接口端点(例如bar和bar_receiver_)将收到断开连接错误。
传递关联的远程器
同样,假设您已经有一个Remote <Foo> foo,并且您想在其上调用SetBar()。 你可以做:
mojo::AssociatedReceiver<Bar> bar_receiver(some_bar_impl); mojo::PendingAssociatedRemote<Bar> bar; mojo::PendingAssociatedReceiver<Bar> bar_pending_receiver = bar.InitWithNewEndpointAndPassReceiver(); foo->SetBar(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->SetBar(std::move(bar));
性能考量
在与主序列(主接口所在的位置)不同的序列上使用关联的接口时:
- 发送消息:直接在呼叫序列上进行发送。 因此没有序列跳变。
- 接收消息:与主接口绑定在不同序列上的关联接口在分发期间会产生额外的时间戳。
因此,与性能相关的接口更适合于在主序列上发生消息接收的情况。
测试
关联的接口需要先与主接口关联,然后才能使用。 这意味着必须在主接口的一端或在本身已经具有主接口的另一个相关接口的一端上发送关联接口的一端。
如果要在不首先关联的情况下测试关联的接口端点,则可以使用AssociatedRemote :: BindNewEndpointAndPassDedicatedReceiverForTesting。 这将创建有效的关联接口端点,这些端点实际上并未与其他任何关联。
阅读更多
同步通话
在决定使用同步通话之前,请仔细考虑
尽管同步调用很方便,但在并非绝对必要时应避免使用它们:
- 同步调用损害了并行性,因此损害了性能
- 重载会更改消息顺序并产生您在编码时可能从未想到的调用堆栈。 一直以来都是一个巨大的痛苦。
- 同步呼叫可能会导致死锁。
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通常表示发生了连接错误。)
在服务端,使用带有回调的签名。 原因是在某些情况下,实现可能需要执行同步方法的结果所依赖的一些异步工作。
注意:您还可以在客户端使用带有回调的签名来异步调用该方法。
重载
等待同步方法调用的响应时,调用线程上会发生什么? 它继续处理传入的同步请求消息(即同步方法调用); 阻止其他消息,包括与正在进行的同步呼叫不匹配的异步消息和同步响应消息。
请注意,与正在进行的同步通话不匹配的同步响应消息无法重新输入。 那是因为它们对应于调用堆栈中向下同步的调用。 因此,在堆栈展开时需要将它们排入队列并进行处理。
避免死锁
请注意,重新进入行为无法防止涉及异步调用的死锁。 您需要避免调用序列,例如:
阅读更多
类型映射
在许多情况下,您可能希望生成的C ++绑定使用更自然的类型来表示接口方法中的某些Mojom类型。 例如,考虑一个Mojom结构,例如下面的Rect:
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; }
正确的答案是:“是的! 那样就好了!” 幸运的是,它可以!
定义StructTraits
为了教生成的绑定代码如何将任意本机类型T序列化为任意Mojom类型mojom :: U,我们需要定义合适的mojo :: StructTraits模板专业化。
有效的StructTraits专业化必须定义以下静态方法:
-
Mojom结构的每个字段的单个静态访问器,其名称与结构字段完全相同。 这些访问器都必须对本机类型的对象进行引用(最好是const),并且必须返回与Mojom struct字段类型兼容的值。 这用于在消息序列化过程中安全且一致地从本机类型提取数据,而不会产生额外的复制成本。
-
给定Mojom结构的序列化表示形式的单个静态读取方法,该方法初始化本机类型的实例。 读取方法必须返回布尔值,以指示输入的数据是被接受(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()方法将返回false。 这是反序列化过程中的验证步骤:如果客户端发送宽度或高度为负的gfx :: Rect,则其消息将被拒绝并且管道将被关闭。 这样,类型映射不仅可以使调用站点和接口实现更加方便,而且可以用来启用自定义验证逻辑。
当struct字段具有非基本类型时,例如 字符串或数组,建议返回访问器中数据的只读视图以避免复制。 这是安全的,因为保证了输入对象的寿命超过了accessor方法返回的结果的使用时间。
以下示例使用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,但是我们仍然需要教导绑定生成器(以及构建系统)有关映射的知识。 为此,我们必须向GN中的mojom目标添加更多信息:
# 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文档。
有了这种额外的配置,对gfx.mojom.Rect的所有mojom引用(例如,对于方法参数或结构字段)将在生成的C ++代码中为gfx :: Rect引用。
对于绑定的Blink变体,请改为添加到blink_cpp_typemaps列表中。
没有traits_sources的类型映射
在typemap配置中使用traits_sources意味着列出的源将直接烘焙到相应的mojom目标自身的源中。 如果要对Blink和非Blink绑定使用相同的类型映射,则可能会出现问题。
在这种情况下,建议您为类型图特征定义一个单独的组件目标,并在类型图的traits_public_deps中引用该目标:
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参考
每个StructTraits专业化的静态getter方法(每个结构字段一个)必须返回一种类型,该类型可以在序列化期间用作该字段的数据源。 这是将Mojom字段类型映射到有效的getter返回类型的快速参考:
Mojom Field Type | C++ Getter Return Type |
---|---|
bool |
bool |
int8 |
int8_t |
uint8 |
uint8_t |
int16 |
int16_t |
uint16 |
uint16_t |
int32 |
int32_t |
uint32 |
uint32_t |
int64 |
int64_t |
uint64 |
uint64_t |
float |
float |
double |
double |
handle |
mojo::ScopedHandle |
handle<message_pipe> |
mojo::ScopedMessagePipeHandle |
handle<data_pipe_consumer> |
mojo::ScopedDataPipeConsumerHandle |
handle<data_pipe_producer> |
mojo::ScopedDataPipeProducerHandle |
handle<shared_buffer> |
mojo::ScopedSharedBufferHandle |
pending_remote<Foo> |
mojo::PendingRemote<Foo> |
pending_receiver<Foo> |
mojo::PendingReceiver<Foo> |
pending_associated_remote<Foo> |
mojo::PendingAssociatedRemote<Foo> |
pending_associated_receiver<Foo> |
mojo::PendingAssociatedReceiver<Foo> |
string |
Value or reference to any type T that has a mojo::StringTraits specialization defined. By default this includes std::string , base::StringPiece , and 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<T> , mojo::CArray<T> , and WTF::Vector<T> (Blink). |
map<K, V> |
Value or reference to any type T that has a mojo::MapTraits specialization defined. By default this includes std::map<T> , mojo::unordered_map<T> , and WTF::HashMap<T> (Blink). |
FooEnum |
Value of any type that has an appropriate EnumTraits specialization defined. By default this inlcudes only the generated FooEnum type. |
FooStruct |
Value or reference to any type that has an appropriate StructTraits specialization defined. By default this includes only the generated FooStructPtr type. |
FooUnion |
Value of reference to any type that has an appropriate UnionTraits specialization defined. By default this includes only the generated FooUnionPtr type. |
Foo? |
base::Optional<CppType> , 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类型
StructTraits专业化上的静态读取方法会生成一个生成的FooDataView参数(例如上例中的RectDataView),它可以在传入消息的内容中提供对序列化Mojom结构的直接视图。 为了尽可能简化操作,生成的FooDataView类型具有与每个struct字段相对应的生成方法:
-
对于POD字段类型(例如布尔,浮点数,整数),它们是简单的访问器方法,其名称与字段名称相同。 因此,在示例示例中,我们可以访问诸如data.x()和data.width()之类的东西。 返回类型与上表``StructTraits参考''下列出的映射完全对应。
-
对于句柄和接口类型(例如handle或pending_remote <Foo>),它们被命名为TakeFieldName(用于名为field_name的字段),它们按值返回适当的仅移动句柄类型。 返回类型与上表``StructTraits参考''下列出的映射完全对应。
-
对于所有其他字段类型(例如,枚举,字符串,数组,映射,结构),它们被命名为ReadFieldName(对于名为field_name的字段),并且它们返回布尔值(表示读取成功或失败)。 成功后,他们用反序列化的字段值填充其输出参数。 输出参数可以是指向任何类型的指针,该类型已定义了适当的StructTraits专业化,如上表中《 StructTraits参考》中所述。
这里有一个例子。 假设我们引入了一个新的Mojom结构:
struct RectPair { Rect left; Rect right; };
and a corresponding C++ type:
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)实际上只是给定C ++绑定的一个变体(默认或铬变体) Mojom文件。
树中当前定义的唯一其他变量是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目标除了常规的cpp_typemaps外还支持blink_cpp_typemaps参数。 这列出了要应用于Blink绑定的类型映射。
要专门依赖生成的Blink绑定,请参考$ {target_name} _blink。 因此,例如,定义:
# In //foo/mojom mojom("mojom") { sources = [ "db.mojom", ] }
C ++源代码可以通过依赖于“ // foo / mojom:mojom_blink”来依赖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 ++特定注意事项。
查询接口版本
远程定义以下方法来查询或声明远程接口版本:
void QueryVersion(base::OnceCallback<void(uint32_t)> callback);
这将向远程端点查询其绑定的版本号。 收到响应后,将使用远程版本号调用回调。 请注意,此值由Remote实例缓存,以避免重复查询。
void RequireVersion(uint32_t version);
通知远程端点客户端需要最低版本的版本。 如果远程端点不能支持该版本,它将立即关闭管道的末端,从而阻止接收其他任何请求。
版本化枚举
为了方便起见,每个可扩展枚举都有一个生成的帮助函数,以确定实现的枚举定义的当前版本是否知道接收到的枚举值。 例如:
[Extensible] enum Department { SALES, DEV, RESEARCH, };
在与生成的C ++枚举类型相同的名称空间中生成函数:
inline bool IsKnownEnumValue(Department value);
在Chrome中使用Mojo绑定
See Converting Legacy Chrome IPC To Mojo.
附加文件
Calling Mojo From Blink: A brief overview of what it looks like to use Mojom C++ bindings from within Blink code