实现 C++ FIDL 客户端

前提条件

本教程以 FIDL 服务器教程为基础。如需查看完整的 FIDL 教程,请参阅概览

概览

本教程实现了 FIDL 协议的客户端,并针对在前一个教程中创建的服务器运行该客户端。本教程中的客户端是异步的。请参阅针对同步客户端的备用教程

客户端示例的结构

本教程附带的示例代码位于您的 Fuchsia 结算账号://examples/fidl/cpp/client。它由一个客户端组件及其包含的软件包组成。如需详细了解如何构建组件,请参阅构建组件

为了让客户端组件启动并运行,//examples/fidl/cpp/client/BUILD.gn 中定义了三个目标:

  1. 客户端的原始可执行文件。这将生成一个具有指定输出名称的二进制文件,该二进制文件可在 Fuchsia 上运行:

    executable("bin") {
      output_name = "fidl_echo_cpp_client"
      sources = [ "main.cc" ]
    
      deps = [
        "//examples/fidl/fuchsia.examples:fuchsia.examples_cpp",
    
        # This library is used to log messages.
        "//sdk/lib/syslog/cpp",
    
        # This library provides an asynchronous event loop implementation.
        "//zircon/system/ulib/async-loop:async-loop-cpp",
    
        # This library is used to consume capabilities, e.g. protocols,
        # from the component's incoming directory.
        "//sdk/lib/component/incoming/cpp",
      ]
    }
    
    
  2. 用于运行客户端可执行文件的组件。组件是 Fuchsia 上的软件执行单元。组件由其清单文件描述。在本例中,meta/client.cmlecho-client 配置为在 :bin 中运行 fidl_echo_cpp_client 的可执行组件。

    fuchsia_component("echo-client") {
      component_name = "echo_client"
      manifest = "meta/client.cml"
      deps = [ ":bin" ]
    }
    
    

    服务器组件清单位于 //examples/fidl/cpp/client/meta/client.cml。清单中的二进制文件名称必须与 BUILD.gn 中定义的 executable 的输出名称一致。

    {
        include: [ "syslog/client.shard.cml" ],
    
        // Information about the program to run.
        program: {
            // Use the built-in ELF runner.
            runner: "elf",
    
            // The binary to run for this component.
            binary: "bin/fidl_echo_cpp_client",
        },
    
        // Capabilities used by this component.
        use: [
            { protocol: "fuchsia.examples.Echo" },
        ],
    }
    
    
  3. 然后将该组件放入软件包(即 Fuchsia 上软件分发的单元)中。

    # C++ async client and server example package
    fuchsia_package("echo-cpp-client") {
      deps = [
        ":echo-client",
        "//examples/fidl/cpp/server:echo-server",
        "//examples/fidl/echo-realm:echo_realm",
      ]
    }
    
    

构建客户端

  1. 将客户端添加到您的 build 配置中。此操作只需执行一次:

    fx set core.x64 --with //examples/fidl/cpp/client
    
  2. 构建客户端:

    fx build examples/fidl/cpp/client
    

连接到协议

fuchsia.examples/Echo

int main(int argc, const char** argv) {
  // Connect to the |fuchsia.examples/Echo| protocol inside the component's
  // namespace. This can fail so it's wrapped in a |zx::result| and it must be
  // checked for errors.
  zx::result client_end = component::Connect<fuchsia_examples::Echo>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Echo| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // ...

同时,组件管理器会将请求路由到服务器组件。系统将使用服务器端点调用服务器教程中实现的服务器处理程序,将通道绑定到服务器实现。

此处需要注意的要点是,此代码假定组件的命名空间已包含 Echo 协议的实例。

初始化事件循环

异步客户端需要使用 async_dispatcher_t* 来异步监控来自某个通道的消息。async::Loop 提供了一个由事件循环支持的调度程序实现。

  // As in the server, the code sets up an async loop so that the client can
  // listen for incoming responses from the server without blocking.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

调度程序用于运行异步代码段。它首先用于运行 EchoString 方法,并在收到响应时退出。然后,它会在调用 SendString 之后运行以监听事件,并在收到 OnString 事件时退出。在这两个实例之间调用 ResetQuit() 可让客户端重复使用循环。

初始化客户端

要向服务器发出 Echo 请求,请使用上一步中的客户端端点、循环调度程序和事件处理脚本委托来初始化客户端:

  // Define the event handler implementation for the client.
  //
  // The event handler delegate should be an object that implements the
  // |fidl::AsyncEventHandler<Echo>| virtual class, which has methods
  // corresponding to the events offered by the protocol. By default those
  // methods are no-ops.
  class EventHandler : public fidl::AsyncEventHandler<fuchsia_examples::Echo> {
   public:
    void OnString(fidl::Event<::fuchsia_examples::Echo::OnString>& event) override {
      FX_LOGS(INFO) << "(Natural types) got event: " << event.response();
      loop_.Quit();
    }

    // One may also override the |on_fidl_error| method, which is called
    // when the client encounters an error and is going to teardown.
    void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; }

    explicit EventHandler(async::Loop& loop) : loop_(loop) {}

   private:
    async::Loop& loop_;
  };

  // Create an instance of the event handler.
  EventHandler event_handler{loop};

  // Create a client to the Echo protocol, passing our |event_handler| created
  // earlier. The |event_handler| must live for at least as long as the
  // |client|. The |client| holds a reference to the |event_handler| so it is a
  // bug for the |event_handler| to be destroyed before the |client|.
  fidl::Client client(std::move(*client_end), dispatcher, &event_handler);

进行 FIDL 调用

进行 FIDL 调用的方法在解引用运算符后面公开,因此 FIDL 调用看起来像 client->EchoString(...)

异步 EchoString 调用会接受一个请求对象,并接受根据调用结果调用的回调,该回调指示成功还是失败:

  client->EchoString({"hello"}).ThenExactlyOnce(
      [&](fidl::Result<fuchsia_examples::Echo::EchoString>& result) {
        // Check if the FIDL call succeeded or not.
        if (!result.is_ok()) {
          // If the call failed, log the error, and crash the program.
          // Production code should do more graceful error handling depending
          // on the situation.
          FX_LOGS(ERROR) << "EchoString failed: " << result.error_value();
          ZX_PANIC("%s", result.error_value().FormatDescription().c_str());
        }
        // Dereference (->) the result object to access the response payload.
        FX_LOGS(INFO) << "(Natural types) got response: " << result->response();
        loop.Quit();
      });
  // Run the dispatch loop, until we receive a reply, in which case the callback
  // above will quit the loop, breaking us out of the |Run| function.
  loop.Run();
  loop.ResetQuit();

您还可以使用自然结构体支持的指定初始化样式双大括号语法:

  client->EchoString({{.value = "hello"}})
      .ThenExactlyOnce(
        // ... callback ...

单向 SendString 调用没有回复,因此不需要回调。返回的结果表示发送请求时发生的所有错误。

  // Make a SendString one way call with natural types.
  fit::result<::fidl::Error> result = client->SendString({"hello"});
  if (!result.is_ok()) {
    FX_LOGS(ERROR) << "SendString failed: " << result.error_value();
    ZX_PANIC("%s", result.error_value().FormatDescription().c_str());
  }

使用有线网域对象拨打电话

上述教程使用自然网域对象进行客户端调用:每次调用都会消耗使用自然网域对象表示的请求消息,并也会在自然网域对象中返回回复。在针对性能和堆分配进行优化时,您可以使用有线域对象进行调用。为此,请在进行调用时使用的解引用运算符前面插入 .wire(),即 client.wire()->EchoString(...)

使用电线类型进行 EchoString 双向调用:

  client.wire()->EchoString("hello").ThenExactlyOnce(
      [&](fidl::WireUnownedResult<fuchsia_examples::Echo::EchoString>& result) {
        if (!result.ok()) {
          FX_LOGS(ERROR) << "EchoString failed: " << result.error();
          ZX_PANIC("%s", result.error().FormatDescription().c_str());
          return;
        }
        fidl::WireResponse<fuchsia_examples::Echo::EchoString>& response = result.value();
        std::string reply(response.response.data(), response.response.size());
        FX_LOGS(INFO) << "(Wire types) got response: " << reply;
        loop.Quit();
      });

使用电线类型进行 SendString 单向调用:

  fidl::Status wire_status = client.wire()->SendString("hello");
  if (!wire_status.ok()) {
    FX_LOGS(ERROR) << "SendString failed: " << result.error_value();
    ZX_PANIC("%s", result.error_value().FormatDescription().c_str());
  }

传输客户端调用中使用的相关类和函数的形状与自然客户端调用中使用的形状和函数类似。当针对其他类或函数调用其他类或函数时,对应的线上类通常带有 Wire 前缀。指针与引用和参数结构之间也存在差异:

  • 接受自然域对象的 EchoString 方法接受单个参数,即请求域对象:

    client->EchoString({{.value = "hello"}})
        .ThenExactlyOnce(
    

    当请求载荷是结构体时,接受传输网域对象的 EchoString 方法会将请求正文中的结构体字段列表展平为单独的参数(此处为单个 fidl::StringView 参数):

    client.wire()->EchoString("hello").ThenExactlyOnce(
    
  • 异步自然调用中的回调接受 fidl::Result<Method>&

    client->EchoString({"hello"}).ThenExactlyOnce(
        [&](fidl::Result<fuchsia_examples::Echo::EchoString>& result) {
          // Check if the FIDL call succeeded or not.
          if (!result.is_ok()) {
            // If the call failed, log the error, and crash the program.
            // Production code should do more graceful error handling depending
            // on the situation.
            FX_LOGS(ERROR) << "EchoString failed: " << result.error_value();
            ZX_PANIC("%s", result.error_value().FormatDescription().c_str());
          }
          // Dereference (->) the result object to access the response payload.
          FX_LOGS(INFO) << "(Natural types) got response: " << result->response();
          loop.Quit();
        });
    // Run the dispatch loop, until we receive a reply, in which case the callback
    // above will quit the loop, breaking us out of the |Run| function.
    loop.Run();
    loop.ResetQuit();
    
    • 如需检查成功或错误,请使用 is_ok()is_error() 方法。
    • 如需之后访问响应载荷,请使用 value()->
    • 您可以移出结果或载荷,因为这些类型都实现了分层对象所有权。

    异步线路调用中的回调接受 fidl::WireUnownedResult<Method>&

    client.wire()->EchoString("hello").ThenExactlyOnce(
        [&](fidl::WireUnownedResult<fuchsia_examples::Echo::EchoString>& result) {
          if (!result.ok()) {
            FX_LOGS(ERROR) << "EchoString failed: " << result.error();
            ZX_PANIC("%s", result.error().FormatDescription().c_str());
            return;
          }
          fidl::WireResponse<fuchsia_examples::Echo::EchoString>& response = result.value();
          std::string reply(response.response.data(), response.response.size());
          FX_LOGS(INFO) << "(Wire types) got response: " << reply;
          loop.Quit();
        });
    
    • 如需检查是否成功,请使用 ok() 方法。
    • 如需之后访问响应载荷,请使用 value()->
    • 您必须在回调内同步使用结果。结果类型为无主,这意味着它仅借用由 FIDL 运行时在其他地方分配的响应。
  • 在自然情况下,单向调用也会获取整个请求域对象,并且在线路 case 中,将多个请求结构体字段展平为单独的参数:

    // Make a SendString one way call with natural types.
    fit::result<::fidl::Error> result = client->SendString({"hello"});
    
    fidl::Status wire_status = client.wire()->SendString("hello");
    

运行客户端

为了使客户端和服务器使用 Echo 协议进行通信,组件框架必须将 fuchsia.examples.Echo 功能从服务器路由到客户端。

  1. 配置 build 以添加所提供的包含 echo 领域、服务器和客户端的软件包:

    fx set core.x64 --with //examples/fidl/cpp/client
    
  2. 构建 Fuchsia 映像:

    fx build
    
  3. 运行 echo_realm 组件。这将创建客户端和服务器组件实例并路由功能:

    ffx component run /core/ffx-laboratory:echo-client fuchsia-pkg://fuchsia.com/echo-cpp-client#meta/echo_realm.cm
    
  4. 启动 echo_client 实例:

    ffx component start /core/ffx-laboratory:echo_realm/echo_client
    

当客户端尝试连接到 Echo 协议时,服务器组件便会启动。您应该会在设备日志 (ffx log) 中看到类似于以下内容的输出:

[echo_server][][I] Running C++ echo server with natural types
[echo_server][][I] Incoming connection for fuchsia.examples.Echo
[echo_client][][I] (Natural types) got response: hello
[echo_client][][I] (Natural types) got response: hello
[echo_client][][I] (Natural types) got response: hello
[echo_client][][I] (Natural types) got event: hello
[echo_client][][I] (Wire types) got response: hello
[echo_client][][I] (Natural types) got event: hello
[echo_server][][I] Client disconnected

终止领域组件以停止执行并清理组件实例:

ffx component destroy /core/ffx-laboratory:echo_realm

仅有线域对象客户端

fidl::Client 支持使用自然网域对象有线网域对象进行调用。如果您只需要使用传输域对象,则可以创建一个 WireClient,将等效的方法调用接口作为通过对 fidl::Client 调用 client.wire() 获取的子集公开。

WireClient 的创建方式与 Client 相同:

fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler);

fidl::Client 始终以自然网域对象的形式向用户公开接收到的事件。另一方面,fidl::WireClient 将以有线域对象的形式公开收到的事件。为此,传递给 WireClient 的事件处理脚本需要实现 fidl::WireAsyncEventHandler<Protocol>

  • 实现自然事件处理脚本:

    class EventHandler : public fidl::AsyncEventHandler<fuchsia_examples::Echo> {
     public:
      void OnString(fidl::Event<::fuchsia_examples::Echo::OnString>& event) override {
        FX_LOGS(INFO) << "(Natural types) got event: " << event.response();
        loop_.Quit();
      }
    
      // ...
    };
    
  • 实现线上事件处理脚本:

    class EventHandler : public fidl::WireAsyncEventHandler<fuchsia_examples::Echo> {
     public:
      void OnString(fidl::WireEvent<fuchsia_examples::Echo::OnString>* event) override {
        FX_LOGS(INFO) << "(Natural types) got event: " << event->response.get();
        loop_.Quit();
      }
    
      // ...
    };
    

同步调用

WireClient 对象还允许同步调用,这种调用将一直阻塞,直到收到响应并返回响应对象。可以使用 .sync() 访问器进行选择。(例如 client.sync()->EchoString())。

  // Make a synchronous EchoString call, which blocks until it receives the response,
  // then returns a WireResult object for the response.
  fidl::WireResult result_sync = client.sync()->EchoString("hello");
  ZX_ASSERT(result_sync.ok());
  std::string_view response = result_sync->response.get();
  FX_LOGS(INFO) << "Got synchronous response: " << response;

在同步调用中,系统会返回一个结果对象,以同步方式传达调用成功或失败。

使用电汇客户端的完整示例代码位于 Fuchsia 签出的 //examples/fidl/cpp/client/wire 中。