實作 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.
        "//sdk/lib/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.cml 會將 echo-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. 將用戶端新增至建構設定。這項操作只需執行一次:

    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());
  }

使用線路網域物件撥打電話

上述教學課程會使用自然網域物件建立用戶端呼叫:每個呼叫都會使用自然網域物件代表要求訊息,並以自然網域物件傳回回覆。在最佳化效能和堆積分配方面,其中一種可以使用「線域物件」呼叫。如要執行這項操作,請在呼叫時使用的解參運算子 (client.wire()->EchoString(...)) 前插入 .wire()

使用線路類型建立 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()->
    • 您必須在回呼中同步使用結果。結果類型為「unowned」,表示它只會借用 FIDL 執行階段在其他位置分配的回應。
  • 單向呼叫也會在自然情況下接收整個要求網域物件,並在線路情況下將要求結構體欄位扁平化為個別的引數:

    // 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. 設定建構作業,加入提供的套件,其中包含回音領域、伺服器和用戶端:

    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 中。