Using natural and wire domain objects

Prerequisites

This tutorial builds on the Compiling FIDL tutorial. For more information on other FIDL tutorials, see the overview.

Overview

This tutorial details how to use the natural and wire domain objects by creating a unit test exercising those data types.

This document covers how to complete the following tasks:

Using the domain objects example code

The example code accompanying this tutorial is located in your Fuchsia checkout at //examples/fidl/cpp/domain_objects. It consists of a unit test component and its containing package. For more information about building unit test components, see Build components.

You may build and run the example on a running instance of Fuchsia emulator via the following:

# Add the domain objects unit test to the build.
# This only needs to be done once.
fx set core.x64 --with //examples/fidl/cpp/domain_objects
# Run the domain objects unit test.
fx test -vo fidl-examples-domain-objects-cpp-test

Add the C++ bindings of a FIDL library as a build dependency

For each FIDL library declaration, such as the one in Compiling FIDL, the C++ bindings code for that library is generated under the original target name suffixed with _cpp:

"//examples/fidl/fuchsia.examples:fuchsia.examples_cpp",

The test target looks like:

test("test") {
  testonly = true
  output_name = "fidl_examples_domain_objects_cpp_test"
  sources = [
    "advanced.cc",
    "main.cc",
  ]
  deps = [
    "//examples/fidl/fuchsia.examples:fuchsia.examples_cpp",

    "//src/lib/fxl/test:gtest_main",
  ]
}

Note the line which adds the dependency on the C++ bindings by referencing that _cpp target.

(Optional) To view the generated bindings:

  1. Build using fx build.
  2. Change to the generated files directory: out/default/fidling/gen/examples/fidl/fuchsia.examples/fuchsia.examples/cpp/fidl/fuchsia.examples/cpp, where the generated files are located. You may need to change out/default if you have set a different build output directory. You can check your build output directory with cat .fx-build-dir.

For more information on how to find generated bindings code, see Viewing generated bindings code.

Include the bindings header into your code

After adding the build dependency, you may include the bindings header. The include pattern is #include <fidl/my.library.name/cpp/fidl.h>.

The following include statement at the top of domain_objects/main.cc includes the bindings and makes the generated APIs available to the source code:

#include <fidl/fuchsia.examples/cpp/fidl.h>

Using natural domain objects

Natural types are the ergonomics and safety focused flavor of C++ domain objects. A tree of FIDL values is represented as a tree of C++ objects with hierarchical ownership. That means if a function receives some object of natural type, it can assume unique ownership of all child objects in the entire tree. The tree is torn down when the root object goes out of scope.

At a high level the natural types embrace std:: containers and concepts. For example, a table is represented as a collection of std::optional<Field>s. A vector is std::vector<T>, etc. They also implement idiomatic C++ moves, copies, and equality. For example, a resource type is move-only, while a value type will implement both copy and moves, where moves are designed to optimize the transfer of objects. Moving a table doesn't make it empty (it just recursively moves the fields), similar to std::optional.

Natural bits

Using the strict fuchsia.examples/FileMode FIDL type and the flexible fuchsia.examples/FlexibleFileMode FIDL type as examples:

// Bits implement bitwise operators such as |, ~, &, ^.
auto flags = ~fuchsia_examples::FileMode::kRead & fuchsia_examples::FileMode::kExecute;
flags = fuchsia_examples::FileMode::kRead | fuchsia_examples::FileMode::kWrite;

// Bits may be explicitly casted to their underlying integer type.
ASSERT_EQ(static_cast<uint16_t>(flags), 0b11);

// They may also be explicitly constructed from an underlying type, but
// this may result in invalid values for strict bits.
flags = fuchsia_examples::FileMode(0b11);

// A safer alternative is |TryFrom|, which constructs an instance of
// |FileMode| only if underlying primitive does not contain any unknown
// members that is not defined in the FIDL schema. Otherwise, returns
// |std::nullopt|.
std::optional<fuchsia_examples::FileMode> maybe_flags =
    fuchsia_examples::FileMode::TryFrom(0b1111);
ASSERT_FALSE(maybe_flags.has_value());

// Another alternative is |TruncatingUnknown| which clears any bits not
// defined in the FIDL schema.
fuchsia_examples::FileMode truncated_flags =
    fuchsia_examples::FileMode::TruncatingUnknown(0b1111);
ASSERT_EQ(truncated_flags, fuchsia_examples::FileMode(0b111));

// Bits implement bitwise-assignment.
flags |= fuchsia_examples::FileMode::kExecute;

// They also support equality and expose a |kMask| that is the
// bitwise OR of all defined bit members.
ASSERT_EQ(flags, fuchsia_examples::FileMode::kMask);

// A flexible bits type additionally supports querying the unknown bits.
fuchsia_examples::FlexibleFileMode flexible_flags = fuchsia_examples::FlexibleFileMode(0b1111);
ASSERT_TRUE(flexible_flags.has_unknown_bits());
ASSERT_EQ(static_cast<uint16_t>(flexible_flags.unknown_bits()), 0b1000);

Natural enums

Using the strict fuchsia.examples/LocationType FIDL type and the flexible fuchsia.examples/FlexibleLocationType FIDL type as examples:

// Enums members are scoped constants under the enum type.
fuchsia_examples::LocationType location = fuchsia_examples::LocationType::kAirport;

// They may be explicitly casted to their underlying type.
ASSERT_EQ(static_cast<uint32_t>(fuchsia_examples::LocationType::kMuseum), 1u);

// Enums support switch case statements.
// A strict enum can be switched exhaustively.
(void)[=] {
  switch (location) {
    case fuchsia_examples::LocationType::kAirport:
      return 1;
    case fuchsia_examples::LocationType::kMuseum:
      return 2;
    case fuchsia_examples::LocationType::kRestaurant:
      return 3;
  }
};

// A flexible enum requires a `default:` case.
fuchsia_examples::FlexibleLocationType flexible_location =
    fuchsia_examples::FlexibleLocationType::kAirport;
(void)[=] {
  switch (flexible_location) {
    case fuchsia_examples::FlexibleLocationType::kAirport:
      return 1;
    case fuchsia_examples::FlexibleLocationType::kMuseum:
      return 2;
    case fuchsia_examples::FlexibleLocationType::kRestaurant:
      return 3;
    default:  // Removing this branch will fail to compile.
      return 4;
  }
};

// A flexible enum also supports asking if the current enum value was
// not known in the FIDL schema.
ASSERT_FALSE(flexible_location.IsUnknown());

Natural structs

Natural structs are straightforward record objects that expose const and mutable accessors. Using the fuchsia.examples/Color FIDL type as an example:

// Structs may be default constructed with fields set to default values,
// provided that all fields are also default constructible.
fuchsia_examples::Color default_color;
ASSERT_EQ(default_color.id(), 0u);
ASSERT_EQ(default_color.name(), "red");

// They support constructing by supplying fields in a sequence.
fuchsia_examples::Color blue = {1, "blue"};
ASSERT_EQ(blue.id(), 1u);

// They also support a more readable syntax that names individual fields,
// similar to C++ designated initialization. The double brace (`{{`) syntax
// is necessary to workaround C++ limitations on aggregate initialization.
fuchsia_examples::Color red{{.id = 2, .name = "red"}};
ASSERT_EQ(red.id(), 2u);
fuchsia_examples::Color designated_1 = {{.id = 1, .name = "designated"}};
ASSERT_EQ(designated_1.id(), 1u);
fuchsia_examples::Color designated_2{{.id = 2, .name = "designated"}};
ASSERT_EQ(designated_2.id(), 2u);

// Setters are simply accessors that return non-const references.
fuchsia_examples::Color color;
color.id() = 42;
color.name() = "yellow";
ASSERT_EQ(color.id(), 42u);
ASSERT_EQ(color.name(), "yellow");

// Equality is implemented for value types.
ASSERT_EQ(color, fuchsia_examples::Color(42, "yellow"));

// Copies and moves.
fuchsia_examples::Color color_copy{color};
ASSERT_EQ(color_copy.name(), "yellow");
fuchsia_examples::Color color_moved{std::move(color)};
ASSERT_EQ(color_moved.name(), "yellow");
// The state of |color| is now unspecified.

Natural unions

Natural unions are sum types similar to std::variant. Using the strict fuchsia.examples/JsonValue FIDL type and the flexible fuchsia.examples/FlexibleJsonValue FIDL type as examples:

// Factory functions are used to construct natural union objects.
// To construct a union whose active member is |int_value|, use |WithIntValue|.
auto int_val = fuchsia_examples::JsonValue::WithIntValue(1);

// |Which| obtains an enum corresponding to the active member, which may be
// used in switch cases.
ASSERT_EQ(int_val.Which(), fuchsia_examples::JsonValue::Tag::kIntValue);

// When directly accessing a field, one must first check if the field is
// active before dereferencing it.
ASSERT_TRUE(int_val.int_value().has_value());
ASSERT_TRUE(static_cast<bool>(int_val.int_value()));
ASSERT_EQ(int_val.int_value().value(), 1);

// Another example, this time activating the |string_value| member.
auto str_val = fuchsia_examples::JsonValue::WithStringValue("1");
ASSERT_EQ(str_val.Which(), fuchsia_examples::JsonValue::Tag::kStringValue);
ASSERT_TRUE(str_val.string_value().has_value());

// Unions are not default constructible, to avoid invalid states.
static_assert(!std::is_default_constructible_v<fuchsia_examples::JsonValue>,
              "Unions cannot be default constructed");

fuchsia_examples::JsonValue value = fuchsia_examples::JsonValue::WithStringValue("hello");
ASSERT_FALSE(value.int_value());
ASSERT_TRUE(value.string_value());

// |value_or| returns a fallback if the corresponding member is not active.
ASSERT_EQ(value.int_value().value_or(42), 42);

// Setting a field causes that field to become the active member.
value.int_value() = 2;
ASSERT_TRUE(value.int_value());
ASSERT_FALSE(value.string_value());

// |take| invokes the move operation on the member if it is active.
value.string_value() = "foo";
std::optional<std::string> str = value.string_value().take();
ASSERT_TRUE(str.has_value());
ASSERT_EQ(str.value(), "foo");

// Equality is implemented for value types.
value.string_value() = "bar";
ASSERT_EQ(value, fuchsia_examples::JsonValue::WithStringValue("bar"));

// Copies and moves.
fuchsia_examples::JsonValue value_copy{value};
ASSERT_EQ(value.string_value().value(), "bar");
fuchsia_examples::JsonValue value_moved{std::move(value)};
ASSERT_EQ(value_moved.string_value().value(), "bar");

// A flexible union additionally supports querying if the active member was
// not defined in the FIDL schema.
fuchsia_examples::FlexibleJsonValue flexible_value =
    fuchsia_examples::FlexibleJsonValue::WithIntValue(1);
// If |flexible_value| was received from a peer with a different FIDL schema,
// |Which| may return |kUnknown| if that peer sent a union with a member that
// we do not understand. In this example |flexible_value| holds a known active
// member.
ASSERT_NE(flexible_value.Which(), fuchsia_examples::FlexibleJsonValue::Tag::kUnknown);

Natural tables

Natural tables are record types where every field is optional. Using the fuchsia.examples/User FIDL type as an example:

// A default constructed table is empty. That is, every field is absent.
fuchsia_examples::User user;
ASSERT_TRUE(user.IsEmpty());

// Each accessor returns a |std::optional<T>|, where |T| is the field type.
ASSERT_FALSE(user.age().has_value());

// To set fields, simply use the mutable accessors.
user.age() = 100;
user.age() = *user.age() + 100;
ASSERT_EQ(user.age().value(), 200);

// |value_or| returns a fallback if the corresponding field is absent.
ASSERT_EQ(user.name().value_or("anonymous"), "anonymous");
user.age().reset();
ASSERT_TRUE(user.IsEmpty());

// Similar to structs, tables support constructing by naming individual fields.
// Fields that are omitted from the designated initialization syntax will be
// absent from the table.
user = {{.age = 100, .name = "foo"}};
ASSERT_TRUE(user.age());
ASSERT_TRUE(user.name());

user = {{.age = 100}};
ASSERT_TRUE(user.age());
ASSERT_FALSE(user.name());

// Equality is implemented for value types.
ASSERT_EQ(user, fuchsia_examples::User{{.age = 100}});

// Copies and moves.
fuchsia_examples::User user_copy{user};
ASSERT_EQ(*user.age(), 100);
fuchsia_examples::User user_moved{std::move(user)};
ASSERT_EQ(*user_moved.age(), 100);

Using wire domain objects

Wire types are the performance oriented flavor of C++ domain objects. Differing from natural types which maintain hierarchical object ownership, wire objects never own their out-of-line children. Whether a child object is stored inline or out-of-line is determined by the FIDL wire format.

Natural types may implicitly heap allocate the necessary storage. Conversely, the user has complete control over memory allocation of wire types. For example, you may allocate the elements of a FIDL vector on the stack, from a memory pool, or as part of a larger object. The wire vector type, fidl::VectorView<T>, is an unowned view type consisting of a raw pointer and a length. One may send the vector as part of a FIDL request without extra heap allocations by borrowing the elements via this type.

To distinguish from the natural types, wire types from a FIDL library are defined in the ...::wire nested namespace, e.g. fuchsia_my_library::wire.

The prevalence of unowned pointers in wire types makes them flexible but very unsafe. This tutorial will focus on the safer side of using wire types based on memory arenas. For more advanced usages involving unsafe memory borrows, refer to Memory ownership of wire domain objects.

Wire bits and enums

Because bits and enums have a very simple memory layout and do not have any out-of-line children, the wire types for FIDL bits and enums are the same as their natural type counterparts. To stay coherent with the overall namespace naming profiles, bits and enums are aliased into the fuchsia_my_library::wire nested namespace, appearing alongside wire structs, unions, and tables.

Using the fuchsia.examples/FileMode FIDL bits as an example, fuchsia_examples::wire::FileMode is a type alias of fuchsia_examples::FileMode.

static_assert(std::is_same<fuchsia_examples::FileMode, fuchsia_examples::wire::FileMode>::value,
              "natural bits should be equivalent to wire bits");
static_assert(fuchsia_examples::FileMode::kMask == fuchsia_examples::wire::FileMode::kMask,
              "natural bits should be equivalent to wire bits");

using fuchsia_examples::wire::FileMode;
auto flags = FileMode::kRead | FileMode::kWrite | FileMode::kExecute;
ASSERT_EQ(flags, FileMode::kMask);

Similarly, using the fuchsia.examples/LocationType FIDL enum as an example, fuchsia_examples::wire::LocationType is a type alias of fuchsia_examples::LocationType.

static_assert(
    std::is_same<fuchsia_examples::LocationType, fuchsia_examples::wire::LocationType>::value,
    "natural enums should be equivalent to wire enums");

ASSERT_EQ(static_cast<uint32_t>(fuchsia_examples::wire::LocationType::kMuseum), 1u);

Wire structs

Wire structs are simple C++ structs that hold public member variables. Using the fuchsia.examples/Color FIDL type as an example:

// Wire structs are simple C++ structs with all their member fields declared
// public. One may invoke aggregate initialization:
fuchsia_examples::wire::Color blue = {1, "blue"};
ASSERT_EQ(blue.id, 1u);
ASSERT_EQ(blue.name.get(), "blue");

// ..or designated initialization.
fuchsia_examples::wire::Color blue_designated = {.id = 1, .name = "blue"};
ASSERT_EQ(blue_designated.id, 1u);
ASSERT_EQ(blue_designated.name.get(), "blue");

// A wire struct may be default constructed, but user-defined default values
// are not supported.
// Default-initializing a struct means all fields are zero-initialized.
fuchsia_examples::wire::Color default_color;
ASSERT_EQ(default_color.id, 0u);
ASSERT_TRUE(default_color.name.is_null());
ASSERT_TRUE(default_color.name.empty());

// There are no getters/setters. One simply reads or mutates the member field.
blue.id = 2;
ASSERT_EQ(blue.id, 2u);

// Here we demonstrate that wire structs do not own their out-of-line children.
// Copying a struct will not copy their out-of-line children. Pointers are
// simply aliased.
{
  fuchsia_examples::wire::Color blue2 = blue;
  ASSERT_EQ(blue2.name.data(), blue.name.data());
}
// Similarly, destroying a wire struct object does not destroy out-of-line
// children. Destroying |blue2| does not invalidate the string contents in |name|.
ASSERT_EQ(blue.name.get(), "blue");

Wire unions

Wire unions are sum types with a memory layout akin to a discriminator tag followed by a reference to the active member. Using the strict fuchsia.examples/JsonValue FIDL type and the flexible fuchsia.examples/FlexibleJsonValue FIDL type as examples:

// When the active member is larger than 4 bytes, it is stored out-of-line,
// and the union will borrow the out-of-line content. The lifetimes can be
// tricky to reason about, hence the FIDL runtime provides a |fidl::AnyArena|
// interface for arena-based allocation of members. The built-in
// implementation is |fidl::Arena|.
//
// Pass the arena as the first argument to |With...| factory functions, to
// construct the member content on the arena, and have the union reference it.
fidl::Arena arena;
fuchsia_examples::wire::JsonValue str_union =
    fuchsia_examples::wire::JsonValue::WithStringValue(arena, "1");

// |Which| obtains an enum corresponding to the active member, which may be
// used in switch cases.
ASSERT_EQ(str_union.Which(), fuchsia_examples::wire::JsonValue::Tag::kStringValue);

// Before accessing the |string_value| member, one should check if the union
// indeed currently holds this member, by querying |is_string_value|.
// Accessing the wrong member will cause a panic.
ASSERT_TRUE(str_union.is_string_value());
ASSERT_EQ("1", str_union.string_value().get());

// When the active member is smaller or equal to 4 bytes, such as an
// |int32_t| here, the entire member is inlined into the union object.
// In these cases, arena allocation is not necessary, and the union
// object wholly owns the member.
fuchsia_examples::wire::JsonValue int_union = fuchsia_examples::wire::JsonValue::WithIntValue(1);
ASSERT_TRUE(int_union.is_int_value());
ASSERT_EQ(1, int_union.int_value());

// A default constructed wire union is absent.
// It must be initialized with a valid member before use.
// One is not allowed to send absent unions through FIDL client/server APIs,
// unless those APIs take optional unions.
fuchsia_examples::wire::JsonValue default_union;
ASSERT_TRUE(default_union.has_invalid_tag());
default_union = fuchsia_examples::wire::JsonValue::WithStringValue(arena, "hello");
ASSERT_FALSE(default_union.has_invalid_tag());
ASSERT_TRUE(default_union.is_string_value());
ASSERT_EQ(default_union.string_value().get(), "hello");

// A flexible union additionally supports querying if the active member was
// not defined in the FIDL schema.
fuchsia_examples::wire::FlexibleJsonValue flexible_value =
    fuchsia_examples::wire::FlexibleJsonValue::WithIntValue(1);
// If |flexible_value| was received from a peer with a different FIDL schema,
// |Which| may return |kUnknown| if that peer sent a union with a member that
// we do not understand. In this example |flexible_value| holds a known active
// member because we just created it ourselves.
ASSERT_NE(flexible_value.Which(), fuchsia_examples::wire::FlexibleJsonValue::Tag::kUnknown);

Wire tables

Wire tables are record types where every field is optional. Differing from natural tables, wire tables do not own any member field. Copying a wire table is akin to aliasing (copying) a pointer. Similar to pointers, moving a wire table is an anti-pattern because that equates to a copy.

Because of the memory layout constraints of wire tables, one always use an associated Builder type to create new instances. Once a table is built, one may not add new members or clear existing members.

Using the fuchsia.examples/User FIDL type as an example:

fidl::Arena arena;
// To construct a wire table, you need to first create a corresponding
// |Builder| object, which borrows an arena. The |arena| will be used to
// allocate the table frame, a bookkeeping structure for field presence.
auto builder = fuchsia_examples::wire::User::Builder(arena);

// To set a table field, call the member function with the same name on the
// builder. The arguments will be forwarded to the field constructor, and the
// field is allocated on the initial |arena|.
builder.age(10);

// Note that only the inline portion of the field is automatically placed in
// the arena. The field itself may reference its own out-of-line content,
// such as in the case of |name| whose type is |fidl::StringView|. |name|
// will reference the "jdoe" literal, which lives in static program storage.
builder.name("jdoe");

// Call |Build| to finalize the table builder into a |User| table.
// The builder is no longer needed after this point. |user| will continue to
// reference objects allocated in the |arena|.
fuchsia_examples::wire::User user = builder.Build();
ASSERT_FALSE(user.IsEmpty());

// Before accessing a field, one should check if it is present, by querying
// |has_...|. Accessing an absent field will panic.
ASSERT_TRUE(user.has_name());
ASSERT_EQ(user.name().get(), "jdoe");

// Setters may be chained, leading to a fluent syntax.
user = fuchsia_examples::wire::User::Builder(arena).age(30).name("bob").Build();
ASSERT_FALSE(user.IsEmpty());
ASSERT_TRUE(user.has_age());
ASSERT_EQ(user.age(), 30);
ASSERT_TRUE(user.has_name());
ASSERT_EQ(user.name().get(), "bob");

// A default constructed wire table is empty.
// This is mostly useful to make requests or replies with empty tables.
fuchsia_examples::wire::User defaulted_user;
ASSERT_TRUE(defaulted_user.IsEmpty());

// In some situations it could be difficult to provide an arena when
// constructing tables. For example, here it is hard to provide constructor
// arguments to 10 tables at once. Because a default constructed wire table is
// empty, a new table instance should be built and assigned in its place.
fidl::Array<fuchsia_examples::wire::User, 10> users;
for (auto& user : users) {
  ASSERT_TRUE(user.IsEmpty());
  user = fuchsia_examples::wire::User::Builder(arena).age(30).Build();
  ASSERT_FALSE(user.IsEmpty());
  ASSERT_EQ(user.age(), 30);
}
ASSERT_EQ(users[0].age(), 30);

// Finally, tables support checking if it was received with unknown fields.
// A table created by ourselves will never have unknown fields.
ASSERT_FALSE(user.HasUnknownData());

For more information on the bindings, see the bindings reference.

Convert between natural and wire domain objects

To streamline interoperability, you may call fidl::ToWire and fidl::ToNatural functions to convert between wire and natural domain objects. Using the fuchsia.examples/User FIDL type as an example:

Convert from natural to wire: fidl::ToWire

// Let's start with a natural table.
fuchsia_examples::User user{{.age = 100, .name = "foo"}};

// To convert it to its corresponding wire domain object, we need a
// |fidl::AnyArena| implementation to allocate the storage, here an |arena|.
fidl::Arena arena;

// Call |fidl::ToWire| with the arena and the natural domain object.
// All out-of-line fields will live on the |arena|.
fuchsia_examples::wire::User wire_user = fidl::ToWire(arena, user);
ASSERT_TRUE(wire_user.has_age());
ASSERT_EQ(wire_user.age(), 100);
ASSERT_TRUE(wire_user.has_name());
ASSERT_EQ(wire_user.name().get(), "foo");

Convert from wire to natural: fidl::ToNatural

fidl::Arena arena;

// Let's start with a wire table.
fuchsia_examples::wire::User wire_user =
    fuchsia_examples::wire::User::Builder(arena).age(30).name("bob").Build();

// Call |fidl::ToNatural| with the wire domain object.
// All child fields will be owned by |user|.
fuchsia_examples::User user = fidl::ToNatural(wire_user);
ASSERT_TRUE(user.age().has_value());
ASSERT_EQ(user.age().value(), 30);
ASSERT_TRUE(user.name().has_value());
ASSERT_EQ(user.name().value(), "bob");