本文檔是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