传输域对象的内存所有权

本文档简要介绍了使用新 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(对于较大的值),或者该值的 arena 和构造函数参数。

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

创建线视图和对象

概括来讲,有两种方法可以创建传输对象:使用 arena 或不安全的借用内存。在大多数情况下,使用 arenas 是更安全且性能最高的方法。以不安全的方式借用内存非常容易出错和损坏,但当需要控制分配的每一个字节时,就可能会调用该方法。

使用 arenas 创建电线对象

线对象与 arena 接口 fidl::AnyArena(通常在其构造函数或工厂函数中)集成,让用户可以注入自定义分配行为。FIDL 运行时提供了 Arena 接口 fidl::Arena 的标准实现。Area 管理已分配的有线对象的生命周期(它拥有这些对象)。一旦销毁 Area,它已分配的所有对象就会释放,并调用其析构函数。

fidl::Arenalib/fidl/cpp/wire/arena.h 中定义。

对象首先在属于 arena(竞技场的内联字段)的缓冲区内分配。缓冲区的默认大小为 512 字节。您可以使用 fidl::Arena<size> 选择其他大小。通过调整大小,可以让在请求期间创建的所有分配的对象都适合放在堆栈中,从而避免开销更高的堆分配。

当内嵌缓冲区已满时, arena 会在堆上分配更多缓冲区。其中每个缓冲区均为 16 KiB。如果需要大于 16 KiB 的对象,该区域将使用定制的缓冲区,该缓冲区具有足够的空间以适应必要大小。

使用 arena 的标准模式如下:

  • 定义一个 fidl::Arena 类型的局部变量 arena。
  • 使用 arena 分配对象。
  • 通过调用 FIDL 方法或通过完成器进行回复来发送分配的对象。
  • 退出函数作用域后,所有这些局部变量都会自动取消分配。

Area 需要比引用其中对象的所有视图类型的存在时间更长。

请参阅有线网域对象教程,查看带注释的示例,了解如何实际使用 arena 来构建表、联合等。

创建借用无主数据的线视图

除了托管分配策略之外,还可以直接创建指向 FIDL 无主内存的指针。我们不建议这样做,因为很容易意外创建释放后使用 bug。大多数视图类型都会提供一个 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);

您也可以直接从字符串字面量创建 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");