新的 C++ 绑定分为自然和线两种变体。自然绑带在安全性和人体工程学方面进行了优化,而线缆绑带在性能方面进行了优化。大多数代码都应使用自然绑定。 只有在性能要求较高或需要精确控制内存分配时,才应使用有线绑定。
库
假设有以下库声明:
library fuchsia.examples;
协议类型是在 fuchsia_examples 命名空间中生成的。此库的网域对象在 fuchsia_examples::wire 命名空间中生成,而测试框架在 fidl::testing 命名空间中生成。
生成的类型名称会进行转换,以遵循 Google C++ 样式指南。
常量
常量以 constexpr 的形式生成。例如,以下常量:
const BOARD_SIZE uint8 = 9;
const NAME string = "Tic-Tac-Toe";
在头文件中生成为:
constexpr uint8_t kBoardSize = 9u;
extern const char[] kName;
内置类型中概述了 FIDL 基元类型与 C++ 类型之间的对应关系。字符串不再声明为 constexpr,而是声明为头文件中的 extern const char[],并在 .cc 文件中定义。
字段
本部分介绍了 FIDL 工具链如何将 FIDL 类型转换为 C++ 有线类型。这些类型可以作为聚合类型中的成员或作为协议方法的形参出现。
内置类型
FIDL 类型会根据下表转换为 C++ 类型:
| FIDL 类型 | C++ Wire 类型 |
|---|---|
bool |
bool,(要求 sizeof(bool) == 1) |
int8 |
int8_t |
int16 |
int16_t |
int32 |
int32_t |
int64 |
int64_t |
uint8 |
uint8_t |
uint16 |
uint16_t |
uint32 |
uint32_t |
uint64 |
uint64_t |
float32 |
float |
float64 |
double |
array<T, N> |
fidl::Array<T, N> |
vector<T>:N |
fidl::VectorView<T> |
string |
fidl::StringView |
client_end:P |
fidl::ClientEnd<P> |
server_end:P |
fidl::ServerEnd<P> |
zx.Handle |
zx::handle |
zx.Handle:S |
尽可能使用相应的 zx 类型。例如,zx::vmo 或 zx::channel。 |
可选的向量、字符串、客户端/服务器端和句柄具有与其非可选对应项相同的 C++ 有线类型。
用户定义的类型
C++ 有线绑定为每个用户定义的位、枚举、结构、表和联合定义了一个类。对于严格枚举,它们定义的是 enum class,而不是常规类。对于联合类型,它们使用相同的类来表示可选值和非可选值。对于装箱结构体,它们使用 fidl::ObjectView。如需详细了解 fidl::ObjectView,请参阅有线网域对象的内存所有权。
类型定义
块数
假设 bit 定义如下:
type FileMode = strict bits : uint16 {
READ = 0b001;
WRITE = 0b010;
EXECUTE = 0b100;
};
FIDL 工具链会生成一个 FileMode 类,其中包含每个标志的静态成员,以及一个 kMask 成员,其中包含所有位成员(在本例中为 0b111)的掩码:
const static FileMode kReadconst static FileMode kWriteconst static FileMode kExecuteconst static FileMode kMask
FileMode 提供以下方法:
explicit constexpr FileMode(uint16_t):从底层原始值构造一个值,同时保留任何未知的位成员。constexpr static std::optional<FileMode> TryFrom(uint16_t value):如果值不包含任何未知成员,则根据基础原始值构造位实例;否则,返回std::nullopt。constexpr static FileMode TruncatingUnknown(uint16_t value):根据基础原始值构造一个位实例,并清除所有未知成员。- 按位运算符:提供了
|、|=、&、&=、^、^=和~运算符的实现,允许对位(例如mode |= FileMode::kExecute)执行按位运算。 - 比较运算符
==和!=。 - 适用于
uint16_t和bool的显式转换函数。
如果 FileMode 为 flexible,则它将具有以下额外方法:
constexpr FileMode unknown_bits() const:返回一个位值,其中仅包含此位值中的未知成员。constexpr bool has_unknown_bits() const:返回此值是否包含任何未知位。
用法示例:
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);
枚举
假设有以下 enum 定义:
type LocationType = strict enum {
MUSEUM = 1;
AIRPORT = 2;
RESTAURANT = 3;
};
FIDL 工具链会使用指定的底层类型生成 C++ enum class,如果未指定底层类型,则生成 uint32_t:
enum class LocationType : uint32_t {
kMuseum = 1u;
kAirport = 2u;
kRestaurant = 3u;
};
用法示例:
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);
灵活的枚举
灵活的枚举实现为 class 而不是 enum class,并具有以下方法:
constexpr LocationType():默认构造函数,用于将枚举初始化为未指定的未知值。constexpr LocationType(uint32_t value):显式构造函数,用于接收枚举的基础类型的值。constexpr bool IsUnknown():返回枚举值是否未知。constexpr static LocationType Unknown():返回保证会被视为未知值的枚举值。如果枚举具有使用[Unknown]注释的成员,则返回该成员的值。如果不存在此类成员,则返回的枚举成员的底层值未指定。explicit constexpr operator int32_t() const:将枚举转换回其基础值。
生成的类包含每个枚举成员的静态成员,这些成员保证与等效严格枚举中的 enum class 成员匹配:
const static LocationType kMuseumconst static LocationType kAirportconst static LocationType kRestaurant
结构体
假设有以下 struct 声明:
type Color = struct {
id uint32;
@allow_deprecated_struct_defaults
name string:MAX_STRING_LENGTH = "red";
};
FIDL 工具链会生成等效的 struct:
struct Color {
uint32_t id = {};
fidl::StringView name = {};
}
C++ 绑定不支持默认值,而是将结构体的所有字段初始化为零。
用法示例:
// 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");
联合体
假设有以下联合定义:
type JsonValue = strict union {
1: int_value int32;
2: string_value string:MAX_STRING_LENGTH;
};
FIDL 将生成一个 JsonValue 类。JsonValue 包含一个表示可能变体的公共标记枚举类:
enum class Tag : fidl_xunion_tag_t {
kIntValue = 2,
kStringValue = 3,
};
Tag 的每个成员都有一个与其在 union 定义中指定的序号相匹配的值。
JsonValue 提供以下方法:
JsonValue():默认构造函数。在设置变体之前,构造的联合最初处于“缺席”状态。应尽可能优先使用WithFoo构造函数。~JsonValue():用于清除底层联合数据的析构函数。JsonValue(JsonValue&&):默认移动构造函数。JsonValue& operator=(JsonValue&&):默认移动分配static JsonValue WithIntValue(fidl::ObjectView<int32>)和static JsonValue WithStringValue(fidl::ObjectView<fidl::StringView>):直接构造联合的特定变体的静态构造函数。bool has_invalid_tag():如果JsonValue的实例尚未设置变体,则返回true。如果不先设置变体就调用此方法,会导致断言错误。bool is_int_value() const和bool is_string_value() const:每个变体都有一个关联的方法,用于检查JsonValue的实例是否属于该变体const int32_t& int_value() const和const fidl::StringView& string_value() const:每个变体的只读访问器方法。如果不先设置变体就调用这些方法,会导致断言错误。int32_t& int_value()和fidl::StringView& string_value():每个变体的可变访问器方法。如果JsonValue未设置指定的变体,这些方法将失败Tag Which() const:返回JsonValue的当前标记。如果不先设置变体就调用此方法,会导致断言错误。
用法示例:
// 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 invalid.
// It must be initialized with a valid member before use.
// One is not allowed to send invalid unions through FIDL client/server APIs.
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");
// Optional unions are represented with |fidl::WireOptional|.
fidl::WireOptional<fuchsia_examples::wire::JsonValue> optional_json;
ASSERT_FALSE(optional_json.has_value());
optional_json = fuchsia_examples::wire::JsonValue::WithIntValue(42);
ASSERT_TRUE(optional_json.has_value());
// |fidl::WireOptional| has a |std::optional|-like API.
fuchsia_examples::wire::JsonValue& value = optional_json.value();
ASSERT_TRUE(value.is_int_value());
// When switching over the tag from a flexible union, one must add a `default:`
// case, to handle members not understood by the FIDL schema or to handle
// newly added members in a source compatible way.
fuchsia_examples::wire::FlexibleJsonValue flexible_value =
fuchsia_examples::wire::FlexibleJsonValue::WithIntValue(1);
switch (flexible_value.Which()) {
case fuchsia_examples::wire::FlexibleJsonValue::Tag::kIntValue:
ASSERT_EQ(flexible_value.int_value(), 1);
break;
case fuchsia_examples::wire::FlexibleJsonValue::Tag::kStringValue:
FAIL() << "Unexpected tag. |flexible_value| was set to int";
break;
default: // Removing this branch will fail to compile.
break;
}
灵活的联合和未知变体
灵活的联合在生成的 Tag 类中有一个额外的变体:
enum class Tag : fidl_xunion_tag_t {
... // other fields omitted
kUnknown = ::std::numeric_limits<::fidl_union_tag_t>::max(),
};
当包含具有未知变体的联合的 FIDL 消息被解码为 JsonValue 时,JsonValue::Which() 将返回 JsonValue::Tag::kUnknown。
C++ 绑定不存储未知变体的原始字节和句柄。
不支持对具有未知变体的联合进行编码,这会导致编码失败。
表格
假设有以下表格定义:
type User = table {
1: age uint8;
2: name string:MAX_STRING_LENGTH;
};
FIDL 工具链会生成一个 User 类,其中包含以下方法:
User():默认构造函数,用于初始化一个未设置任何字段的空表格。User::Builder(fidl::AnyArena& arena):构建器工厂。 返回一个fidl::WireTableBuilder<User>,用于从提供的 arena 分配帧和成员。User::ExternalBuilder(fidl::ObjectView<fidl::WireTableFrame<User>> frame): 外部构建器工厂。返回包含所提供帧的fidl::WireTableExternalBuilder<User>。此构建器需要谨慎的内存管理,但有时可能很有用。买者自慎。User(User&&):默认移动构造函数。~User():默认析构函数。User& operator=(User&&):默认移动分配。bool IsEmpty() const:如果未设置任何字段,则返回 true。bool has_age() const和bool has_name() const:返回字段是否已设置。const uint8_t& age() const和const fidl::StringView& name() const:只读字段访问器方法。如果不先设置相应字段就调用这些方法,会导致断言错误。
为了构建表格,系统会生成三个额外的类:fidl::WireTableBuilder<User>、fidl::WireTableExternalBuilder<User> 和 fidl::WireTableFrame<User>。
fidl::WireTableFrame<User> 是表的内部存储空间容器,与构建器分开分配,以保持底层有线格式的对象布局。它仅供 build 内部使用。
fidl::WireTableFrame<User> 具有以下方法:
WireTableFrame():默认构造函数。
fidl::WireTableExternalBuilder<User> 具有以下方法:
fidl::WireTableExternalBuilder<User> age(uint8_t):通过将年龄内嵌到表格框架中来设置年龄。fidl::WireTableExternalBuilder<User> name(fidl::ObjectView<fidl::StringView>):设置具有已分配值的名称。User Build():构建并返回表格对象。调用Build()后,必须舍弃构建器。
fidl::WireTableBuilder<User> 具有 fidl::WireTableExternalBuilder<User> 的所有方法(但 setter 的返回类型正确),并添加了以下方法:
fidl::WireTableBuilder<User> name(std::string_view):通过从构建器的 arena 分配新的fidl::StringView并将提供的字符串复制到其中来设置名称。
用法示例:
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());
除了使用 fidl::ObjectView 分配字段之外,您还可以使用有线网域对象的内存所有权中所述的任何分配策略。
内嵌布局
生成的 C++ 代码使用 fidlc 为内嵌布局选择的名称。
C++ 绑定还会生成用于引用内嵌布局的作用域名称。例如,对于以下 FIDL:
type Outer = struct {
inner struct {};
};
可以使用其全局唯一名称 Inner 以及作用域名称 Outer::Inner 来引用内部结构体。当使用 @generated_name 属性替换顶级名称时,这会非常有用,例如在以下情况下:
type Outer = struct {
inner
@generated_name("SomeCustomName") struct {};
};
内部结构体可以称为 SomeCustomName 或 Outer::Inner。
另一个示例是协议结果类型:TicTacToe_MakeMove_Result 等类型的成功变体和错误变体可以分别引用为 TicTacToe_MakeMove_Result::Response 和 TicTacToe_MakeMove_Result::Err。
协议
假设协议如下:
closed protocol TicTacToe {
strict StartGame(struct {
start_first bool;
});
strict MakeMove(struct {
row uint8;
col uint8;
}) -> (struct {
success bool;
new_state box<GameState>;
});
strict -> OnOpponentMove(struct {
new_state GameState;
});
};
FIDL 将生成一个 TicTacToe 类,该类充当类型和类的入口点,客户端和服务器都将使用这些类型和类与此服务进行交互。本部分其余各个子部分将介绍此类的成员。
类型化渠道端点
C++ 绑定通过 Zircon 渠道传输发送和接收 FIDL 协议消息,这些消息携带任意字节 Blob 和句柄。该 API 不会公开原始端点(例如 zx::channel),而是公开三个模板化端点类:
fidl::ClientEnd<TicTacToe>:TicTacToe协议的客户端端点;它拥有zx::channel。需要独占频道所有权的客户端绑定会使用此类型。例如,fidl::WireClient<TicTacToe>可以由fidl::ClientEnd<TicTacToe>构建,也称为“将渠道绑定到消息调度程序”。fidl::UnownedClientEnd<TicTacToe>:一个借用TicTacToe协议的某个客户端端点的不拥有值。不需要对频道具有独占所有权的客户端 API 将采用此类型。UnownedClientEnd可以通过调用borrow()从同一协议类型的ClientEnd派生。借用自端点可以在同一进程中进行std::move,但在存在未拥有的借用时,无法在进程外丢弃或转移该端点。fidl::ServerEnd<TicTacToe>:TicTacToe协议的服务器端点;它拥有zx::channel。需要独占频道所有权的服务器绑定会使用此类型。例如,可以向fidl::BindServer<TicTacToe>提供fidl::ServerEnd<TicTacToe>(以及其他参数)来创建服务器绑定。
没有 UnownedServerEnd,因为目前还不需要它来安全地实现当前的功能集。
可以使用 ::fidl::CreateEndpoints<TicTacToe> 库调用创建一对客户端和服务器端点。在协议请求流水线处理方案中,在通过 std::move() 将客户端端点连接到远程服务器后,可以立即开始对客户端端点执行操作。
如需了解详情,请参阅有关这些类型的类文档。
请求和响应类型
可通过一对别名(fidl::WireRequest 和 fidl::WireEvent)访问 FIDL 方法或事件的请求类型:
fidl::WireRequest<TicTacToe::StartGame>fidl::WireRequest<TicTacToe::MakeMove>fidl::WireEvent<TicTacToe::OnOpponentMove>
如果请求或事件使用的类型是命名类型,则别名将指向该类型。如果请求类型是匿名类型,则别名将指向为该匿名类型生成的类型名称。对于方法请求和事件,生成的请求类型都命名为 [Method]Request。
与请求不同,双向方法的响应会生成为一种新类型,即 fidl::WireResult:
fidl::WireResult<TicTacToe::MakeMove>
fidl::WireResult 类型继承自 fidl::Status,其状态表示调用在 FIDL 层是否成功。如果方法具有非空响应或使用 FIDL 错误语法,则生成的 WireResult 类型还将具有一组用于访问返回值或应用层错误的访问器。包含的结果可用的访问器如下:
WireResultUnwrapType<FidlMethod>* Unwrap()const WireResultUnwrapType<FidlMethod>* Unwrap() constWireResultUnwrapType<FidlMethod>& value()const WireResultUnwrapType<FidlMethod>& value() constWireResultUnwrapType<FidlMethod>* operator->()const WireResultUnwrapType<FidlMethod>* operator->() constWireResultUnwrapType<FidlMethod>& operator*()const WireResultUnwrapType<FidlMethod>& operator*() const
WireResultUnwrapType 是另一种类型别名,具体取决于方法是否使用错误语法。假设有以下示例库,
library response.examples;
protocol Test {
Foo() -> (struct { x int32; });
Bar() -> () error int32;
Baz() -> (struct { x int32; }) error int32;
};
以下是 Test 协议中每种方法的 fidl::WireResultUnwrapType:
fidl::WireResultUnwrapType<response_examples::Test::Foo> = response_examples::wire::TestFooResponsefidl::WireResultUnwrapType<response_examples::Test::Bar> = fit::result<int32_t>fidl::WireResultUnwrapType<response_examples::Test::Baz> = fit::result<int32_t, ::response_examples::wire::TestBazResponse*>
客户端
C++ 有线绑定提供了多种方式来以客户端身份与 FIDL 协议进行交互:
fidl::WireClient<TicTacToe>:此类针对出站异步和同步调用以及异步事件处理公开了线程安全 API。它拥有渠道的客户端。需要async_dispatcher_t*来支持异步 API 以及事件和错误处理。 必须与单线程调度程序搭配使用。此类对象必须绑定到客户端端点,并在运行调度程序的同一线程上销毁。这是推荐的变体,适用于大多数使用情形,但无法使用async_dispatcher_t或需要将客户端在线程之间移动的情形除外。fidl::WireSharedClient<TicTacToe>:与WireClient相比,此类对线程模型的要求较低,但需要两阶段关闭模式来防止释放后使用。此类对象可能会在任意线程上销毁。它还支持与多线程调度程序搭配使用。如需了解详情,请参阅新的 C++ 绑定线程指南。fidl::WireSyncClient<TicTacToe>:此类公开了用于传出调用和事件处理的纯同步 API。它拥有渠道的客户端。fidl::WireCall<TicTacToe>:此类与WireSyncClient相同,只是它不拥有通道客户端端的控制权。在实现采用原始zx_handle_t的 C API 时,WireCall可能比WireSyncClient更可取。
WireClient
fidl::WireClient 是线程安全的,支持同步和异步调用,以及异步事件处理。
创建
客户端是使用协议 P 的客户端端 fidl::ClientEnd<P>、async_dispatcher_t* 和指向 WireAsyncEventHandler 的可选指针创建的,该指针定义了在收到 FIDL 事件或客户端处于未绑定状态时要调用的方法。如果未替换特定事件的虚拟方法,则会忽略该事件。
class EventHandler : public fidl::WireAsyncEventHandler<TicTacToe> {
public:
EventHandler() = default;
void OnOpponentMove(fidl::WireEvent<OnOpponentMove>* event) override {
/* ... */
}
void on_fidl_error(fidl::UnbindInfo unbind_info) override { /* ... */ }
};
fidl::ClientEnd<TicTacToe> client_end = /* logic to connect to the protocol */;
EventHandler event_handler;
fidl::WireClient<TicTacToe> client;
client.Bind(std::move(client_end), dispatcher, &event_handler);
如果服务器端关闭或收到来自服务器的无效消息,绑定可能会自动拆除。您还可以通过销毁客户端对象来主动拆除绑定。
传出 FIDL 方法
您可以通过 fidl::WireClient 实例调用传出 FIDL API。对 fidl::WireClient 进行解引用可访问以下方法:
对于
StartGame(触发和忘记):fidl::Status StartGame(bool start_first):一种有管理的“触发后即忘记”方法。
对于
MakeMove(双向):[...] MakeMove(uint8_t row, uint8_t col):异步双向方法的受管理变体。它会返回一个内部类型,该类型必须用于注册异步延续以接收结果,例如回调。请参阅指定异步延续。除非调度程序正在关闭,否则将在调度程序线程上执行续延。
fidl::WireClient::buffer 可用于访问以下方法:
fidl::Status StartGame(bool start_first):由调用方分配的 fire-and-forget 方法的变体。[...] MakeMove(uint8_t row, uint8_t col):双向方法的异步、调用方分配变体。它返回的内部类型与受管理的变体中的内部类型相同。
fidl::WireClient::sync 可用于访问以下方法:
fidl::WireResult<MakeMove> MakeMove(uint8_t row, uint8_t col):双向方法的同步受管理变体。WireSyncClient上也存在相同的方法。
指定异步延续
请参阅相应的 C++ 文档注释。
系统会使用表示成功解码的响应或错误的 result 对象来调用 continuation。当用户需要将每个 FIDL 调用的错误传播给其发起者时,此方法非常有用。例如,服务器可能需要在处理现有 FIDL 调用的同时进行另一项 FIDL 调用,并且需要在发生错误时使原始调用失败。
双向调用返回的对象上有一些方法:
Then:接受回调,并在客户端消失之前最多调用一次回调。ThenExactlyOnce:如果传递了回调,则无论调用成功还是失败,回调都会执行一次。不过,由于回调是异步调用的,因此在销毁客户端时,请注意释放后使用 bug:回调捕获的对象可能无效。如果需要控制分配,
ThenExactlyOnce还可以采用响应上下文。TicTacToe只有一个响应上下文fidl::WireResponseContext<TicTacToe::MakeMove>,其中包含纯虚方法,应覆盖这些方法以处理调用的结果:
virtual void OnResult(fidl::WireUnownedResult<MakeMove>& result) = 0;
系统会使用表示成功解码的响应或错误的 result 对象来调用 OnResult。您有责任确保响应上下文对象的生命周期长于整个异步调用的持续时间,因为 fidl::WireClient 通过地址借用上下文对象以避免隐式分配。
集中式错误处理程序
如果因错误而拆除绑定,系统将从调度程序线程调用 fidl::WireAsyncEventHandler<TicTacToe>::on_fidl_error 并提供详细原因。如果错误是调度程序关闭,则将从调用调度程序关闭的线程调用 on_fidl_error。建议将用于记录或释放资源的所有中央逻辑放在该处理程序中。
WireSyncClient
fidl::WireSyncClient<TicTacToe> 是一个同步客户端,提供以下方法:
explicit WireSyncClient(fidl::ClientEnd<TicTacToe>):构造函数。~WireSyncClient():默认析构函数。WireSyncClient(&&):默认移动构造函数。WireSyncClient& operator=(WireSyncClient&&):默认移动分配。const fidl::ClientEnd<TicTacToe>& client_end() const:返回底层客户端点。fidl::Status StartGame(bool start_first):一种有管理的“触发后即忘”方法调用变体。请求的缓冲区分配完全在此函数内处理。fidl::WireResult<TicTacToe::MakeMove> MakeMove(uint8_t row, uint8_t col):双向方法调用的受管理变体,它将参数作为实参,并返回一个WireResult对象。请求和响应的缓冲区分配完全在此函数内处理。绑定会在编译时根据 FIDL 有线格式和长度上限约束条件计算出适合此调用的安全缓冲区大小。如果缓冲区大小不超过 512 字节,则在堆栈上分配;否则在堆上分配。如需详细了解缓冲区管理,请参阅 WireResult。fidl::Status HandleOneEvent(SyncEventHandler& event_handler):用于从渠道中准确消耗一个事件的块。如果服务器已发送墓志铭,则返回墓志铭中包含的状态。请参阅活动。
fidl::WireSyncClient<TicTacToe>::buffer 提供以下方法:
fidl::WireUnownedResult<TicTacToe::StartGame> StartGame(bool start_first):一种由调用方分配的“即发即弃”调用变体,它接受作为实参传递给buffer的请求缓冲区的后备存储空间以及请求参数,并返回fidl::WireUnownedResult。fidl::WireUnownedResult<TicTacToe::MakeMove> MakeMove(uint8_t row, uint8_t col):双向方法的调用方分配变体,它会请求用于对请求进行编码的空间,以及用于从传递给buffer方法的同一内存资源接收响应的空间。
请注意,每种方法都有自有变体和调用方分配的变体。简而言之,每种方法的自有变体都会处理请求和响应的内存分配,而调用者分配的变体则允许用户自行提供缓冲区。自有变体更易于使用,但可能会导致额外的分配。
WireCall
fidl::WireCall<TicTacToe> 提供的方法与 WireSyncClient 中的方法类似,唯一的区别在于 WireCall 可以使用 fidl::UnownedClientEnd<TicTacToe> 构建,即它会借用客户端端点:
fidl::WireResult<StartGame> StartGame(bool start_first):StartGame的自有变体。fidl::WireResult<MakeMove> MakeMove(uint8_t row, uint8_t col):MakeMove的自有变体。
fidl::WireCall<TicTacToe>(client_end).buffer 提供以下方法:
fidl::WireUnownedResult<StartGame> StartGame(bool start_first):由调用方分配的StartGame变体。fidl::WireUnownedResult<MakeMove> MakeMove(uint8_t row, uint8_t col);:由调用方分配的MakeMove变体。
Result、WireResult 和 WireUnownedResult
WireSyncClient 和 WireCall 的每个方法的受管理变体都会返回 fidl::WireResult<Method> 类型,而调用方分配变体都会返回 fidl::WireUnownedResult<Method>。fidl::WireClient 上的“即发即弃”方法会返回 fidl::Status。这些类型定义了相同的一组方法:
zx_status status() const返回传输状态。它返回在以下过程中遇到的第一个错误(如果适用):线性化、编码、对底层渠道进行调用,以及解码结果。如果状态为ZX_OK,则表示调用成功,反之亦然。- 如果
status()不为ZX_OK,fidl::Reason reason() const会返回有关哪个操作失败的详细信息。例如,如果编码失败,reason()将返回fidl::Reason::kEncodeError。当状态为ZX_OK时,不应调用reason()。 - 当状态不是
ZX_OK时,const char* error_message() const包含简短的错误消息。否则,返回nullptr。 - (仅适用于双向调用的 WireResult 和 WireUnownedResult)
T* Unwrap()返回指向响应结构体的指针。对于WireResult,指针指向结果对象拥有的内存。对于WireUnownedResult,指针指向调用方提供的缓冲区。只有在状态为ZX_OK时,才能调用Unwrap()。
此外,双向调用的 WireResult 和 WireUnownedResult 将实现返回响应结构本身的解引用运算符。这样一来,您就可以使用如下代码:
fidl::WireResult result = client.sync()->MakeMove(0, 0);
auto* response = result.Unwrap();
bool success = response->success;
可简化为:
fidl::WireResult result = client.sync()->MakeMove(0, 0);
bool success = result->success;
WireResult<Method>管理所有缓冲区和句柄的所有权,而::Unwrap()返回其视图。因此,此对象必须比对未封装的响应的任何引用存活更长时间。
分配策略和移动语义
如果消息保证在 512 字节以下,WireResult 会内联存储响应缓冲区。由于结果对象通常在调用者的堆栈上实例化,因此实际上这意味着,当响应足够小时,它会进行堆栈分配。如果最大响应大小超过 512 字节,WireResult 将包含一个堆分配的缓冲区。
因此,不支持在 WireResult 上使用 std::move()。如果缓冲区是内联的,则必须复制内容;如果缓冲区是外联的,则必须将指向外联对象的指针更新为目标对象内的位置。对于通常被认为成本较低的移动操作,这些都是令人惊讶的开销。
如果结果对象需要在多个函数调用之间传递,请考虑在最外层的函数中预先分配缓冲区,并使用调用方分配的风格。
服务器
为 FIDL 协议实现服务器涉及提供 TicTacToe 的具体实现。
生成的 fidl::WireServer<TicTacToe> 类具有与 FIDL 协议中定义的方法调用相对应的纯虚方法。用户通过提供 fidl::WireServer<TicTacToe> 的具体实现来实现 TicTacToe 服务器,该实现具有以下纯虚方法:
virtual void StartGame(StartGameRequestView request, StartGameCompleter::Sync& completer)virtual void MakeMove(MakeMoveRequestView request, MakeMoveCompleter::Sync& completer)
如需了解如何绑定和设置服务器实现,请参阅示例 C++ 服务器。
C++ 有线绑定还提供了一些函数,用于根据实现手动调度消息,例如 fidl::WireDispatch<TicTacToe>:
void fidl::WireDispatch<TicTacToe>(fidl::WireServer<TicTacToe>* impl, fidl::IncomingMessage&& msg, ::fidl::Transaction* txn):调度传入的消息。如果没有匹配的处理程序,它会关闭消息中的所有句柄,并通知txn发生错误。
请求
请求作为每个生成的 FIDL 方法处理程序的第一个实参提供。这是请求的视图(一个指针)。所有请求实参都使用箭头运算符和实参名称进行访问。
例如:
request->start_firstrequest->row
如需了解请求生命周期的相关注意事项,请参阅有线网域对象的内存所有权。
完成者
在每个生成的 FIDL 方法处理程序中,completer 作为最后一个实参提供,位于相应方法的所有 FIDL 请求形参之后。完成器类捕获了完成 FIDL 交易的各种方式,例如通过发送回复、使用墓志铭关闭渠道等,并且有同步和异步版本(尽管 ::Sync 类默认作为实参提供)。在此示例中,这些补全器包括:
fidl::WireServer<TicTacToe>::StartGameCompleter::Syncfidl::WireServer<TicTacToe>::StartGameCompleter::Asyncfidl::WireServer<TicTacToe>::MakeMoveCompleter::Syncfidl::WireServer<TicTacToe>::MakeMoveCompleter::Async
所有补全器类都提供以下方法:
void Close(zx_status_t status):关闭渠道并发送status作为墓志铭。
此外,双向方法将提供两个版本的 Reply 方法来回复响应:一个受管理变体和一个调用方分配变体。这些对应于 Client API 中的变体。例如,MakeMoveCompleter::Sync 和 MakeMoveCompleter::Async 都提供以下 Reply 方法:
::fidl::Status Reply(bool success, fidl::ObjectView<GameState> new_state)::fidl::Status Reply(fidl::BufferSpan _buffer, bool success, fidl::ObjectView<GameState> new_state)
由于 Reply 返回的状态与取消绑定状态相同,因此可以安全地忽略。
最后,可以使用 ToAsync() 方法将双向方法的同步完成器转换为异步完成器。异步完成器可以超出处理程序的范围,例如将其移至 lambda 捕获中,从而允许服务器异步响应请求。异步完成器具有与同步完成器相同的用于响应客户端的方法。如需查看使用示例,请参阅异步响应请求
并行消息处理
默认情况下,来自单个绑定的消息会按顺序处理,即如果需要,附加到调度程序(运行循环)的单个线程会被唤醒,读取消息,执行处理程序,然后返回到调度程序。::Sync 完成器提供了一个额外的 API EnableNextDispatch(),可用于有选择地打破此限制。具体而言,对此 API 的调用将使等待调度程序的另一个线程能够处理绑定上的下一个消息,而第一个线程仍处于处理程序中。请注意,对同一 Completer 重复调用 EnableNextDispatch() 具有幂等性。
void DirectedScan(int16_t heading, ScanForPlanetsCompleter::Sync& completer) override {
// Suppose directed scans can be done in parallel. It would be suboptimal to block one scan until
// another has completed.
completer.EnableNextDispatch();
fidl::VectorView<Planet> discovered_planets = /* perform a directed planet scan */;
completer.Reply(std::move(discovered_planets));
}
由调用方分配的方法
上述许多 API 都提供了所生成方法的自有和调用方分配变体。
调用方分配的变体将所有内存分配责任都推迟给调用方。类型 fidl::BufferSpan 引用缓冲区地址和大小。绑定库将使用它来构建 FIDL 请求,因此它必须足够大。方法参数(例如 heading)会线性化到缓冲区内的相应位置。创建缓冲区的途径有很多:
// 1. On the stack
using StartGame = TicTacToe::StartGame;
fidl::SyncClientBuffer<StartGame> buffer;
auto result = client.buffer(buffer.view())->StartGame(true);
// 2. On the heap
auto buffer = std::make_unique<fidl::SyncClientBuffer<StartGame>>();
auto result = client.buffer(buffer->view())->StartGame(true);
// 3. Some other means, e.g. thread-local storage
constexpr uint32_t buffer_size = fidl::SyncClientMethodBufferSizeInChannel<StartGame>();
uint8_t* buffer = allocate_buffer_of_size(buffer_size);
fidl::BufferSpan buffer_span(/* data = */buffer, /* capacity = */request_size);
auto result = client.buffer(buffer_span)->StartGame(true);
// Check the transport status (encoding error, channel writing error, etc.)
if (result.status() != ZX_OK) {
// Handle error...
}
// Don't forget to free the buffer at the end if approach #3 was used...
使用调用方分配的风格时,
result对象会借用请求和响应缓冲区(因此其类型为WireUnownedResult)。请确保缓冲区在result对象之后仍然存在。 请参阅 WireUnownedResult。
事件
在 C++ 绑定中,可以异步或同步处理事件,具体取决于所用客户端的类型。
异步客户端
使用 fidl::WireClient 时,可以通过向类传递 fidl::WireAsyncEventHandler<TicTacToe>* 来异步处理事件。WireAsyncEventHandler 类具有以下成员:
virtual void OnOpponentMove(fidl::WireEvent<OnOpponentMove>* event) {}:OnOpponentMove 事件的处理脚本(每个事件对应一个方法)。virtual on_fidl_error(::fidl::UnbindInfo info) {}:当客户端遇到严重错误时调用的方法。
为了能够处理事件和错误,必须定义一个继承自 fidl::WireAsyncEventHandler<TicTacToe> 的类。
同步客户端
在 WireSyncClient 中,通过调用 HandleOneEvent 函数并向其传递 fidl::WireSyncEventHandler<TicTacToe> 来同步处理事件。
WireSyncEventHandler 是一个类,其中包含每个事件的纯虚方法。在此示例中,它包含以下成员:
virtual void OnOpponentMove(fidl::WireEvent<TicTacToe::OnOpponentMove>* event) = 0:OnOpponentMove 事件的句柄。
为了能够处理事件,必须定义一个继承自 WireSyncEventHandler 的类。此类必须为协议中的所有事件定义虚拟方法。然后,必须创建此类的实例。
处理一个事件的方式有两种。每个都使用用户定义的事件处理脚本类的一个实例:
::fidl::Status fidl::WireSyncClient<TicTacToe>::HandleOneEvent( SyncEventHandler& event_handler):同步客户端的绑定版本。::fidl::Status fidl::WireSyncEventHandler<TicTacToe>::HandleOneEvent( fidl::UnownedClientEnd<TicTacToe> client_end):一种无绑定版本,使用fidl::UnownedClientEnd<TicTacToe>来处理特定处理脚本的一个事件。
对于每次对 HandleOneEvent 的调用,该方法都会在通道上等待一条传入消息。然后对消息进行解码。如果结果为 fidl::Status::Ok(),则表示已调用一个虚拟方法。否则,没有调用任何虚拟方法,并且状态会指示错误。
如果处理程序始终相同(从一次 HandleOneEvent 调用到另一次),则应构建一次 WireSyncEventHandler 对象,并在每次需要调用 HandleOneEvent 时使用该对象。
如果某个事件被标记为过渡事件,则系统会提供默认实现,这会导致 HandleOneEvent 在收到用户未处理的过渡事件时返回错误。
服务器
fidl::WireSendEvent 用于从服务器端发送事件。有两个重载:
fidl::WireSendEvent(const fidl::ServerBindingRef<Protocol>& binding_ref)通过服务器绑定引用发送事件。fidl::WireSendEvent(const fidl::ServerEnd<Protocol>& endpoint)通过端点发送事件。
使用服务器绑定对象发送事件
将服务器实现绑定到通道时,fidl::BindServer 会返回一个 fidl::ServerBindingRef<Protocol>,您可以通过该 fidl::ServerBindingRef<Protocol> 安全地与服务器绑定进行交互。
使用绑定引用调用 fidl::WireSendEvent 会返回一个用于发送事件的接口。
事件发送者接口包含用于发送每个事件的方法。举个具体示例,TicTacToe 的事件发送器接口提供以下方法:
fidl::Status OnOpponentMove(GameState new_state):受管理的配置。
调用 .buffer(...) 会返回一个类似的接口,用于分配调用方分配的变体,从传递给 .buffer 的内存资源分配编码缓冲区,类似于 客户端 API 以及服务器完成器。
使用 ServerEnd 对象发送事件
服务器端点本身由 fidl::ServerEnd<Protocol> 表示。
使用服务器绑定对象发送事件是在服务器端点绑定到实现时发送事件的标准方法。不过,有时可能需要直接在 fidl::ServerEnd<TicTacToe> 对象上发送事件,而无需设置服务器绑定。
fidl::WireSendEvent 接受对 fidl::ServerEnd<Protocol> 的常量引用。
它不支持 zx::unowned_channel,以减少在别处关闭句柄后使用端点的可能性。
结果
假设有以下方法:
protocol TicTacToe {
MakeMove(struct {
row uint8;
col uint8;
}) -> (struct {
new_state GameState;
}) error MoveError;
};
FIDL 将在与具有错误类型的方法对应的 completer 上生成便捷方法。根据 Reply“变体”,完成器将具有 ReplySuccess、ReplyError 或这两种方法,以便直接使用成功或错误数据进行响应,而无需构建联合。
对于受管理的配置,这两种方法均适用:
void ReplySuccess(GameState new_state)void ReplyError(MoveError error)
由于 ReplyError 不需要堆分配,因此对于调用方分配的变种,只有 ReplySuccess:
void ReplySuccess(fidl::BufferSpan _buffer, GameState new_state)
请注意,传入的缓冲区用于保存整个响应,而不仅仅是与成功变体对应的数据。
定期生成的 Reply 方法也可使用:
void Reply(TicTacToe_MakeMove_Result result):自有变体。void Reply(fidl::BufferSpan _buffer, TicTacToe_MakeMove_Result result):调用方分配的变体。
自有和调用方分配的变体使用 TicTacToe_MakeMove_Result 形参,该形参生成为具有两个变体的 union:Response(属于 TicTacToe_MakeMove_Response)和 Err(属于 MoveError)。TicTacToe_MakeMove_Response 生成为 struct,并将响应参数作为其字段。在这种情况下,它只有一个字段 new_state,即 GameState。
未知互动处理
服务器端
当协议声明为 open 或 ajar 时,生成的 fidl::WireServer<Protocol> 类型也会继承自 fidl::UnknownMethodHandler<Protocol>。UnknownMethodHandler 定义了一个服务器必须实现的抽象方法(名为 handle_unknown_method),其签名如下:
virtual void handle_unknown_method(UnknownMethodMetadata<Protocol> metadata,
UnknownMethodCompleter::Sync& completer) = 0;
提供的 UnknownMethodMetadata 是一个结构体,包含一个或两个字段,具体取决于协议是 ajar 还是 open。以下是元数据结构的示例,为简单起见,省略了模板实参:
struct UnknownMethodMetadata {
// Ordinal of the method that was called.
uint64_t method_ordinal;
// Whether the method that was called was a one-way method or a two-way
// method. This field is only defined if the protocol is open, since ajar
// protocols only handle one-way methods.
UnknownMethodType unknown_method_type;
};
UnknownMethodType 是一个枚举,包含两个变体:kOneWay 和 kTwoWay,用于指示调用了哪种方法。
UnknownMethodCompleter 与单向方法使用的完成器类型相同。
客户端
客户端无法判断服务器是否知道某个 flexible 单向方法。对于 flexible 双向方法,如果服务器不知道该方法,则 fidl::WireResult 将具有 fidl::Status 的 ZX_ERR_NOT_SUPPORTED,原因为 fidl::Reason::kUnknownMethod。kUnknownMethod 原因仅适用于灵活的双向方法。
除了可能会因 kUnknownMethod 的 Reason 而出现错误之外,客户端上的 strict 和 flexible 方法之间没有 API 差异。
对于 open 和 ajar 协议,生成的 fidl::WireAsyncEventHandler<Protocol> 和 fidl::WireSyncEventHandler<Protocol> 将继承自 fidl::UnknownEventHandler<Protocol>。UnknownEventHandler 定义了一个事件处理程序必须实现的方法,名为 handle_unknown_event,具有以下签名:
virtual void handle_unknown_event(UnknownEventMetadata<Protocol> metadata) = 0;
UnknownEventMetadata 具有以下布局,为简单起见,省略了模板实参:
struct UnknownEventMetadata {
// Ordinal of the event that was received.
uint64_t event_ordinal;
};
协议组成
FIDL 没有继承的概念,并且会为所有组合协议生成如上所述的完整代码。换句话说,为
protocol A {
Foo();
};
protocol B {
compose A;
Bar();
};
提供与为以下对象生成的代码相同的 API:
protocol A {
Foo();
};
protocol B {
Foo();
Bar();
};
生成的代码完全相同,只是方法序号不同。
协议和方法属性
过渡性
对于使用 @transitional 属性注释的协议方法,协议类上的 virtual 方法附带默认的 Close(ZX_NOT_SUPPORTED) 实现。这样,缺少方法替换的协议类实现就可以成功编译。
可检测到
使用 @discoverable 属性注释的协议会导致 FIDL 工具链在协议类上生成一个额外的 static const char
Name[] 字段,其中包含完整的协议名称。
持久性以及 FIDL 线格式的独立使用
目前尚不支持单独使用 FIDL 有线格式,例如对单个 FIDL 网域对象进行编码和解码 (https://fxbug.dev/42163274)。
测试脚手架
FIDL 工具链还会生成一个名为 wire_test_base.h 的文件,其中包含用于测试 FIDL 客户端和服务器实现的便捷代码。如需使用这些标头,请依赖于带有 _testing 后缀(而非 my_library_cpp)的绑定目标标签 my_library_cpp_testing。
服务器测试库
测试基本标头包含每个协议的类,这些类为每个类的方法提供桩实现,从而使您能够仅实现测试期间使用的方法。这些类是 fidl::testing::WireTestBase<Protocol> 或 fidl::testing::TestBase<Protocol> 的模板专用化,其中 Protocol 是已桩化的 FIDL 协议(例如,对于协议 games.tictactoe/TicTacToe,测试基类为 fidl::testing::WireTestBase<games_tictactoe::TicTacToe> 或 fidl::testing::TestBase<games_tictactoe::TicTacToe>)。
对于上面列出的同一 TicTacToe 协议,生成的测试基类子类 fidl::WireServer<TicTacToe> 和 fidl::Server<TicTacToe>(请参阅协议)提供以下方法:
virtual ~WireTestBase() = default或virtual ~TestBase() = default:析构函数。virtual void NotImplemented_(const std::string& name, ::fidl::CompleterBase& completer) = 0:纯虚方法,会被替换以定义未实现方法的行为。
测试基准为虚拟协议方法 StartGame 和 MakeMove 提供了一种实现,该实现仅分别调用 NotImplemented_("StartGame", completer) 和 NotImplemented_("MakeMove",
completer)。
同步事件处理程序测试基类
测试基本标头包含每个协议的类,这些类为每个类的事件提供桩实现,从而可以仅实现测试期间使用的事件。与服务器测试基准类似,这些类是 fidl::testing::WireSyncEventHandlerTestBase<Protocol> 的模板专业化,其中 Protocol 是已桩化的 FIDL 协议。
对于上面列出的同一 TicTacToe 协议,生成的测试基类子类 fidl::WireSyncEventHandler<TicTacToe>(请参阅协议)提供以下事件:
virtual ~WireSyncEventHandlerTestBase() = default:析构函数。virtual void NotImplemented_(const std::string& name) = 0:纯虚方法,会被替换以定义未实现事件的行为。
测试基准为虚拟协议事件 OnOpponentMove 提供了一个实现,该实现仅用于调用 NotImplemented_("OnOpponentMove")。