传输域对象的内存所有权

本文档简要介绍了使用新 C++ 绑定中的有线网域对象时可用于管理内存的工具。

线框网域对象以性能换取安全性

C++ 绑定的有线网域对象(也称为“有线类型”)是精简类型,其内存布局尽可能与源 *.fidl 类型的 FIDL 有线格式相匹配。虽然提供了便捷的封装容器和访问器,但它们不会尝试封装底层传输格式的结构。采用这种设计后,字段通常可以就地访问,并以更少的复制次数进行序列化。了解 FIDL 传输格式的布局有助于与传输类型进行交互。特别是,有线类型不拥有其带外子级(如 FIDL 有线格式所定义)。

线类型使用以下方式来保留对对象的非自有引用:

  • 对于字符串,为 fidl::StringView
  • 对于对象向量,为 fidl::VectorView
  • 对于内嵌对象,为 fidl::ObjectView
  • 对于名为 Foo 的表,Foo 类封装了向量视图,引用了由 fidl::WireTableFrame<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);
});

创建线框视图和对象

从高层来看,创建 wire 对象有两种方式:使用 arena 或以不安全的方式借用内存。使用 arena 更安全,并且在大多数情况下性能良好。不安全地借用内存非常容易出错和损坏,但如果需要控制分配的每个字节,则可能需要调用此函数。

使用 arena 创建线对象

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

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

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

当内联缓冲区已满时,内存池会在堆上分配更多缓冲区。每个缓冲区的容量为 16 KiB。如果需要大于 16 KiB 的对象,竞技场将使用一个自定义缓冲区,该缓冲区具有足够的空间来容纳所需的大小。

使用竞技场的标准模式是:

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

竞技场需要比引用其中对象的全部视图类型更长寿。

如需查看有关如何在实践中使用 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.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");