新 C++ 绑定教程

本部分将帮助您了解如何使用新的 C++ FIDL 绑定。如需有关设置 build 和从头开始编写简单客户端或服务器的分步指南,请参阅开始使用。如需有关如何有效使用绑定的更详细指南和建议,请参阅主题。如需了解代码中经常出现的名称和概念,以及有关如何做出正确选择的简要说明,请参阅术语

概括来讲,C++ 绑定由以下部分组成:

  • 数据域对象(生成的 FIDL 结构,例如结构体、表等)
  • 行为:用于通过协议发送这些网域对象、接收事件等的客户端/服务器 API

自然域对象和线程域对象

这些绑定支持两种类型的域对象:自然类型和线程类型:

  • 自然类型是针对人体工学进行优化的高级领域对象。
    • 这些类型使用智能指针拥有其子项。
    • 它们使用 std::vectorstd::optionalstd::string 等惯用 C++ 类型。
    • 假设有一个名为 fuchsia.my.lib 的 FIDL 库,则系统会在 fuchsia_my_lib 命名空间中生成类型。
  • wire 类型针对性能和原地解码进行了优化。
    • 它们是专用的 C++ 标准布局类型,其内存布局与 FIDL 线格格式一致。
    • 离线子项是指向单独缓冲区的无所有权指针。请参阅线程域对象的内存所有权
    • 假设有一个名为 fuchsia.my.lib 的 FIDL 库,则系统会在 fuchsia_my_lib::wire 命名空间中生成类型。

开始项目时,默认选择自然类型,因为它们更易于使用且性能合理。只有在优化关键路径中的逻辑或需要精确控制内存分配时,才应改用线类型。由于 Wire 类型由不安全的 View 组成,因此不当使用这些 View 可能会导致“释放后使用”和其他内存安全 bug。

使用入门

  1. 使用自然域对象和线框域对象
  2. 编写服务器
  3. 编写客户端(async同步

主题

术语

客户端和服务器中是否有 WireWire

如果客户端/服务器 API 名称中存在 Wire 前缀,则表示该 API 仅接受线型。否则,API 通常可以同时接受线程类型和自然类型,或默认使用自然类型。例如,fidl::Client 支持使用自然类型和线程类型进行调用,而 fidl::WireClient 公开的接口更为严格,仅接受线程类型。

fidl::Server 会以自然类型接收请求,并可以发送自然类型或线程类型的回复。另一方面,fidl::WireServer 会以线程类型接收请求,并以线程类型发送响应。

为了在发送端同时支持线程和自然类型,同时避免函数重载歧义,线程接口位于 .wire() 访问器下。例如,给定 fidl::Client<MyProtocol> client;,您可以编写 client->SomeMethod(natural_type); 以使用自然类型发出请求,并编写 client.wire()->SomeMethod(wire_type); 以使用线程类型发出请求。

建议

使用不带 Wire 前缀的客户端/服务器 API。只有在需要确保在编译时仅使用线类型时,才可以定义使用 Wire 对应项(例如 fidl::WireClient)的函数签名。您还可以通过依赖于 fuchsia.my.lib_cpp_wire 目标(而不是 fuchsia.my.lib_cpp GN 目标)来仅依赖于绑定的线部分。

客户端中是否有 Syncsync

同步(简称“sync”)适用于有响应的 FIDL 调用(双向调用),表示调用是阻塞的:发出此类调用的线程在收到响应之前不会从调用返回。例如,fidl::WireSyncClient 是一个客户端,其中所有双向调用都是同步的。同样,fidl::WireClient 有一个 .sync() 访问器,用于返回用于进行同步调用的接口。

单向调用没有响应,因此不适用于同步概念。

建议

如果您的代码是仅使用其他组件功能的独立程序,请确定其业务需求所需的并发级别:

  • 如果它不管理大量并发操作,您可以使用同步客户端,从而实现易于阅读的顺序逻辑。例如,运行时间较短的命令行工具可以使用 fidl::SyncClient

  • 如果您的代码管理大量并发操作,通常可以访问异步调度程序 (async_dispatcher_t*)。在这种情况下,在同步和异步客户端和调用之间进行选择时,请优先选择异步对应项。例如,fidl::WireClient 优先于 fidl::WireSyncClient.sync(),而无需通过 .sync()。特别是,如果调度程序是单线程的,请勿对调度程序线程进行同步调用,以免出现死锁。

如果您的代码是服务(即向其他组件提供功能的组件),则应使用异步调度程序和异步客户端来支持多个使用方所需的并发级别。

如果您的代码是其他应用使用的库,则需要仔细考虑它应根据用户的需求公开同步接口还是异步接口。例如,对于在异步调度程序上调度工作且具有高并发性的应用,使用同步客户端并公开同步接口的库会更难使用。

以上是一般建议,不同的异步运行时可能有自己的更具体的建议。

客户端中是否有 Shared 与是否有 shared 的对比

如果客户端类型的名称中包含“shared”,则可以在任意线程中绑定和销毁该类型。请参阅线程指南中的 SharedClient。它有一个不带“shared”的对应项,例如 Client,必须在调度程序线程中绑定和销毁。WireClientWireSharedClient 之间也存在类似的关系。

建议

ClientSharedClient 之间进行选择时,请优先选择 Client,除非应用的线程模型或性能要求需要以多线程方式使用客户端。如需了解使用 SharedClient 时需要注意的许多方面,请参阅线程指南Client 中的额外限制旨在减少内存争用和释放后使用。例如,如果您的对象都位于同一单线程异步调度程序中,您可以使用 Client

双向通话中的 ThenThenExactlyOnce

当异步调用有响应时,您可以通过以下两种方式指定回调来接收该调用的结果:

  • 使用 .ThenExactlyOnce(...) 时,系统始终会恰好调用一次回调,以传递结果。
  • 使用 .Then(...) 时,系统会在销毁客户端对象时静默舍弃回调,这适用于面向对象的代码。

Then 的动机

进行异步双向调用时,该调用的结果会在执行已离开调用原始范围之后的稍后时间传回给应用。异步调度程序稍后会调用您在发出调用时指定的后续逻辑,称为接续。这意味着,很容易在对象被销毁后使用它们,从而导致内存损坏:

// The following snippet shows an example of use-after-free
// occurring in asynchronous two-way calls.
void Foo(fidl::WireClient<MyProtocol>& client) {
  bool call_ok;
  client->SomeMethod().Then(
      // The following lambda function represents the continuation.
      [&call_ok] (fidl::WireUnownedResult<SomeMethod>& result) {
        // `call_ok` has already gone out of scope.
        // This would lead to memory corruption.
        call_ok = result.ok();
      });
}

当接续捕获 this 指针且所述引用的对象也拥有客户端时,就会发生更为隐秘的这种损坏形式。销毁外部对象(进而销毁客户端)会导致所有待处理的双向调用失败。在其接续运行时,它捕获的 this 指针将失效。

ThenThenExactlyOnce 都会为双向调用注册一个接续。不过,Then 旨在减少上述损坏情况。具体而言:

  • Then 可确保在客户端被销毁之前,所提供的接续函数最多只会被调用一次。如果您的接续项仅捕获与客户端具有相同生命周期的对象(例如,您的用户对象拥有客户端),则应选择 Then。销毁用户对象会使所有未处理的回调进入非活动状态。没有释放后再使用问题。

  • 另一方面,ThenExactlyOnce 保证只调用一次接续。如果客户端对象被销毁,接续会收到取消错误。您需要确保在继续执行时,所有引用的对象都仍然有效,这可能在客户端对象被销毁后的不确定时间。如果必须恰好调用一次接续(例如,与 fpromise 补全器或 FIDL 服务器补全器交互时,或在单元测试期间),则应选择 ThenExactlyOnce

建议

一般而言:

  • 如果回调类似于 client_->Foo([this],请使用 Then(请注意,client_ 是成员变量)。
  • 如果您的回调如下所示:
    • client->Foo([completer],或者
    • client->Foo([],或者
    • client->Foo([&](在单元测试中很常见),
    • 回调捕获弱指针或强指针,
    • 使用 ThenExactlyOnce

请勿捕获具有不同生命周期的对象,以免在继续执行时只有部分对象处于活动状态。

Zircon 通道传输与驱动程序传输

FIDL 协议与 FIDL 定义中指定的相应传输相关联,传输决定了可能会通过该协议流动的资源类型,并且可能会影响用于发送和接收消息的生成 API。C++ 绑定支持两种传输:

Zircon 通道传输由端点类型 fidl::ClientEnd<SomeProtocol>fidl::ServerEnd<SomeProtocol> 表示。

驱动程序传输使用端点类型 fdf::ClientEnd<SomeProtocol>fdf::ServerEnd<SomeProtocol>

竞技场

Arenas 对象管理内存缓冲区池并提供高效的分配。它们在线程网域对象和线程客户端和服务器中被广泛使用,以避免昂贵的复制。

竞技场不适用于自然域对象及其关联的客户端和服务器,这些对象和服务器封装了内存分配的详细信息。

您可以使用 fidl::Arena 创建位于该竞技场中的线程网域对象。请参阅内存管理

将协议与线程域对象一起通过驱动程序传输使用时,应使用 fdf::Arena 对象分配编码消息所需的缓冲区。