低层 C++ 类型通道迁移

目标与动力

FIDL 协议和协议请求从本质上由 Zircon 通道提供支持。假设有以下 FIDL 定义:

library foo;

protocol Calculator {};

resource struct Record {
    // Client endpoint of a channel speaking the Calculator protocol
    Calculator c;
    // Server endpoint of a channel speaking the Calculator protocol
    request<Calculator> s;
};

我们之前在 LLCPP 中生成了包含两个 Zircon 通道的结构体:

struct Record {
    zx::channel c;
    zx::channel s;
};

任何 FIDL 协议都变成了一个通道,这为意外混淆协议类型或方向打开了大门(下面是一些已被识别 修复的情况)。为了提高类型安全性和自行编写文档,我们已将生成的代码更改为以下内容:

struct Record {
    // Now it's clear that |c| is a client channel endpoint speaking the |Calculator| protocol.
    fidl::ClientEnd<foo::Calculator> c;
    // Similarly, |s| is a server channel endpoint for that protocol.
    fidl::ServerEnd<foo::Calculator> s;
};

同样,LLPP 运行时中之前处理 zx::channel 的所有函数都进行了更新,以表示更精确的类型,对协议的方向和种类进行编码(例如:fidl::BindServer)。

不过,大多数用户代码仍然使用 zx::channel。它们会继续编译,因为我们为 fidl::ClientEnd / fidl::ServerEnd 添加了临时的隐式转换支持,但代价是类型安全。为了在整个代码库中带来这项更改的优势,用户代码应通过其公共接口传播 fidl::ClientEnd / fidl::ServerEnd 类型,而不是从原始通道在本地进行类型转换。

技术背景

LLCPP 类型信道参考

如何提供帮助

选择任务

搜索包含字符串 TODO(https://fxbug.dev/42148734) 的 BUILD.gn 文件。如下所示:

# TODO(https://fxbug.dev/42148734): This target uses raw zx::channel with LLCPP which is deprecated.
# Please migrate to typed channel APIs (fidl::ClientEnd<T>, fidl::ServerEnd<T>).
# See linked bug for details.
configs += [ "//build/cpp:fidl-llcpp-deprecated-raw-channels" ]

移除这些行和 fx build。如果构建成功,没有任何警告或错误,请跳到最后一步。否则,警告和错误会指向已弃用的用法。接下来,您将了解三种典型的场景:

场景 1:实现服务器

迁移服务器非常简单,您需要查找服务器实现从名为 RawChannelInterface 的类继承的位置。该类是一个 shim,可将接受 fidl::ClientEnd<P> / fidl::ServerEnd<P> 参数的服务器方法转换为接受 zx::channel 的方法。将其更改为常用的 Interface,并更新方法参数以匹配以下内容:

FIDL

protocol Foo {
    TakeBar(Bar bar);
    HandleBar(request<Bar> bar);
};

之前

class MyServer : public fidl::WireRawChannelInterface<Foo> {
  void TakeBar(zx::channel bar, TakeBarCompleter::Sync& completer) override;
  void HandleBar(zx::channel bar, HandleBarCompleter::Sync& completer) override;
};

之后

class MyServer : public Foo::Interface {
  void TakeBar(fidl::ClientEnd<Bar> bar, TakeBarCompleter::Sync& completer) override;
  void HandleBar(fidl::ServerEnd<Bar> bar, HandleBarCompleter::Sync& completer) override;
};

场景 2:协议请求流水线

常见的做法是创建一对通道端点,然后将服务器端传递给协议实现。我们可以避免使用 fidl::CreateEndpoints<Protocol> 方法创建原始 Zircon 通道:

之前

zx::channel client_end, server_end;
zx_status_t status = zx::channel::create(0, &client_end, &server_end);
if (status != ZX_OK)
  return status;
foo.HandleBar(std::move(server_end));
fidl::WireClient<Bar> bar(std::move(client_end), &dispatcher);

之后

auto bar_ends = fidl::CreateEndpoints<Bar>();
if (!bar_ends.is_ok())
  return bar_ends.status_value();
foo.HandleBar(std::move(bar_ends->server));
fidl::WireClient bar(std::move(bar_ends->client), &dispatcher);

// Alternatively, |CreateEndpoints| supports returning the client-end by address,
// which would be useful when the client-end is an instance variable, for example
// in a test fixture.
fidl::ClientEnd<Foo> bar_client_end;
auto bar_server_end = fidl::CreateEndpoints(&bar_client_end);
if (!bar_server_end.is_ok())
  return bar_server_end.status_value();
foo.HandleBar(std::move(*bar_server_end));

请注意,使用类型化通道时,可以省略 fidl::WireClient 的协议模板参数,以使代码更简洁。

同步客户端

您可以使用 fidl::WireSyncClientfidl::ClientEnd 转换为该协议的相应同步客户端。这样做的好处是,不必重复说明协议类型(一次在 ClientEnd 中,然后在同步客户端类中)。

fidl::WireSyncClient bar{std::move(bar_ends->client)};

场景 3:连接到协议

fdio_service_connect 通常用于连接到组件命名空间中的 FIDL 服务。由于其签名是 C,因此使用起来会非常冗长,尤其是在存在类型化通道时。我们创建了人体工学封装容器:component::Connect<Protocol>component::ConnectAt<Protocol>component::OpenServiceRoot。它们位于 sdk/lib/sys/component/cpp 库中。

连接到单个协议

之前

zx::channel client_end, server_end;
zx_status_t status = zx::channel::create(0, &client_end, &server_end);
if (status != ZX_OK)
  return status;
status = fdio_service_connect("/svc/fuchsia.Foo", server_end.release());
if (status != ZX_OK)
  return status;
fidl::WireClient<Foo> foo(std::move(client_end), &dispatcher);

之后

// The channel creation and service connection is done in one function.
// By default it opens the protocol name.
// Returns |zx::result<fidl::ClientEnd<Foo>>|.
auto client_end = component::Connect<Foo>();
if (!client_end.is_ok())
  return client_end.status_value();
// Note: can omit template argument
fidl::WireClient foo(std::move(*client_end), &dispatcher);

正在打开 Service Directory

之前

zx::channel client_end, server_end;
zx_status_t status = zx::channel::create(0, &client_end, &server_end);
if (status != ZX_OK)
  return status;
status = fdio_service_connect("/svc", server_end.release());
if (status != ZX_OK)
  return status;
fidl::WireClient<::fuchsia_io::Directory> dir(std::move(client_end));

之后

// The channel creation and service connection is done in one function.
// Opens "/svc" and returns the client endpoint, as a
// |zx::result<fidl::ClientEnd<::fuchsia_io::Directory>>|.
auto client_end = component::OpenServiceRoot<Foo>();
if (!client_end.is_ok())
  return client_end.status_value();
// Note: can omit template argument
fidl::WireClient dir(std::move(*client_end), &dispatcher);

注意:传播协议类型

请尽可能在相关函数和变量之间传播协议类型。每当您在某个渠道中创建 ClientEnd / ServerEnd / UnownedClientEnd 时,考虑一下源渠道是否也可以更改为类型化渠道。它们充当自检文档,可以揭示对通过通道的协议类型的错误的假设。与 LLCPP 生成的结构不同,在公共 API 上使用类型化通道不会使接口倾向于特定的所有权模型或类型集,因为类型化频道只是围绕 Zircon 通道的轻量级封装容器。以下示例展示了 zx::unowned_channel 的迁移示例:

之前

// |client| should speak the |fuchsia.foobar/Baz| protocol.
zx_status_t DoThing(zx::unowned_channel client, int64_t args) {
  return fidl::WireCall<fuchsia_foobar::Baz>(std::move(client))->Method(args).status();
}

之后

// The intended protocol is encoded in the type system. No need for comment.
zx_status_t DoThing(fidl::UnownedClientEnd<fuchsia_foobar::Baz> client, int64_t args) {
  return fidl::WireCall(client)->Method(args).status();
}

注意:解决因协议组合而导致的类型不匹配问题

当一个 FIDL 协议构成另一个 FIDL 协议时,不存在“is-a”(继承、子假设)关系。这意味着,当协议 More 构成协议 Less 时,可能希望使用 fidl::ClientEnd<More> 调用函数 void foo(fidl::ClientEnd<Less>),但我们不提供这些类型之间的隐式转换。

确定使用是安全的后,可以通过 fidl::ClientEnd<Less>(more_client_end.TakeChannel()) 手动将一个客户端转换为另一个客户端。优先评论该转化是安全的(例如,More 不会在 Less 之上添加新事件)。

最后一步:创建 CL

在上传更改之前,请务必仔细检查以下三个位置:

  • "//build/cpp:fidl-llcpp-deprecated-raw-channels" 配置已从特定于目标的 BUILD.gn 文件中移除。
  • //build/cpp/BUILD.gn 中,删除可见性部分中与您的 GN 目标相对应的行,以免其回归到原始通道。还可以轻松直观呈现迁移进度。
  • 如果您确定要迁移的目标是特定 FIDL 协议的 RawChannelInterface 的最后一位用户,可以fidlgen_cpp 编译器中删除该协议。别担心,如果您提前移除,代码将无法编译。

然后,您可以上传 CL 并使用 Bug: 69585 对其进行标记 🎉?

如果需要 FIDL 团队的具体审核,您可以添加 ianloic@、yifeit@ 之一。

CL 示例

在迁移过程中发现的已知痛点:

  • 转换 fdio_get_service_handle 时,该函数接受 zx_handle_t 的输出参数,不包含任何协议类型。我们想要的是 fidl::ClientEnd<T>
  • 转换 fdio_open(path, flags, server.release()) 时,没有类型安全的 fdio_open 替代方案。
  • 在 HLCPP 和 LLCPP 端点类型之间进行转换比较复杂。我们希望 fidl::ClientEnd<::my_thing::Protocol>fidl::InterfaceHandle<my::thing::Protocol> 能够轻松相互转换,对于服务器,也是如此。
  • HLCPP 和旧版组件框架 API(sys::ServiceDirectorysys::OutgoingDirectory)使用 HLCPP InterfaceHandleInterfaceRequest 类型,因此需要额外转换为 LLCPP 类型的信道。

赞助商

如有疑问或了解最新状态,请与我们联系:

  • yifeit@google.com
  • ianloic@google.com