有線網域物件的記憶體擁有權

本文件概略說明使用新 C++ 繫結的線路網域物件時,可用來管理記憶體的工具。

線路領域物件會保障效能

C++ 繫結的線路網域物件 (又稱為「線型類型」) 是細型類型,其記憶體版面配置盡可能符合來源 *.fidl 類型的 FIDL 線路格式。系統會提供便利的包裝函式和存取子,但不會嘗試封裝基礎線路格式的結構。透過這項設計,欄位通常可以原地存取,且在複製過程中進行序列化。瞭解 FIDL 線路格式的版面配置,可有效協助與線路類型互動。特別是,接線類型並不具有由 FIDL 線路格式定義的外線子項。

線型類型會使用以下方式,保留對物件的未擁有參照:

  • 針對字串,fidl::StringView
  • 如為物件向量,則為 fidl::VectorView
  • 針對外行物件,fidl::ObjectView
  • 針對名為 Foo 的資料表,這是納入向量檢視畫面的 Foo 類別,並參照由 fidl::WireTableFrame<Foo> 表示的郵件標頭集合,然後參照該資料表的欄位。
  • 針對名為 Foo 的聯集,此 Foo 類別可儲存序數和信封標頭
    • 小於 4 個位元組的小型欄位會使用內嵌表示法,並儲存在標頭中。
    • 大於 4 個位元組的欄位會使用外行表示法進行離線儲存,並由標頭參照。
  • 如果是 MyMethod 要求訊息,則為 MyMethodRequestView 包裝指向要求的指標。這個定義的範圍僅限於包含的 fidl::WireServer<Protocol> 類別。

除非有使用內嵌表示法的有效成員聯集,但這些是非擁有的檢視畫面,僅會保留參照,無法管理物件生命週期。物件的生命週期必須對外管理。這表示參照的物件必須超過檢視畫面的保留時間。

複製檢視表時,系統會為檢視表中的參照建立別名。移動檢視畫面等同於副本移動,且不會清除來源檢視畫面。

基於記憶體安全原因,資料表無法變更。資料表的預設建構函式會傳回空白的資料表。如要建立具有欄位的資料表,您必須使用建構工具。資料表的成員可以變動,但無法在建立後新增或移除成員。

為了簡單且一致的資料表,聯集也無法變更。其預設建構函式會將這些元素置於不存在狀態。除非聯集在程式庫定義中標示為 optional,否則會傳送缺少的聯集,是執行階段錯誤。如要取得成員 bar 的聯集 Foo,請呼叫靜態工廠函式 Foo::WithBar(...)。引數可以是值 (適用於信封內的值)、值的 fidl::ObjectView (針對較大的值),或是值的面積和建構函式引數。

fidl::StringView

定義於 lib/fidl/cpp/wire/string_view.h

保留儲存在緩衝區中可變長度字串的參照。fidl_string 的 C++ 包裝函式。不是內容的記憶體。

fidl::StringView 可以藉由提供指標和 UTF-8 位元組數 (不包括結尾的 \0) 建構。或者,您也可以傳遞 C++ 字串常值,或是實作 [const] char* data()size() 的任何值。字串檢視畫面會借用容器的內容。

其記憶體版面配置與 fidl_string 相容。

fidl::VectorView<T>

定義於 lib/fidl/cpp/wire/vector_view.h

保留對儲存在緩衝區內可變長度元素的參照。fidl_vector 的 C++ 包裝函式。不是元素的記憶體。

fidl::VectorView 可以單獨提供指標和元素數量進行建構。或者,您也可以傳遞任何支援 std::data 的值,例如標準容器或陣列。向量檢視畫面會借用容器的內容。

這個記憶體版面配置與 fidl_vector 相容。

fidl::Array<T, N>

定義於 lib/fidl/cpp/wire/array.h

擁有固定長度的元素陣列。這與 std::array<T, N> 類似,但其設計旨在與 FIDL 陣列和標準版面配置相容的記憶體版面配置。在適用情況下,解構器會關閉控點 (例如 fidl::Array 控點)。

要求/回應處理常式中的訊息檢視畫面

伺服器實作中的要求處理常式會收到要求訊息的檢視畫面。但這類資源並未擁有備份檢視畫面的緩衝區。

要求檢視畫面背後的資料保證會上線,直到方法處理常式結束為止。因此,如果伺服器想要以非同步方式進行回覆,且回覆使用了要求訊息,則使用者需要將要求訊息中的相關欄位複製到自有儲存空間:

// A FIDL method called "StartGame".
virtual void StartGame(
    StartGameRequestView request, StartGameCompleter::Sync completer) {
  // Suppose the request has a `foo` field that is a string view,
  // we need to copy it to an owning type e.g. |std::string|.
  auto foo = std::string(request->foo.get());
  // Make an asynchronous reply using the owning type.
  async::PostDelayedTask(
      dispatcher_,
      [foo = std::move(foo), completer = completer.ToAsync()]() mutable {
        // As an example, we simply echo back the string.
        completer.Reply(fidl::StringView::FromExternal(foo));
      });
}

同樣地,傳遞至用戶端的回應處理常式和事件處理常式也只會收到回應/事件訊息的檢視畫面。如要在處理常式傳回後存取儲存空間,則需要複製到使用者擁有的儲存空間:

// Suppose the response has a `bar` field that is a table:
//
// type Bar = table {
//     1: a uint32;
//     2: b string;
// };
//
// we need to copy the table to an owned type by copying each element.
struct OwnedBar {
  std::optional<uint32_t> a;
  std::optional<std::string> b;
};
// Suppose we are in a class that has a `OwnedBar bar_` member.
client_->MakeMove(args).Then([](fidl::WireUnownedResult<TicTacToe::MakeMove>& result) {
  assert(result.ok());
  auto* response = result.Unwrap();
  // Create an owned value and copy the wire table into it.
  OwnedBar bar;
  if (response->bar.has_a())
    bar.a = response->bar.a();
  if (response->bar.has_b())
    bar.b = std::string(response->bar.b().get());
  bar_ = std::move(bar);
});

建立線路視圖和物件

大致來說,建立線路物件的方法有兩種:使用運動場,或以不安全的方式借用記憶體。在大多數情況下,使用領域較為安全且效能良好。不安全的借用記憶體非常容易發生錯誤及損毀,但可能會在需要控管每個配置的每個位元組時呼叫記憶體。

運用競技場建立線路物體

線材物件會與運動場介面 fidl::AnyArena 整合,通常位於其建構函式或工廠函式中,可讓使用者插入自訂配置行為。FIDL 執行階段提供區域介面 fidl::Arena 的標準實作。舞台會管理配置的線路物件的生命週期 (擁有物件)。競技場遭到刪除後,系統會取消配置的所有物件,並呼叫其解構函式。

fidl::Arena 是在 lib/fidl/cpp/wire/arena.h 中定義。

系統會先將物件配置在屬於運動場的緩衝區內 (運動場的內嵌欄位)。緩衝區的預設大小為 512 個位元組。您可以使用 fidl::Arena<size> 選取其他大小。透過調整大小,我們可以讓要求期間建立的所有配置物件都納入堆疊,進而避免使用成本較高的堆積配置。

當內嵌緩衝區已滿時,運動場會在堆積上分配更多緩衝區。每個緩衝區皆為 16 KiB。如果其中一個需要大於 16 KiB 的物件,區域會使用自訂的緩衝區,且有足夠空間容納所需的大小。

使用運動場的標準模式如下:

  • 定義 fidl::Arena 類型的本機變數。
  • 使用競技場分配物件。
  • 透過進行 FIDL 方法呼叫或透過完整函式回覆,傳送配置的物件。
  • 離開函式範圍時,系統會自動取消分配所有本機變數。

舞台必須保留所有參照該類物件的檢視畫面類型。

請參閱Wire 網域物件教學課程,藉由加註的範例,瞭解如何使用運動場建立資料表、聯集等作業。

建立線圖,借用未擁有的資料

除了代管配置策略之外,也可以直接建立指向 FIDL 未擁有的記憶體指標。我們不建議這麼做,因為這很容易意外產生「使用釋放後記憶體」的錯誤。多數檢視畫面類型都提供 FromExternal 工廠函式,可明確借用非 FIDL 執行階段管理的物件指標。

如要使用 fidl::ObjectView<T>::FromExternal 從外部物件建立 ObjectView

fidl::StringView str("hello");
// |object_view| is a view that borrows the string view.
// Destroying |str| will invalidate |object_view|.
fidl::ObjectView object_view = fidl::ObjectView<fidl::StringView>::FromExternal(&str);
// |object_view| may be dereferenced to access the pointee.
ASSERT_EQ(object_view->begin(), str.begin());

如要使用 fidl::VectorView<T>::FromExternal 從外部集合建立 VectorView

std::vector<uint32_t> vec = {1, 2, 3, 4};
// |vv| is a view that borrows the vector contents of |vec|.
// Destroying the contents in |vec| will invalidate |vv|.
fidl::VectorView<uint32_t> vv = fidl::VectorView<uint32_t>::FromExternal(vec);
ASSERT_EQ(vv.count(), 4UL);

如要使用 fidl::StringView::FromExternal 從外部緩衝區建立 StringView

std::string string = "hello";
// |sv| is a view that borrows the string contents of |string|.
// Destroying the contents in |string| will invalidate |sv|.
fidl::StringView sv = fidl::StringView::FromExternal(string);
ASSERT_EQ(sv.size(), 5UL);

您也可以在不使用 FromExternal 的情況下,直接從字串常值建立 StringView。這種做法安全無虞,因為字串常值具有靜態生命週期

fidl::StringView sv1 = "hello world";
fidl::StringView sv2("Hello");
ASSERT_EQ(sv1.size(), 11UL);
ASSERT_EQ(sv2.size(), 5UL);

如要建立會借用外部儲存成員的線聯集,請將參照成員的 ObjectView 傳遞至對應的聯集工廠函式:

fidl::StringView sv = "hello world";
fuchsia_examples::wire::JsonValue val = fuchsia_examples::wire::JsonValue::WithStringValue(
    fidl::ObjectView<fidl::StringView>::FromExternal(&sv));
ASSERT_TRUE(val.is_string_value());

線形資料表會儲存對 fidl::WireTableFrame<SomeTable> 的參照,後者負責追蹤欄位中繼資料。如要建立借用外部畫面的線路表,請將 ObjectView 傳遞至 ExternalBuilder

以下範例說明如何設定內嵌於畫面中的欄位:

fidl::WireTableFrame<fuchsia_examples::wire::User> frame;
// Construct a table creating a builder borrowing the |frame|.
auto builder = fuchsia_examples::wire::User::ExternalBuilder(
    fidl::ObjectView<fidl::WireTableFrame<fuchsia_examples::wire::User>>::FromExternal(&frame));
// Small values <= 4 bytes are inlined inside the frame of the table.
builder.age(30);
// The builder is turned into an actual instance by calling |Build|.
auto user = builder.Build();
ASSERT_FALSE(user.IsEmpty());
ASSERT_EQ(user.age(), 30);

以下範例說明如何設定儲存在畫面外的欄位:

fidl::WireTableFrame<fuchsia_examples::wire::User> frame;
// Construct a table creating a builder borrowing the |frame|.
auto builder = fuchsia_examples::wire::User::ExternalBuilder(
    fidl::ObjectView<fidl::WireTableFrame<fuchsia_examples::wire::User>>::FromExternal(&frame));
// Larger values > 4 bytes are still stored out of line, i.e. outside the
// frame of the table. One needs to make an |ObjectView| pointing to larger
// fields separately, using an arena or with unsafe borrowing here.
fidl::StringView str("hello");
fidl::ObjectView object_view = fidl::ObjectView<fidl::StringView>::FromExternal(&str);
builder.name(object_view);
// The builder is turned into an actual instance by calling |Build|.
auto user = builder.Build();
ASSERT_FALSE(user.IsEmpty());
ASSERT_TRUE(user.has_name());
ASSERT_EQ(user.name().get(), "hello");