本文档简要介绍了使用新 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::Arena
在 lib/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");