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

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

線路網域物件會為了效能而犧牲交易安全

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++ 包裝函式。不會擁有內容的記憶體。

您可以分別提供指標和 UTF-8 位元組數 (不含尾端 \0),建構 fidl::StringView。或者,也可以傳遞 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);
});

建立線路檢視畫面和物件

從高層次來看,建立線路物件的方法有兩種:使用競技場,或不安全地借用記憶體。在大多數情況下,使用競技場較安全且效能良好。不安全地借用記憶體非常容易發生錯誤和損毀,但如果需要控制分配的每個位元組,則可能需要呼叫。

使用競技場建立線路物件

Wire 物件會與競技場介面 fidl::AnyArena 整合,通常是在建構函式或工廠函式中,讓使用者注入自訂分配行為。FIDL 執行階段提供競技場介面 fidl::Arena 的標準實作。競技場會管理已分配線路物件的生命週期 (擁有物件)。競技場一經銷毀,系統就會解除分配所有已分配的物件,並呼叫其解構函式。

fidl::Arena 定義於 lib/fidl/cpp/wire/arena.h

物件會先在屬於競技場的緩衝區內分配 (競技場的內嵌欄位)。緩衝區的預設大小為 512 個位元組。你可以使用 fidl::Arena<size> 選取其他尺寸。只要調整大小,就能讓要求期間建立的所有已分配物件都適合堆疊,進而避免成本較高的堆積分配。

當內嵌緩衝區已滿,競技場會在堆積上分配更多緩衝區。 每個緩衝區的大小為 16 KiB。如果需要大於 16 KiB 的物件,競技場會使用自訂緩衝區,並提供足夠空間來容納必要大小。

使用競技場的標準模式如下:

  • 定義 fidl::Arena 類型的區域變數競技場。
  • 使用競技場分配物件。
  • 透過發出 FIDL 方法呼叫或透過完成器回覆,傳送已分配的物件。
  • 離開函式範圍後,系統會自動取消分配所有這些本機變數。

競技場的生命週期必須比參照其中物件的所有檢視區塊類型都長。

如需附註範例,瞭解如何在實務上使用競技場建構資料表、聯集等,請參閱線路網域物件教學課程

建立借用非自有資料的線路檢視畫面

除了受管理配置策略之外,您也可以直接建立指標,指向 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.size(), 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);

您也可以直接從字串常值建立 StringView,而不使用 FromExternal。由於字串常值具有靜態生命週期,因此這項做法很安全。

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");