Memory ownership of wire domain objects

This document provides an overview of the tools available to manage memory when using the wire domain objects from the new C++ bindings.

Wire domain objects trade safety for performance

The wire domain objects of the C++ bindings, also termed "wire types", are thin types whose memory layout matches that of the FIDL wire format of their source *.fidl types as closely as possible. Convenience wrappers and accessors are provided, but they do not attempt to encapsulate the structure of the underlying wire format. With this design, fields oftentimes may be accessed in-place and serialized with less copying. Understanding the layout of the FIDL wire format would greatly assist in interacting with wire types. In particular, wire types do not own their out-of-line children, as defined by the FIDL wire format.

The wire types keep unowned references to objects using:

  • For a string, fidl::StringView.
  • For a vector of objects, fidl::VectorView.
  • For an out-of-line object, fidl::ObjectView.
  • For a table named Foo, a Foo class that wraps a vector view, referencing a collection of envelope headers represented by a fidl::WireTableFrame<Foo>, which in turn references the fields of the table.
  • For a union named Foo, a Foo class that stores the ordinal and an envelope header:
    • Small fields <= 4 bytes use the inline representation and are stored inside the header.
    • Larger fields > 4 bytes use the out-of-line representation are stored out-of-line and referenced by the header.
  • For a MyMethod request message, a MyMethodRequestView that wraps a pointer to the request. This definition is scoped to the containing fidl::WireServer<Protocol> class.

With the exception of a union with an active member using the inline representation, these are non-owning views that only keep a reference and do not manage the object lifetime. The lifetime of the objects must be managed externally. This means that the referenced objects must outlive the views.

Copying a view will alias the references within the view. Moving a view is equivalent to a copy and does not clear the source view.

For memory safety reasons tables are immutable. The default constructor for a table returns an empty table. To create a table with fields you must use a builder. The members of tables may be mutable but you can't add or remove members after creation.

For simplicity and consistency with tables, unions are also immutable. Their default constructor puts them in the absent state. It's a runtime error to send an absent union unless the union is marked optional in the library definition. To get a union Foo with a member bar call the static factory function Foo::WithBar(...). The arguments are either the value (for values inlined into the envelope), a fidl::ObjectView of the value (for larger values) or an arena and constructor arguments for the value.

fidl::StringView

Defined in lib/fidl/cpp/wire/string_view.h

Holds a reference to a variable-length string stored within the buffer. C++ wrapper of fidl_string. Does not own the memory of the contents.

fidl::StringView may be constructed by supplying the pointer and number of UTF-8 bytes (excluding trailing \0) separately. Alternatively, one could pass a C++ string literal, or any value that implements [const] char* data() and size(). The string view would borrow the contents of the container.

It is memory layout compatible with fidl_string.

fidl::VectorView<T>

Defined in lib/fidl/cpp/wire/vector_view.h

Holds a reference to a variable-length vector of elements stored within the buffer. C++ wrapper of fidl_vector. Does not own the memory of elements.

fidl::VectorView may be constructed by supplying the pointer and number of elements separately. Alternatively, one could pass any value that supports std::data, such as a standard container, or an array. The vector view would borrow the contents of the container.

It is memory layout compatible with fidl_vector.

fidl::Array<T, N>

Defined in lib/fidl/cpp/wire/array.h

Owns a fixed-length array of elements. It is similar to std::array<T, N>, but designed to be memory layout compatible with FIDL arrays, and standard-layout. The destructor closes handles if applicable e.g. a fidl::Array of handles.

Message views in request/response handlers

The request handlers in server implementations receive a view of the request message. They do not own the buffer backing the view.

The data behind the request view is only guaranteed to live until the end of the method handler. Therefore, if the server wishes to make a reply asynchronously, and the reply makes use of the request message, the user needs to copy relevant fields from the request message to owned storage:

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

Similarly, the response handlers and event handlers passed to a client also only receive a view of the response/event message. Copying to user-owned storage is required if they need to be accessed after the handler returns:

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

Creating wire views and objects

At the high level there are two ways to create wire objects: using an arena, or unsafely borrowing memory. Using arenas is the safer and is performant in most cases. Unsafely borrowing memory is very prone to errors and corruptions, but may be called for when one needs to control every single byte of allocation.

Create wire objects using arenas

Wire objects integrate with an arena interface fidl::AnyArena, typically in their constructors or factory functions, which lets users inject custom allocation behavior. The FIDL runtime provides a standard implementation of the arena interface, fidl::Arena. The arena manages the lifetime of the allocated wire objects (it owns the objects). As soon as the arena is destroyed, all the objects it has allocated are deallocated and their destructors are called.

fidl::Arena is defined in lib/fidl/cpp/wire/arena.h.

The objects are first allocated within a buffer which belongs to the arena (an inline field of the arena). The default size of the buffer is 512 bytes. A different size can be selected using fidl::Arena<size>. By tweaking the size, one could make all their allocated objects created during a request fit on the stack, thus avoiding costlier heap allocations.

When the inline buffer is full, the arena allocates more buffers on the heap. Each of these buffers is 16 KiB. If one needs an object larger than 16 KiB, the arena will use a bespoke buffer with enough space to accommodate the necessary size.

The standard pattern for using the arena is:

  • Define a local variable arena of type fidl::Arena.
  • Allocate objects using the arena.
  • Send the allocated objects by making a FIDL method call or making a reply via a completer.
  • Upon exiting the function scope, all of these local variables are automatically de-allocated.

The arena needs to outlive all the view types that refer to objects within it.

See wire domain object tutorial for annotated examples of how to use the arena in practice to build tables, unions, etc.

Create wire views borrowing unowned data

In addition to the managed allocation strategies, it is also possible to directly create pointers to memory unowned by FIDL. This is discouraged, as it is easy to accidentally create use-after-free bugs. Most view types offer an FromExternal factory function to explicitly borrow pointers to objects that are not managed by the FIDL runtime.

To create an ObjectView from an external object using fidl::ObjectView<T>::FromExternal:

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

To create a VectorView from an external collection using fidl::VectorView<T>::FromExternal:

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

To create a StringView from an external buffer using fidl::StringView::FromExternal:

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

A StringView can also be created directly from string literals without using FromExternal. This is safe since string literals have static lifetime.

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

To create a wire union borrowing a member stored externally, pass an ObjectView referencing the member to the corresponding union factory function:

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

Wire tables store references to a fidl::WireTableFrame<SomeTable>, which is responsible for keeping track of field metadata. To create a wire table borrowing an external frame, pass an ObjectView to an ExternalBuilder.

An example of setting a field that is inlined into the frame:

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

An example of setting a field that is stored out-of-line from the frame:

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