FIDL 範例

這個 FIDL 範例目錄旨在透過簡化的實際軟體工作流程實作來示範 FIDL 概念。

索引範例

下列範例逐步說明實用的 FIDL 概念。

計算機

計算機範例會顯示建立第一個 FIDL 通訊協定的基本構成要素。

鍵/值存放區

鍵/值儲存庫範例示範如何使用 FIDL 建構簡易的鍵/值存放區,以瞭解使用該語言的各種資料類型。

Canvas

畫布範例示範如何使用 FIDL 建構簡單的 2D 行轉譯畫布,以瞭解常用的資料流程模式。

概念索引

上一節列出的至少其中一個例子說明瞭 FIDL 語言中的每個「概念」。下節會列出每種這類概念的快速參考資料以及實作範例。

確認模式

FIDL 方案:確認模式

對於將單向呼叫的方法而言,確認模式是簡單的流量控制方法。該方法不會做為單向呼叫,而是會轉換為沒有回應的雙向呼叫,俗稱「堆疊」ack。堆疊的唯一原因是通知訊息已收到訊息,以便傳送者決定如何繼續進行。

雙方的談判費用也會增加在管道上。如果用戶端在繼續下一個呼叫前等待確認,這種模式也可能導致效能降低。

傳送非計量付費的單向呼叫會產生簡單的設計,但可能會陷入潛在陷阱:如果伺服器處理更新的速度比用戶端傳送更新的速度慢許多,該怎麼辦?舉例來說,用戶端可以從部分文字檔案中載入包含數千行數行的繪圖,並嘗試依序傳送。我該如何對用戶端套用背壓,以免伺服器不堪整波更新?

只要使用確認模式,並將單向呼叫 AddLine(...); 轉換為雙向 AddLine(...) -> ();,我們即可向用戶端提供意見回饋。這可讓用戶端視情況限制輸出內容。在此範例中,我們只會讓用戶端等待確認,然後再傳送下則等待的下一則訊息,但較複雜的設計能夠以最佳方式傳送訊息,而且只有在接收的非同步堆疊頻率低於預期時才會節流。

首先,我們必須定義介面定義和測試工具。FIDL、CML 和領域介面定義會設定 Scaffold,可供任意實作使用:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.canvas.addlinemetered;

/// A point in 2D space.
type Point = struct {
    x int64;
    y int64;
};

/// A line in 2D space.
alias Line = array<Point, 2>;

/// A bounding box in 2D space. This is the result of "drawing" operations on our canvas, and what
/// the server reports back to the client. These bounds are sufficient to contain all of the
/// lines (inclusive) on a canvas at a given time.
type BoundingBox = struct {
    top_left Point;
    bottom_right Point;
};

/// Manages a single instance of a canvas. Each session of this protocol is responsible for a new
/// canvas.
@discoverable
open protocol Instance {
    /// Add a line to the canvas.
    ///
    /// This method can be considered an improvement over the one-way case from a flow control
    /// perspective, as it is now much more difficult for a well-behaved client to "get ahead" of
    /// the server and overwhelm. This is because the client now waits for each request to be acked
    /// by the server before proceeding. This change represents a trade-off: we get much greater
    /// synchronization of message flow between the client and the server, at the cost of worse
    /// performance at the limit due to the extra wait imposed by each ack.
    flexible AddLine(struct {
        line Line;
    }) -> ();

    /// Update the client with the latest drawing state. The server makes no guarantees about how
    /// often this event occurs - it could occur multiple times per board state, for example.
    flexible -> OnDrawn(BoundingBox);
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.canvas.addlinemetered.Instance" },
    ],
    config: {
        // A script for the client to follow. Entries in the script may take one of two forms: a
        // pair of signed-integer coordinates like "-2,15:4,5", or the string "WAIT". The former
        // calls `AddLine(...)`, while the latter pauses execution until the next `->OnDrawn(...)`
        // event is received.
        //
        // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great
        //   deal of string parsing if we were able to use a vector of `union { Point; WaitEnum}`
        //   here.
        script: {
            type: "vector",
            max_count: 100,
            element: {
                type: "string",
                max_size: 64,
            },
        },
    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.canvas.addlinemetered.Instance" },
    ],
    expose: [
        {
            protocol: "examples.canvas.addlinemetered.Instance",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.canvas.addlinemetered.Instance",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{format_err, Context as _, Error},
    config::Config,
    fidl_examples_canvas_addlinemetered::{InstanceEvent, InstanceMarker, Point},
    fuchsia_component::client::connect_to_protocol,
    futures::TryStreamExt,
    std::{thread, time},
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send Instance requests
    // across the channel.
    let instance = connect_to_protocol::<InstanceMarker>()?;
    println!("Outgoing connection enabled");

    for action in config.script.into_iter() {
        // If the next action in the script is to "WAIT", block until an OnDrawn event is received
        // from the server.
        if action == "WAIT" {
            let mut event_stream = instance.take_event_stream();
            loop {
                match event_stream
                    .try_next()
                    .await
                    .context("Error getting event response from proxy")?
                    .ok_or_else(|| format_err!("Proxy sent no events"))?
                {
                    InstanceEvent::OnDrawn { top_left, bottom_right } => {
                        println!(
                            "OnDrawn event received: top_left: {:?}, bottom_right: {:?}",
                            top_left, bottom_right
                        );
                        break;
                    }
                    InstanceEvent::_UnknownEvent { ordinal, .. } => {
                        println!("Received an unknown event with ordinal {ordinal}");
                    }
                }
            }
            continue;
        }

        // If the action is not a "WAIT", we need to draw a line instead. Parse the string input,
        // making two points out of it.
        let mut points = action
            .split(":")
            .map(|point| {
                let integers = point
                    .split(",")
                    .map(|integer| integer.parse::<i64>().unwrap())
                    .collect::<Vec<i64>>();
                Point { x: integers[0], y: integers[1] }
            })
            .collect::<Vec<Point>>();

        // Assemble a line from the two points.
        let from = points.pop().ok_or(format_err!("line requires 2 points, but has 0"))?;
        let to = points.pop().ok_or(format_err!("line requires 2 points, but has 1"))?;
        let line = [from, to];

        // Draw a line to the canvas by calling the server, using the two points we just parsed
        // above as arguments.
        println!("AddLine request sent: {:?}", line);

        // By awaiting on the reply, we prevent the client from sending another request before the
        // server is ready to handle, thereby syncing the flow rate between the two parties over
        // this method.
        instance.add_line(&line).await.context("Error sending request")?;
        println!("AddLine response received");
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fidl::endpoints::RequestStream as _,
    fidl_examples_canvas_addlinemetered::{
        BoundingBox, InstanceRequest, InstanceRequestStream, Point,
    },
    fuchsia_async::{Time, Timer},
    fuchsia_component::server::ServiceFs,
    fuchsia_zircon::{self as zx},
    futures::future::join,
    futures::prelude::*,
    std::sync::{Arc, Mutex},
};

// A struct that stores the two things we care about for this example: the bounding box the lines
// that have been added thus far, and bit to track whether or not there have been changes since the
// last `OnDrawn` event.
#[derive(Debug)]
struct CanvasState {
    // Tracks whether there has been a change since the last send, to prevent redundant updates.
    changed: bool,
    bounding_box: BoundingBox,
}

impl CanvasState {
    /// Handler for the `AddLine` method.
    fn add_line(&mut self, line: [Point; 2]) {
        // Update the bounding box to account for the new lines we've just "added" to the canvas.
        let bounds = &mut self.bounding_box;
        for point in line {
            if point.x < bounds.top_left.x {
                bounds.top_left.x = point.x;
            }
            if point.y > bounds.top_left.y {
                bounds.top_left.y = point.y;
            }
            if point.x > bounds.bottom_right.x {
                bounds.bottom_right.x = point.x;
            }
            if point.y < bounds.bottom_right.y {
                bounds.bottom_right.y = point.y;
            }
        }

        // Mark the state as "dirty", so that an update is sent back to the client on the next tick.
        self.changed = true
    }
}

/// Creates a new instance of the server, paired to a single client across a zircon channel.
async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> {
    // Create a new in-memory state store for the state of the canvas. The store will live for the
    // lifetime of the connection between the server and this particular client.
    let state = Arc::new(Mutex::new(CanvasState {
        changed: true,
        bounding_box: BoundingBox {
            top_left: Point { x: 0, y: 0 },
            bottom_right: Point { x: 0, y: 0 },
        },
    }));

    // Take ownership of the control_handle from the stream, which will allow us to push events from
    // a different async task.
    let control_handle = stream.control_handle();

    // A separate watcher task periodically "draws" the canvas, and notifies the client of the new
    // state. We'll need a cloned reference to the canvas state to be accessible from the new
    // task.
    let state_ref = state.clone();
    let update_sender = || async move {
        loop {
            // Our server sends one update per second.
            Timer::new(Time::after(zx::Duration::from_seconds(1))).await;
            let mut state = state_ref.lock().unwrap();
            if !state.changed {
                continue;
            }

            // After acquiring the lock, this is where we would draw the actual lines. Since this is
            // just an example, we'll avoid doing the actual rendering, and simply send the bounding
            // box to the client instead.
            let bounds = state.bounding_box;
            match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) {
                Ok(_) => println!(
                    "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}",
                    bounds.top_left, bounds.bottom_right
                ),
                Err(_) => return,
            }

            // Reset the change tracker.
            state.changed = false
        }
    };

    // Handle requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    let state_ref = &state;
    let request_handler =
        stream.map(|result| result.context("failed request")).try_for_each(|request| async move {
            // Match based on the method being invoked.
            match request {
                InstanceRequest::AddLine { line, responder } => {
                    println!("AddLine request received: {:?}", line);
                    state_ref.lock().unwrap().add_line(line);

                    // Because this is now a two-way method, we must use the generated `responder`
                    // to send an in this case empty reply back to the client. This is the mechanic
                    // which syncs the flow rate between the client and server on this method,
                    // thereby preventing the client from "flooding" the server with unacknowledged
                    // work.
                    responder.send().context("Error responding")?;
                    println!("AddLine response sent");
                } //
                InstanceRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        });

    // This await does not complete, and thus the function does not return, unless the server errors
    // out. The stream will await indefinitely, thereby creating a long-lived server. Here, we first
    // wait for the updater task to realize the connection has died, then bubble up the error.
    join(request_handler, update_sender()).await.0
}

// A helper enum that allows us to treat a `Instance` service instance as a value.
enum IncomingService {
    Instance(InstanceRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Instance` protocol - this will allow the client to see
    // the server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Instance);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.addlinemetered/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/fidl/new/canvas/add_line_metered/cpp_natural/client/config.h>

// The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming
// events asynchronously.
class EventHandler : public fidl::AsyncEventHandler<examples_canvas_addlinemetered::Instance> {
 public:
  // Handler for |OnDrawn| events sent from the server.
  void OnDrawn(fidl::Event<examples_canvas_addlinemetered::Instance::OnDrawn>& event) override {
    auto top_left = event.top_left();
    auto bottom_right = event.bottom_right();
    FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x()
                  << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x()
                  << ", y: " << bottom_right.y() << " }";
    loop_.Quit();
  }

  void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; }

  void handle_unknown_event(
      fidl::UnknownEventMetadata<examples_canvas_addlinemetered::Instance> metadata) override {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal;
  }

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

 private:
  async::Loop& loop_;
};

// A helper function that takes a coordinate in string form, like "123,-456", and parses it into a
// a struct of the form |{ in64 x; int64 y; }|.
::examples_canvas_addlinemetered::Point ParsePoint(std::string_view input) {
  int64_t x = 0;
  int64_t y = 0;
  size_t index = input.find(',');
  if (index != std::string::npos) {
    std::from_chars(input.data(), input.data() + index, x);
    std::from_chars(input.data() + index + 1, input.data() + input.length(), y);
  }
  return ::examples_canvas_addlinemetered::Point(x, y);
}

// A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it
// into an array of 2 |Point| structs.
::std::array<::examples_canvas_addlinemetered::Point, 2> ParseLine(const std::string& action) {
  auto input = std::string_view(action);
  size_t index = input.find(':');
  if (index != std::string::npos) {
    return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))};
  }
  return {};
}

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop and dispatcher.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the 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<examples_canvas_addlinemetered::Instance>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // Create an instance of the event handler.
  EventHandler event_handler(loop);

  // Create an asynchronous client using the newly-established connection.
  fidl::Client client(std::move(*client_end), dispatcher, &event_handler);
  FX_LOGS(INFO) << "Outgoing connection enabled";

  for (const auto& action : conf.script()) {
    // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received
    // from the server.
    if (action == "WAIT") {
      loop.Run();
      loop.ResetQuit();
      continue;
    }

    // Draw a line to the canvas by calling the server, using the two points we just parsed
    // above as arguments.
    auto line = ParseLine(action);
    FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x() << ", y: " << line[1].y()
                  << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]";

    client->AddLine(line).ThenExactlyOnce(
        [&](fidl::Result<examples_canvas_addlinemetered::Instance::AddLine>& result) {
          // Check if the FIDL call succeeded or not.
          if (!result.is_ok()) {
            // Check that our two-way call succeeded, and handle the error appropriately. In the
            // case of this example, there is nothing we can do to recover here, except to log an
            // error and exit the program.
            FX_LOGS(ERROR) << "Could not send AddLine request: "
                           << result.error_value().FormatDescription();
          }
          FX_LOGS(INFO) << "AddLine response received";

          // Quit the loop, thereby handing control back to the outer loop of actions being iterated
          // over.
          loop.Quit();
        });

    // Run the loop until the callback is resolved, at which point we can continue from here.
    loop.Run();
    loop.ResetQuit();
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.addlinemetered/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <src/lib/fxl/macros.h>
#include <src/lib/fxl/memory/weak_ptr.h>

// A struct that stores the two things we care about for this example: the set of lines, and the
// bounding box that contains them.
struct CanvasState {
  // Tracks whether there has been a change since the last send, to prevent redundant updates.
  bool changed = true;
  examples_canvas_addlinemetered::BoundingBox bounding_box;
};

// An implementation of the |Instance| protocol.
class InstanceImpl final : public fidl::Server<examples_canvas_addlinemetered::Instance> {
 public:
  // Bind this implementation to a channel.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end)
      : binding_(fidl::BindServer(
            dispatcher, std::move(server_end), this,
            [this](InstanceImpl* impl, fidl::UnbindInfo info,
                   fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) {
              if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) {
                FX_LOGS(ERROR) << "Shutdown unexpectedly";
              }
              delete this;
            })),
        weak_factory_(this) {
    // Start the update timer on startup. Our server sends one update per second
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void AddLine(AddLineRequest& request, AddLineCompleter::Sync& completer) override {
    auto points = request.line();
    FX_LOGS(INFO) << "AddLine request received: [Point { x: " << points[1].x()
                  << ", y: " << points[1].y() << " }, Point { x: " << points[0].x()
                  << ", y: " << points[0].y() << " }]";

    // Update the bounding box to account for the new line we've just "added" to the canvas.
    auto& bounds = state_.bounding_box;
    for (const auto& point : request.line()) {
      if (point.x() < bounds.top_left().x()) {
        bounds.top_left().x() = point.x();
      }
      if (point.y() > bounds.top_left().y()) {
        bounds.top_left().y() = point.y();
      }
      if (point.x() > bounds.bottom_right().x()) {
        bounds.bottom_right().x() = point.x();
      }
      if (point.y() < bounds.bottom_right().y()) {
        bounds.bottom_right().y() = point.y();
      }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn|
    // event.
    state_.changed = true;

    // Because this is now a two-way method, we must use the generated |completer| to send an in
    // this case empty reply back to the client. This is the mechanic which syncs the flow rate
    // between the client and server on this method, thereby preventing the client from "flooding"
    // the server with unacknowledged work.
    completer.Reply();
    FX_LOGS(INFO) << "AddLine response sent";
  }

  void handle_unknown_method(
      fidl::UnknownMethodMetadata<examples_canvas_addlinemetered::Instance> metadata,
      fidl::UnknownMethodCompleter::Sync& completer) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal;
  }

 private:
  // Each scheduled update waits for the allotted amount of time, sends an update if something has
  // changed, and schedules the next update.
  void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) {
    async::PostDelayedTask(
        dispatcher,
        [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] {
          // Halt execution if the binding has been deallocated already.
          if (!weak) {
            return;
          }

          // Schedule the next update if the binding still exists.
          weak->ScheduleOnDrawnEvent(dispatcher, after);

          // No need to send an update if nothing has changed since the last one.
          if (!weak->state_.changed) {
            return;
          }

          // This is where we would draw the actual lines. Since this is just an example, we'll
          // avoid doing the actual rendering, and simply send the bounding box to the client
          // instead.
          auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box);
          if (!result.is_ok()) {
            return;
          }

          auto top_left = state_.bounding_box.top_left();
          auto bottom_right = state_.bounding_box.bottom_right();
          FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x()
                        << ", y: " << top_left.y()
                        << " }, bottom_right: Point { x: " << bottom_right.x()
                        << ", y: " << bottom_right.y() << " }";

          // Reset the change tracker.
          state_.changed = false;
        },
        after);
  }

  fidl::ServerBindingRef<examples_canvas_addlinemetered::Instance> binding_;
  CanvasState state_ = CanvasState{};

  // Generates weak references to this object, which are appropriate to pass into asynchronous
  // callbacks that need to access this object. The references are automatically invalidated
  // if this object is destroyed.
  fxl::WeakPtrFactory<InstanceImpl> weak_factory_;
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This
  // directory is where the outgoing FIDL protocols are installed so that they can be provided to
  // other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle.
  // The startup handle is a handle provided to every component by the system, so that they can
  // serve capabilities (e.g. FIDL protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to
  // |examples.canvas.addlinemetered.Instance|.
  result = outgoing.AddUnmanagedProtocol<examples_canvas_addlinemetered::Instance>(
      [dispatcher](fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) {
        // Create an instance of our InstanceImpl that destroys itself when the connection closes.
        new InstanceImpl(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string();
    return -1;
  }

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

C++ (有線)

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.addlinemetered/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/fidl/new/canvas/add_line_metered/cpp_wire/client/config.h>

// The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming
// events asynchronously.
class EventHandler : public fidl::WireAsyncEventHandler<examples_canvas_addlinemetered::Instance> {
 public:
  // Handler for |OnDrawn| events sent from the server.
  void OnDrawn(fidl::WireEvent<examples_canvas_addlinemetered::Instance::OnDrawn>* event) override {
    auto top_left = event->top_left;
    auto bottom_right = event->bottom_right;
    FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x
                  << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x
                  << ", y: " << bottom_right.y << " }";
    loop_.Quit();
  }

  void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; }

  void handle_unknown_event(
      fidl::UnknownEventMetadata<examples_canvas_addlinemetered::Instance> metadata) override {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal;
  }

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

 private:
  async::Loop& loop_;
};

// A helper function that takes a coordinate in string form, like "123,-456", and parses it into a
// a struct of the form |{ in64 x; int64 y; }|.
::examples_canvas_addlinemetered::wire::Point ParsePoint(std::string_view input) {
  int64_t x = 0;
  int64_t y = 0;
  size_t index = input.find(',');
  if (index != std::string::npos) {
    std::from_chars(input.data(), input.data() + index, x);
    std::from_chars(input.data() + index + 1, input.data() + input.length(), y);
  }
  return ::examples_canvas_addlinemetered::wire::Point{.x = x, .y = y};
}

// A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it
// into an array of 2 |Point| structs.
::fidl::Array<::examples_canvas_addlinemetered::wire::Point, 2> ParseLine(
    const std::string& action) {
  auto input = std::string_view(action);
  size_t index = input.find(':');
  if (index != std::string::npos) {
    return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))};
  }
  return {};
}

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop and dispatcher.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the 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<examples_canvas_addlinemetered::Instance>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // Create an instance of the event handler.
  EventHandler event_handler(loop);

  // Create an asynchronous client using the newly-established connection.
  fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler);
  FX_LOGS(INFO) << "Outgoing connection enabled";

  for (const auto& action : conf.script()) {
    // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received
    // from the server.
    if (action == "WAIT") {
      loop.Run();
      loop.ResetQuit();
      continue;
    }

    // Draw a line to the canvas by calling the server, using the two points we just parsed
    // above as arguments.
    auto line = ParseLine(action);
    FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x << ", y: " << line[1].y
                  << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]";

    client->AddLine(line).ThenExactlyOnce(
        [&](fidl::WireUnownedResult<examples_canvas_addlinemetered::Instance::AddLine>& result) {
          // Check if the FIDL call succeeded or not.
          if (!result.ok()) {
            // Check that our two-way call succeeded, and handle the error appropriately. In the
            // case of this example, there is nothing we can do to recover here, except to log an
            // error and exit the program.
            FX_LOGS(ERROR) << "Could not send AddLine request: " << result.status_string();
          }
          FX_LOGS(INFO) << "AddLine response received";

          // Quit the loop, thereby handing control back to the outer loop of actions being iterated
          // over.
          loop.Quit();
        });

    // Run the loop until the callback is resolved, at which point we can continue from here.
    loop.Run();
    loop.ResetQuit();
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.addlinemetered/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <src/lib/fxl/macros.h>
#include <src/lib/fxl/memory/weak_ptr.h>

// A struct that stores the two things we care about for this example: the set of lines, and the
// bounding box that contains them.
struct CanvasState {
  // Tracks whether there has been a change since the last send, to prevent redundant updates.
  bool changed = true;
  examples_canvas_addlinemetered::wire::BoundingBox bounding_box;
};

// An implementation of the |Instance| protocol.
class InstanceImpl final : public fidl::WireServer<examples_canvas_addlinemetered::Instance> {
 public:
  // Bind this implementation to a channel.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end)
      : binding_(fidl::BindServer(
            dispatcher, std::move(server_end), this,
            [this](InstanceImpl* impl, fidl::UnbindInfo info,
                   fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) {
              if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) {
                FX_LOGS(ERROR) << "Shutdown unexpectedly";
              }
              delete this;
            })),
        weak_factory_(this) {
    // Start the update timer on startup. Our server sends one update per second
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void AddLine(AddLineRequestView request, AddLineCompleter::Sync& completer) override {
    auto points = request->line;
    FX_LOGS(INFO) << "AddLine request received: [Point { x: " << points[1].x
                  << ", y: " << points[1].y << " }, Point { x: " << points[0].x
                  << ", y: " << points[0].y << " }]";

    // Update the bounding box to account for the new line we've just "added" to the canvas.
    auto& bounds = state_.bounding_box;
    for (const auto& point : request->line) {
      if (point.x < bounds.top_left.x) {
        bounds.top_left.x = point.x;
      }
      if (point.y > bounds.top_left.y) {
        bounds.top_left.y = point.y;
      }
      if (point.x > bounds.bottom_right.x) {
        bounds.bottom_right.x = point.x;
      }
      if (point.y < bounds.bottom_right.y) {
        bounds.bottom_right.y = point.y;
      }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn|
    // event.
    state_.changed = true;

    // Because this is now a two-way method, we must use the generated |completer| to send an in
    // this case empty reply back to the client. This is the mechanic which syncs the flow rate
    // between the client and server on this method, thereby preventing the client from "flooding"
    // the server with unacknowledged work.
    completer.Reply();
    FX_LOGS(INFO) << "AddLine response sent";
  }

  void handle_unknown_method(
      fidl::UnknownMethodMetadata<examples_canvas_addlinemetered::Instance> metadata,
      fidl::UnknownMethodCompleter::Sync& completer) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal;
  }

 private:
  // Each scheduled update waits for the allotted amount of time, sends an update if something has
  // changed, and schedules the next update.
  void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) {
    async::PostDelayedTask(
        dispatcher,
        [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] {
          // Halt execution if the binding has been deallocated already.
          if (!weak) {
            return;
          }

          // Schedule the next update if the binding still exists.
          weak->ScheduleOnDrawnEvent(dispatcher, after);

          // No need to send an update if nothing has changed since the last one.
          if (!weak->state_.changed) {
            return;
          }

          // This is where we would draw the actual lines. Since this is just an example, we'll
          // avoid doing the actual rendering, and simply send the bounding box to the client
          // instead.
          auto top_left = weak->state_.bounding_box.top_left;
          auto bottom_right = weak->state_.bounding_box.bottom_right;
          fidl::Status status =
              fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right);
          if (!status.ok()) {
            return;
          }
          FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x
                        << ", y: " << top_left.y
                        << " }, bottom_right: Point { x: " << bottom_right.x
                        << ", y: " << bottom_right.y << " }";

          // Reset the change tracker.
          weak->state_.changed = false;
        },
        after);
  }

  fidl::ServerBindingRef<examples_canvas_addlinemetered::Instance> binding_;
  CanvasState state_ = CanvasState{};

  // Generates weak references to this object, which are appropriate to pass into asynchronous
  // callbacks that need to access this object. The references are automatically invalidated
  // if this object is destroyed.
  fxl::WeakPtrFactory<InstanceImpl> weak_factory_;
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This
  // directory is where the outgoing FIDL protocols are installed so that they can be provided to
  // other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle.
  // The startup handle is a handle provided to every component by the system, so that they can
  // serve capabilities (e.g. FIDL protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to
  // |examples.canvas.addlinemetered.Instance|.
  result = outgoing.AddUnmanagedProtocol<examples_canvas_addlinemetered::Instance>(
      [dispatcher](fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) {
        // Create an instance of our InstanceImpl that destroys itself when the connection closes.
        new InstanceImpl(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string();
    return -1;
  }

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

HLCPP

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <lib/async-loop/cpp/loop.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/canvas/addlinemetered/cpp/fidl.h>
#include <examples/fidl/new/canvas/add_line_metered/hlcpp/client/config.h>

#include "lib/fpromise/result.h"

// A helper function that takes a coordinate in string form, like "123,-456", and parses it into a
// a struct of the form |{ in64 x; int64 y; }|.
::examples::canvas::addlinemetered::Point ParsePoint(std::string_view input) {
  int64_t x = 0;
  int64_t y = 0;
  size_t index = input.find(',');
  if (index != std::string::npos) {
    std::from_chars(input.data(), input.data() + index, x);
    std::from_chars(input.data() + index + 1, input.data() + input.length(), y);
  }
  return ::examples::canvas::addlinemetered::Point{.x = x, .y = y};
}

// A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it
// into an array of 2 |Point| structs.
::std::array<::examples::canvas::addlinemetered::Point, 2> ParseLine(const std::string& action) {
  auto input = std::string_view(action);
  size_t index = input.find(':');
  if (index != std::string::npos) {
    return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))};
  }
  return {};
}

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the protocol inside the component's namespace, then create an asynchronous client
  // using the newly-established connection.
  examples::canvas::addlinemetered::InstancePtr instance_proxy;
  auto context = sys::ComponentContext::Create();
  context->svc()->Connect(instance_proxy.NewRequest(dispatcher));
  FX_LOGS(INFO) << "Outgoing connection enabled";

  instance_proxy.set_error_handler([&loop](zx_status_t status) {
    FX_LOGS(ERROR) << "Shutdown unexpectedly";
    loop.Quit();
  });

  // Provide a lambda to handle incoming |OnDrawn| events asynchronously.
  instance_proxy.events().OnDrawn = [&loop](
                                        ::examples::canvas::addlinemetered::Point top_left,
                                        ::examples::canvas::addlinemetered::Point bottom_right) {
    FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x
                  << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x
                  << ", y: " << bottom_right.y << " }";
    loop.Quit();
  };

  instance_proxy.events().handle_unknown_event = [](uint64_t ordinal) {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << ordinal;
  };

  for (const auto& action : conf.script()) {
    // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received
    // from the server.
    if (action == "WAIT") {
      loop.Run();
      loop.ResetQuit();
      continue;
    }

    // Draw a line to the canvas by calling the server, using the two points we just parsed
    // above as arguments.
    auto line = ParseLine(action);
    FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x << ", y: " << line[1].y
                  << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]";

    instance_proxy->AddLine(line, [&](fpromise::result<void, fidl::FrameworkErr> result) {
      if (result.is_error()) {
        // Check that our flexible two-way call was known to the server and handle the case of an
        // unknown method appropriately. In the case of this example, there is nothing we can do to
        // recover here, except to log an error and exit the program.
        FX_LOGS(ERROR) << "Server does not implement AddLine";
      }
      FX_LOGS(INFO) << "AddLine response received";

      // Quit the loop, thereby handing control back to the outer loop of actions being iterated
      // over.
      loop.Quit();
    });

    // Run the loop until the callback is resolved, at which point we can continue from here.
    loop.Run();
    loop.ResetQuit();
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async/cpp/task.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <examples/canvas/addlinemetered/cpp/fidl.h>
#include <src/lib/fxl/macros.h>
#include <src/lib/fxl/memory/weak_ptr.h>

// A struct that stores the two things we care about for this example: the set of lines, and the
// bounding box that contains them.
struct CanvasState {
  // Tracks whether there has been a change since the last send, to prevent redundant updates.
  bool changed = true;
  examples::canvas::addlinemetered::BoundingBox bounding_box;
};

// An implementation of the |Instance| protocol.
class InstanceImpl final : public examples::canvas::addlinemetered::Instance {
 public:
  // Bind this implementation to an |InterfaceRequest|.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::InterfaceRequest<examples::canvas::addlinemetered::Instance> request)
      : binding_(fidl::Binding<examples::canvas::addlinemetered::Instance>(this)),
        weak_factory_(this) {
    binding_.Bind(std::move(request), dispatcher);

    // Gracefully handle abrupt shutdowns.
    binding_.set_error_handler([this](zx_status_t status) mutable {
      if (status != ZX_ERR_PEER_CLOSED) {
        FX_LOGS(ERROR) << "Shutdown unexpectedly";
      }
      delete this;
    });

    // Start the update timer on startup. Our server sends one update per second.
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void AddLine(::std::array<::examples::canvas::addlinemetered::Point, 2> line,
               AddLineCallback callback) override {
    FX_LOGS(INFO) << "AddLine request received: [Point { x: " << line[1].x << ", y: " << line[1].y
                  << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]";

    // Update the bounding box to account for the new line we've just "added" to the canvas.
    auto& bounds = state_.bounding_box;
    for (const auto& point : line) {
      if (point.x < bounds.top_left.x) {
        bounds.top_left.x = point.x;
      }
      if (point.y > bounds.top_left.y) {
        bounds.top_left.y = point.y;
      }
      if (point.x > bounds.bottom_right.x) {
        bounds.bottom_right.x = point.x;
      }
      if (point.y < bounds.bottom_right.y) {
        bounds.bottom_right.y = point.y;
      }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn|
    // event.
    state_.changed = true;

    // Because this is now a two-way method, we must use the generated |callback| to send an in
    // this case empty reply back to the client. This is the mechanic which syncs the flow rate
    // between the client and server on this method, thereby preventing the client from "flooding"
    // the server with unacknowledged work.
    callback(fpromise::ok());
    FX_LOGS(INFO) << "AddLine response sent";
  }

  void handle_unknown_method(uint64_t ordinal, bool method_has_response) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << ordinal;
  }

 private:
  // Each scheduled update waits for the allotted amount of time, sends an update if something has
  // changed, and schedules the next update.
  void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) {
    async::PostDelayedTask(
        dispatcher,
        [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] {
          // Halt execution if the binding has been deallocated already.
          if (!weak) {
            return;
          }

          // Schedule the next update if the binding still exists.
          weak->ScheduleOnDrawnEvent(dispatcher, after);

          // No need to send an update if nothing has changed since the last one.
          if (!weak->state_.changed) {
            return;
          }

          // This is where we would draw the actual lines. Since this is just an example, we'll
          // avoid doing the actual rendering, and simply send the bounding box to the client
          // instead.
          auto top_left = state_.bounding_box.top_left;
          auto bottom_right = state_.bounding_box.bottom_right;
          binding_.events().OnDrawn(top_left, bottom_right);
          FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x
                        << ", y: " << top_left.y
                        << " }, bottom_right: Point { x: " << bottom_right.x
                        << ", y: " << bottom_right.y << " }";

          // Reset the change tracker.
          state_.changed = false;
        },
        after);
  }

  fidl::Binding<examples::canvas::addlinemetered::Instance> binding_;
  CanvasState state_ = CanvasState{};

  // Generates weak references to this object, which are appropriate to pass into asynchronous
  // callbacks that need to access this object. The references are automatically invalidated
  // if this object is destroyed.
  fxl::WeakPtrFactory<InstanceImpl> weak_factory_;
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  //
  // Note that unlike the new C++ bindings, HLCPP bindings rely on the async loop being attached to
  // the current thread via the |kAsyncLoopConfigAttachToCurrentThread| configuration.
  async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component.
  // This directory is where the outgoing FIDL protocols are installed so that they can be
  // provided to other components.
  auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory();

  // Register a handler for components trying to connect to
  // |examples.canvas.addlinemetered.Instance|.
  context->outgoing()->AddPublicService(
      fidl::InterfaceRequestHandler<examples::canvas::addlinemetered::Instance>(
          [dispatcher](fidl::InterfaceRequest<examples::canvas::addlinemetered::Instance> request) {
            // Create an instance of our |InstanceImpl| that destroys itself when the connection
            // closes.
            new InstanceImpl(dispatcher, std::move(request));
          }));

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

別名

FIDL 方案:別名

alias 是 FIDL 宣告,會將新的名稱指派給現有類型。這麼做有幾個好處:

  • 使用 alias 可確保有別名類型所代表概念的單一可靠資料來源。
  • 可為事物命名,尤其是受限的類型。
  • 立即別名類型的不同用途可能會與同一概念的例項連結。

請注意,別名目前不會傳輸至已產生的繫結程式碼。換句話說,指派給 alias 宣告的名稱永遠不會在產生的 FIDL 程式碼中顯示為宣告名稱。

在這個範例中,為 Key 新增 alias 可讓我們避免以自訂的名稱重複,同時讓讀取器清楚知道 Item 類型的 key 值和 ReadItem 要求結構中使用的 key 是刻意用的,並非只是巧妙地相同。

推理

原始的唯寫鍵/值存放區現已擴充,可讀取商店外的項目。

實作

適用於 FIDL 和 CML 定義的變更如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.addreaditem;

// Aliases for the key and value. Using aliases helps increase the readability of FIDL files and
// reduces likelihood of errors due to differing constraints.
alias Key = string:128;
alias Value = vector<byte>:64000;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key Key;
    value Value;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// An enumeration of things that may go wrong when trying to read a value out of our store.
type ReadError = flexible enum {
    UNKNOWN = 0;
    NOT_FOUND = 1;
};

/// A very basic key-value store - so basic, in fact, that one may only write to it, never read!
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;

    /// Reads an item from the store.
    flexible ReadItem(struct {
        key Key;
    }) -> (Item) error ReadError;
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.addreaditem.Store" },
    ],
    config: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        read_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.addreaditem.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.addreaditem.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.addreaditem.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

所有語言的用戶端和伺服器實作也有所變更:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fidl_examples_keyvaluestore_addreaditem::{Item, StoreMarker},
    fuchsia_component::client::connect_to_protocol,
    std::{str, thread, time},
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        match store.write_item(&Item { key: key, value: value.into_bytes() }).await? {
            Ok(_) => println!("WriteItem Success"),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // The structured config for this client contains `read_items`, a vector of strings, each of
    // which is meant to be read from the key-value store. We iterate over these keys, attempting to
    // read them in turn.
    for key in config.read_items.into_iter() {
        let res = store.read_item(key.as_str()).await;
        match res.unwrap() {
            Ok(val) => {
                println!("ReadItem Success: key: {}, value: {}", key, str::from_utf8(&val.1)?)
            }
            Err(err) => println!("ReadItem Error: {}", err.into_primitive()),
        }
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fidl_examples_keyvaluestore_addreaditem::{
        Item, ReadError, StoreRequest, StoreRequestStream, WriteError,
    },
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
    std::cell::RefCell,
    std::collections::hash_map::Entry,
    std::collections::HashMap,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z][A-Za-z0-9_\./]{2,62}[A-Za-z0-9]$")
            .expect("Key validation regex failed to compile");
}

/// Handler for the `WriteItem` method.
fn write_item(store: &mut HashMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Validate the value.
    if attempt.value.is_empty() {
        println!("Write error: INVALID_VALUE, For key: {}", attempt.key);
        return Err(WriteError::InvalidValue);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote value at key: {}", entry.key());
            entry.insert(attempt.value);
            Ok(())
        }
    }
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    let store = RefCell::new(HashMap::<String, Vec<u8>>::new());

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.borrow_mut(), attempt))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::ReadItem { key, responder } => {
                    println!("ReadItem request received");

                    // Read the item from the store, returning the appropriate error if it could not be found.
                    responder
                        .send(match store.borrow().get(&key) {
                            Some(found) => {
                                println!("Read value at key: {}", key);
                                Ok((&key, found))
                            }
                            None => {
                                println!("Read error: NOT_FOUND, For key: {}", key);
                                Err(ReadError::NotFound)
                            }
                        })
                        .context("error sending reply")?;
                    println!("ReadItem response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

匿名類型

FIDL 方案:匿名類型

「匿名類型」是一種定義位於其使用中,而非獨立名為 type 宣告的類型。使用匿名類型有兩項好處首先,它們能防止命名空間汙染過多,因為需要只使用一次的名稱類型 FIDL 作者消失。其次,由於無法透過名稱識別類型,因此這些變數會阻止透過 using 宣告將類型匯入另一個 FIDL 程式庫。

在這個變數中,我們允許鍵/值存放區將其他鍵/值存放區視為成員。簡單來說,我們把它變成一個樹狀結構。具體做法是將 value 的原始定義替換成使用兩成員 union 的變數:一個變化版本會儲存與先前相同 vector<byte> 類型的分葉節點,另一個變數則會以其他巢狀儲存庫的形式儲存分支節點。

推理

在這裡,我們來看看幾個「選用性」用法,藉此宣告可能存在或不存在的類型。FIDL 中有三種選用變種:

  • 類型一律儲存在線的類型,因此具有透過 null 信封描述「缺失」的內建方法。啟用這些類型的選用功能不會影響訊息的傳輸形狀,只會變更該特定類型有效值。透過新增 :optional 限制,unionvector<T>client_endserver_endzx.Handle 類型都可以設為選用。將 value union 設為選用項目,我們就能以缺少 value 的形式,引入標準「空值」項目。這表示空白的 bytes 以及缺少/空白的 store 屬性是無效的值。
  • 與上述類型不同,struct 版面配置並無額外空間,可儲存空值標頭。因此,郵件必須納入信封中,藉此變更所納入訊息的傳輸模式形狀。為確保此線路修改效果清晰易讀,Item struct 類型必須納入 box<T> 類型範本中。
  • 最後,table 版面配置一律為選用項目。缺少的 table 只是沒有任何成員設定的項目。

樹狀結構是一種自然的自參照資料結構,樹狀結構中的任何節點可能包含包含純資料的分葉 (在本例中為字串),或含有更多節點的子樹狀結構。這需要使用遞迴:Item 的定義現在間接依附於本身!在 FIDL 中代表遞迴類型可能有點困難,尤其是因為目前支援有部分限制。只要自我參照建立的週期中至少有一個選用類型,我們就能支援這類類型。舉例來說,這裡我們將 items struct 成員定義為 box<Item>,進而破壞包含週期。

這些變更也會大量使用「匿名類型」,或宣告內嵌於其唯一使用點,而非自行命名的頂層 type 宣告類型。根據預設,在產生的語言繫結中,匿名類型名稱取自本機背景資訊。舉例來說,新推出的 flexible union 會使用其本身成員的名稱 Value,新引入的 struct 會變成 Store,以此類推。由於此經驗法則有時可能會造成衝突,因此 FIDL 允許作者手動覆寫匿名類型的「產生的名稱」,提供一個跳躍點。方法是透過 @generated_name 屬性變更後端產生的名稱。我們可以在這裡使用一個,其中預計的 Store 類型已重新命名為 NestedStore,以避免與 protocol 宣告使用相同名稱的名稱衝突。

實作

FIDL、CML 和領域介面的定義修改如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.supporttrees;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value strict union {
        // Keep the original `bytes` as one of the options in the new union. All leaf nodes in the
        // tree must be `bytes`, or absent unions (representing empty). Empty byte arrays are
        // disallowed.
        1: bytes vector<byte>:64000;

        // Allows a store within a store, thereby turning our flat key-value store into a tree
        // thereof. Note the use of `@generated_name` to prevent a type-name collision with the
        // `Store` protocol below, and the use of `box<T>` to ensure that there is a break in the
        // chain of recursion, thereby allowing `Item` to include itself in its own definition.
        //
        // This is a table so that added fields, like for example a `hash`, can be easily added in
        // the future.
        2: store @generated_name("nested_store") table {
            1: items vector<box<Item>>;
        };
    }:optional;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// A very basic key-value store.
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.supporttrees.Store" },
    ],
    config: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // A newline separated list nested entries. The first line should be the key
        // for the nested store, and each subsequent entry should be a pointer to a text file
        // containing the string value. The name of that text file (without the `.txt` suffix) will
        // serve as the entries key.
        write_nested: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // A list of keys, all of which will be populated as null entries.
        write_null: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.supporttrees.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.supporttrees.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.supporttrees.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fidl_examples_keyvaluestore_supporttrees::{Item, NestedStore, StoreMarker, Value},
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        let res = store
            .write_item(&Item {
                key: key.clone(),
                value: Some(Box::new(Value::Bytes(value.into_bytes()))),
            })
            .await;
        match res? {
            Ok(_) => println!("WriteItem Success at key: {}", key),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // Add nested entries to the key-value store as well. The entries are strings, where the first
    // line is the key of the entry, and each subsequent entry should be a pointer to a text file
    // containing the string value. The name of that text file (without the `.txt` suffix) will
    // serve as the entries key.
    for spec in config.write_nested.into_iter() {
        let mut items = vec![];
        let mut nested_store = NestedStore::default();
        let mut lines = spec.split("\n");
        let key = lines.next().unwrap();

        // For each entry, make a new entry in the `NestedStore` being built.
        for entry in lines {
            let path = format!("/pkg/data/{}.txt", entry);
            let contents = std::fs::read_to_string(path.clone())
                .with_context(|| format!("Failed to load {path}"))?;
            items.push(Some(Box::new(Item {
                key: entry.to_string(),
                value: Some(Box::new(Value::Bytes(contents.into()))),
            })));
        }
        nested_store.items = Some(items);

        // Send the `NestedStore`, represented as a vector of values.
        let res = store
            .write_item(&Item {
                key: key.to_string(),
                value: Some(Box::new(Value::Store(nested_store))),
            })
            .await;
        match res? {
            Ok(_) => println!("WriteItem Success at key: {}", key),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // Each entry in this list is a null value in the store.
    for key in config.write_null.into_iter() {
        match store.write_item(&Item { key: key.to_string(), value: None }).await? {
            Ok(_) => println!("WriteItem Success at key: {}", key),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fidl_examples_keyvaluestore_supporttrees::{
        Item, StoreRequest, StoreRequestStream, Value, WriteError,
    },
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
    std::cell::RefCell,
    std::collections::hash_map::Entry,
    std::collections::HashMap,
    std::str::from_utf8,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile");
}

// A representation of a key-value store that can contain an arbitrarily deep nesting of other
// key-value stores.
#[allow(dead_code)] // TODO(https://fxbug.dev/318827209)
enum StoreNode {
    Leaf(Option<Vec<u8>>),
    Branch(Box<HashMap<String, StoreNode>>),
}

/// Recursive item writer, which takes a `StoreNode` that may not necessarily be the root node, and
/// writes an entry to it.
fn write_item(
    store: &mut HashMap<String, StoreNode>,
    attempt: Item,
    path: &str,
) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            let key = format!("{}{}", &path, entry.key());
            match attempt.value {
                // Null entries are allowed.
                None => {
                    println!("Wrote value: NONE at key: {}", key);
                    entry.insert(StoreNode::Leaf(None));
                }
                Some(value) => match *value {
                    // If this is a nested store, recursively make a new store to insert at this
                    // position.
                    Value::Store(entry_list) => {
                        // Validate the value - absent stores, items lists with no children, or any
                        // of the elements within that list being empty boxes, are all not allowed.
                        if entry_list.items.is_some() {
                            let items = entry_list.items.unwrap();
                            if !items.is_empty() && items.iter().all(|i| i.is_some()) {
                                let nested_path = format!("{}/", key);
                                let mut nested_store = HashMap::<String, StoreNode>::new();
                                for item in items.into_iter() {
                                    write_item(&mut nested_store, *item.unwrap(), &nested_path)?;
                                }

                                println!("Created branch at key: {}", key);
                                entry.insert(StoreNode::Branch(Box::new(nested_store)));
                                return Ok(());
                            }
                        }

                        println!("Write error: INVALID_VALUE, For key: {}", key);
                        return Err(WriteError::InvalidValue);
                    }

                    // This is a simple leaf node on this branch.
                    Value::Bytes(value) => {
                        // Validate the value.
                        if value.is_empty() {
                            println!("Write error: INVALID_VALUE, For key: {}", key);
                            return Err(WriteError::InvalidValue);
                        }

                        println!("Wrote key: {}, value: {:?}", key, from_utf8(&value).unwrap());
                        entry.insert(StoreNode::Leaf(Some(value)));
                    }
                },
            }
            Ok(())
        }
    }
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    let store = RefCell::new(HashMap::<String, StoreNode>::new());

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.borrow_mut(), attempt, ""))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

點數

FIDL 方案:小點

bits 類型是 FIDL 表示位元陣列的方式。在需要一組布林標記的情況下使用。bits 陣列通常用於「覆蓋」基礎子類型,進而控制網路的位元率。

推理

鍵/值儲存庫基準範例實作是很好的起點,但主要缺點是將資料儲存為原始位元組。FIDL 是一種多重輸入的語言。強制將 UTF-8 字串儲存為無類型位元組陣列的資料,會為 *.fidl 檔案讀取器以及使用從這個檔案產生的繫結的程式設計人員清除這項寶貴類型資訊。

實作

這項變更的主要目標是將基準案例的 vector<byte> 類型 value 成員替換成儲存許多可能類型的 union。事實上,此變更是針對 FIDL 的類型進行的良好調查:

  • 所有 FIDL 內建的純量類型都會做為 Value union 中的變體使用:booluint8uint16uint32uint64int8int16int32int64float32float64 (也稱為 FIDL 的原始類型),以及 。string
  • 這個 union 也使用 FIDL 的內建 array<T, N>vector<T> 類型範本。
  • 所有 FIDL 的類型版面配置 (包括 bitsenumtableunionstruct) 至少都在此範例中使用一次。

WriteItem 使用的要求和回應酬載也已從 struct 變更為命名的 table 和內嵌的 flexible union。事實上,這三種版面配置中的任一個,都可以使用要求/回應酬載。除了訊息大小最敏感的情況下,系統會分別使用後者 (分別稱為「資料表酬載」和 *聯集酬載)。這是因為日後能以與二進位檔相容的方式加以擴充,方便日後擴充。

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.usegenericvalues;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value Value;
};

// Because the `Value` must be used both in the request and the response, we give it its own named
// type. The type is a `union` of all possible data types that we take as values, and is marked
// `flexible` to allow for the easy addition of new data types in the future.
type Value = flexible union {
    // Keep the original `bytes` as one of the options in the new union.
    1: bytes vector<byte>:64000;

    // A `string` is very similar to `vector<byte>` on the wire, with the extra constraint that
    // it enforces that it enforces that the byte vector in question is valid UTF-8.
    2: string string:64000;

    // All of FIDL's primitive types.
    3: bool bool;
    4: uint8 uint8;
    5: int8 int8;
    6: uint16 uint16;
    7: int16 int16;
    8: uint32 uint32;
    9: int32 int32;
   10: float32 float32;
   11: uint64 uint64;
   12: int64 int64;
   13: float64 float64;

    // FIDL does not natively support 128-bit integer types, so we have to define our own
    // representations.
   14: uint128 array<uint64, 2>;
};

// Because we now supoprt a richer range of types as values in our store, it is helpful to use a
// `flexible`, and therefore evolvable, `bits` type to store write options.
type WriteOptions = flexible bits : uint8 {
    // This flag allows us to overwrite existing data when there is a collision, rather than failing
    // with an `WriteError.ALREADY_EXISTS`.
    OVERWRITE = 0b1;
    // This flag allows us to concatenate to existing data when there is a collision, rather than
    // failing with an `WriteError.ALREADY_EXISTS`. "Concatenation" means addition for the numeric
    // variants and appending to the `bytes`/`string` variants. If no existing data can be found, we
    // "concatenate" to default values of zero and an empty vector, respectively. Attempting to
    // concatenate to an existing variant of a different type will return a
    // `WriteError.INVALID_VALUE` error.
    CONCAT = 0b10;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// A very basic key-value store.
@discoverable
open protocol Store {
    /// Writes an item to the store.
    ///
    /// Since the value stored in the key-value store can now be different from the input (if the
    /// `WriteOptions.CONCAT` flag is set), we need to return the resulting `Value` to the
    /// requester.
    ///
    /// We use an (anonymous) `table` and a (named) `flexible union` as the request and response
    /// payload, respectively, to allow for easier future evolution. Both of these types are
    /// `flexible`, meaning that adding or removing members is binary-compatible. This makes them
    /// much easier to evolve that the `struct` types that were previously used, which cannot be
    /// changed after release without breaking ABI.
    flexible WriteItem(table {
        1: attempt Item;
        2: options WriteOptions;
    }) -> (Value) error WriteError;
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.usegenericvalues.Store" },
    ],
    config: {
        // A vector of values for every easily representible type in our key-value store. For
        // brevity's sake, the 8, 16, and 32 bit integer types and booleans are omitted.
        //
        // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great
        //   deal of string parsing if we were able to use all FIDL constructs directly here. In
        //   particular, floats and nested types are very difficult to represent, and have been
        //   excluded from this example for the time being.
        set_concat_option: { type: "bool" },
        set_overwrite_option: { type: "bool" },
        write_bytes: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },
        write_strings: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },
        write_uint64s: {
            type: "vector",
            max_count: 16,
            element: { type: "uint64" },
        },
        write_int64s: {
            type: "vector",
            max_count: 16,
            element: { type: "int64" },
        },

        // Note: due to the limitation of structured config not allowing vectors nested in vectors,
        // we only set the lower half of the uint128 for simplicity's sake.
        write_uint128s: {
            type: "vector",
            max_count: 16,
            element: { type: "uint64" },
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.usegenericvalues.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.usegenericvalues.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.usegenericvalues.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fidl_examples_keyvaluestore_usegenericvalues::{
        Item, StoreMarker, StoreProxy, StoreWriteItemRequest, Value, WriteOptions,
    },
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

// A helper function to sequentially write a single item to the key-value store and print a log when
// successful.
async fn write_next_item(
    store: &StoreProxy,
    key: &str,
    value: Value,
    options: WriteOptions,
) -> Result<(), Error> {
    // Create an empty request payload using `::default()`.
    let mut req = StoreWriteItemRequest::default();
    req.options = Some(options);

    // Fill in the `Item` we will be attempting to write.
    println!("WriteItem request sent: key: {}, value: {:?}", &key, &value);
    req.attempt = Some(Item { key: key.to_string(), value: value });

    // Send and async `WriteItem` request to the server.
    match store.write_item(&req).await.context("Error sending request")? {
        Ok(value) => println!("WriteItem response received: {:?}", &value),
        Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
    }
    Ok(())
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // All of our requests will have the same bitflags set. Pull these settings from the config.
    let mut options = WriteOptions::empty();
    options.set(WriteOptions::OVERWRITE, config.set_overwrite_option);
    options.set(WriteOptions::CONCAT, config.set_concat_option);

    // The structured config provides one input for most data types that can be stored in the data
    // store. Iterate through those inputs in the order we see them in the FIDL file.
    //
    // Note that FIDL unions are rendered as enums in Rust; for example, the `Value` union has now
    // become a `Value` Rust enum, with each member taking exactly one argument.
    for value in config.write_bytes.into_iter() {
        write_next_item(&store, "bytes", Value::Bytes(value.into()), options).await?;
    }
    for value in config.write_strings.into_iter() {
        write_next_item(&store, "string", Value::String(value), options).await?;
    }
    for value in config.write_uint64s.into_iter() {
        write_next_item(&store, "uint64", Value::Uint64(value), options).await?;
    }
    for value in config.write_int64s.into_iter() {
        write_next_item(&store, "int64", Value::Int64(value), options).await?;
    }
    for value in config.write_uint128s.into_iter() {
        write_next_item(&store, "uint128", Value::Uint128([0, value]), options).await?;
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
    std::cell::RefCell,
    std::collections::hash_map::Entry,
    std::collections::HashMap,
};

use {
    fidl_examples_keyvaluestore_usegenericvalues::{
        Item, StoreRequest, StoreRequestStream, Value, WriteError, WriteOptions,
    },
    std::collections::hash_map::OccupiedEntry,
    std::ops::Add,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile");
}

/// Sums any numeric type.
fn sum<T: Add + Add<Output = T> + Copy>(operands: [T; 2]) -> T {
    operands[0] + operands[1]
}

/// Clones and inserts an entry, so that the original (now concatenated) copy may be returned in the
/// response.
fn write(inserting: Value, mut entry: OccupiedEntry<'_, String, Value>) -> Value {
    entry.insert(inserting.clone());
    println!("Wrote key: {}, value: {:?}", entry.key(), &inserting);
    inserting
}

/// Handler for the `WriteItem` method.
fn write_item(
    store: &mut HashMap<String, Value>,
    attempt: Item,
    options: &WriteOptions,
) -> Result<Value, WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY for key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            // The `CONCAT` flag supersedes the `OVERWRITE` flag, so check it first.
            if options.contains(WriteOptions::CONCAT) {
                match entry.get() {
                    Value::Bytes(old) => {
                        if let Value::Bytes(new) = attempt.value {
                            let mut combined = old.clone();
                            combined.extend(new);
                            return Ok(write(Value::Bytes(combined), entry));
                        }
                    }
                    Value::String(old) => {
                        if let Value::String(new) = attempt.value {
                            return Ok(write(Value::String(format!("{}{}", old, &new)), entry));
                        }
                    }
                    Value::Uint64(old) => {
                        if let Value::Uint64(new) = attempt.value {
                            return Ok(write(Value::Uint64(sum([*old, new])), entry));
                        }
                    }
                    Value::Int64(old) => {
                        if let Value::Int64(new) = attempt.value {
                            return Ok(write(Value::Int64(sum([*old, new])), entry));
                        }
                    }
                    // Note: only works on the uint64 range in practice.
                    Value::Uint128(old) => {
                        if let Value::Uint128(new) = attempt.value {
                            return Ok(write(Value::Uint128([0, sum([old[1], new[1]])]), entry));
                        }
                    }
                    _ => {
                        panic!("actively unsupported type!")
                    }
                }

                // Only reachable if the type of the would be concatenated value did not match the
                // value already occupying this entry.
                println!("Write error: INVALID_VALUE for key: {}", entry.key());
                return Err(WriteError::InvalidValue);
            }

            // If we're not doing CONCAT, check for OVERWRITE next.
            if options.contains(WriteOptions::OVERWRITE) {
                return Ok(write(attempt.value, entry));
            }

            println!("Write error: ALREADY_EXISTS for key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote key: {}, value: {:?}", entry.key(), &attempt.value);
            entry.insert(attempt.value.clone());
            Ok(attempt.value)
        }
    }
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    let store = RefCell::new(HashMap::<String, Value>::new());

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                // Because we are using a table payload, there is an extra level of indirection. The
                // top-level container for the table itself is always called "payload".
                StoreRequest::WriteItem { payload, responder } => {
                    println!("WriteItem request received");

                    // Error out if either of the request table's members are not set.
                    let attempt = payload.attempt.context("required field 'attempt' is unset")?;
                    let options = payload.options.context("required field 'options' is unset")?;

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(
                            write_item(&mut store.borrow_mut(), attempt, &options)
                                .as_ref()
                                .map_err(|e| *e),
                        )
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

可供偵測

FIDL 方案:@discoverable

@discoverable 屬性會指派服務探索的名稱。這可讓用戶端搜尋正確的名稱,而無需手動確保查詢名稱與伺服器端傳送的名稱相符。

在此範例中,您將建立基本的計算機伺服器和用戶端,用於顯示先定義和提供和使用 FIDL 通訊協定所需的基本設定。

首先,請定義介面定義和測試控管工具。介面定義 (.fidl 檔案本身) 是任何新 FIDL 通訊協定的起點。此外,計算機還包含必要的 CML 和領域定義,以建立用戶端伺服器模式,這個模式可以做為專案鷹架,用於任意實作。

請參閱下列 FIDL 代碼:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// The namespace for this FIDL protocol. This namespace is how both consumers (clients) and providers (servers) reference this protocol.
library examples.calculator.baseline;

// @discoverable indicates 'Calculator' is a protocol that will be served under the examples.calculator.baseline libarary namespace. https://fuchsia.dev/fuchsia-src/reference/fidl/language/attributes#discoverable . If @discoverable is missing, it will lead to a compile time error when trying to import the library.
@discoverable
// A limited-functionality calculator 'protocol' that adds and subtracts integers.
open protocol Calculator {
    // Takes as input a struct with two integers, and returns their sum: (a+b)=sum.  This method is infallible (no errors can be generated) as two int32's cannot overflow a result type of int64.
    flexible Add(struct {
        a int32;
        b int32;
    }) -> (struct {
        sum int64;
    });
    // Takes as input a struct with two integers, and returns their difference: (a-b)=difference.  This method is infallible (no errors can be generated) as two int32's cannot overflow a result type of int64.
    flexible Subtract(struct {
        a int32;
        b int32;
    }) -> (struct {
        difference int64;
    });
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.calculator.baseline.Calculator" },
    ],
    config: {},
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.calculator.baseline.Calculator" },
    ],
    expose: [
        {
            protocol: "examples.calculator.baseline.Calculator",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.calculator.baseline.Calculator",
            from: "#server",
            to: "#client",
        },

        // Route logging support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// TODO(https://fxbug.dev/42063075): Rust implementation.

伺服器

// TODO(https://fxbug.dev/42063075): Rust implementation.

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42063075): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42063075): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42063075): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42063075): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42063075): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42063075): HLCPP implementation.

如本例所示,從頭開始建立 FIDL 通訊協定是平台開發人員等特定開發人員的常見情境。不過,其他類型的開發人員也受益於瞭解如何建構 FIDL 通訊協定,即使一般不如此也是如此。這可協助您瞭解 FIDL 的所有資訊如何相輔相成,包括語法、文法、語言功能、如何提供和使用指定的 FIDL 通訊協定,以及建構系統的運作方式。在後續步驟中,遵循此基準的範例會顯示如何擴充現有的 FIDL 通訊協定,這是相當常見的做法。

列舉

FIDL 方案:Enum

「列舉」是一種 FIDL 資料類型,代表固定的可能常數清單,例如撲克牌上的西裝,或使用者可從下拉式選單中選取的汽車廠牌。接著,此清單值會對應到基礎整數類型,且每個值都對應其中一個列出的成員。

在下方範例中,系統會在以下情境中加入 FIDL 列舉:列舉可能是由失敗方法呼叫造成的可能錯誤值。ReadError 列舉包含兩個成員:NOT_FOUND 可用來表示在讀取嘗試期間無法比對搜尋值,而 UNKNOWN 可為所有無法明確說明的情況視為抓取錯誤。請注意,這個列舉已標示為 flexible,方便日後與新成員一起進化。

推理

原始的唯寫鍵/值存放區現已擴充,可讀取商店外的項目。

實作

適用於 FIDL 和 CML 定義的變更如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.addreaditem;

// Aliases for the key and value. Using aliases helps increase the readability of FIDL files and
// reduces likelihood of errors due to differing constraints.
alias Key = string:128;
alias Value = vector<byte>:64000;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key Key;
    value Value;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// An enumeration of things that may go wrong when trying to read a value out of our store.
type ReadError = flexible enum {
    UNKNOWN = 0;
    NOT_FOUND = 1;
};

/// A very basic key-value store - so basic, in fact, that one may only write to it, never read!
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;

    /// Reads an item from the store.
    flexible ReadItem(struct {
        key Key;
    }) -> (Item) error ReadError;
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.addreaditem.Store" },
    ],
    config: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        read_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.addreaditem.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.addreaditem.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.addreaditem.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

所有語言的用戶端和伺服器實作也有所變更:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fidl_examples_keyvaluestore_addreaditem::{Item, StoreMarker},
    fuchsia_component::client::connect_to_protocol,
    std::{str, thread, time},
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        match store.write_item(&Item { key: key, value: value.into_bytes() }).await? {
            Ok(_) => println!("WriteItem Success"),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // The structured config for this client contains `read_items`, a vector of strings, each of
    // which is meant to be read from the key-value store. We iterate over these keys, attempting to
    // read them in turn.
    for key in config.read_items.into_iter() {
        let res = store.read_item(key.as_str()).await;
        match res.unwrap() {
            Ok(val) => {
                println!("ReadItem Success: key: {}, value: {}", key, str::from_utf8(&val.1)?)
            }
            Err(err) => println!("ReadItem Error: {}", err.into_primitive()),
        }
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fidl_examples_keyvaluestore_addreaditem::{
        Item, ReadError, StoreRequest, StoreRequestStream, WriteError,
    },
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
    std::cell::RefCell,
    std::collections::hash_map::Entry,
    std::collections::HashMap,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z][A-Za-z0-9_\./]{2,62}[A-Za-z0-9]$")
            .expect("Key validation regex failed to compile");
}

/// Handler for the `WriteItem` method.
fn write_item(store: &mut HashMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Validate the value.
    if attempt.value.is_empty() {
        println!("Write error: INVALID_VALUE, For key: {}", attempt.key);
        return Err(WriteError::InvalidValue);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote value at key: {}", entry.key());
            entry.insert(attempt.value);
            Ok(())
        }
    }
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    let store = RefCell::new(HashMap::<String, Vec<u8>>::new());

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.borrow_mut(), attempt))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::ReadItem { key, responder } => {
                    println!("ReadItem request received");

                    // Read the item from the store, returning the appropriate error if it could not be found.
                    responder
                        .send(match store.borrow().get(&key) {
                            Some(found) => {
                                println!("Read value at key: {}", key);
                                Ok((&key, found))
                            }
                            None => {
                                println!("Read error: NOT_FOUND, For key: {}", key);
                                Err(ReadError::NotFound)
                            }
                        })
                        .context("error sending reply")?;
                    println!("ReadItem response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

動態饋給轉送模式

FIDL 方案:動態饋給前向模式

在 FIDL 通訊協定中,使用雙向方法搭配空白回應做為流量控制的通訊協定 (例如 DoSomething(...) -> ();) 有一項基本缺點,這對於易受延遲影響的通訊協定而言是一大缺點:呼叫端會等待每個回覆再傳送下一則訊息,因此會增加每則訊息的延遲時間,或是忽略訊息,直接轉譯空回覆本身,不顯示空回覆。如果通訊協定希望控制流量控製而不產生延遲時間,建議您使用動態饋給轉送模式。在此設定中,一或多種單向方法會將資料寫入伺服器,而其他方法 (單向或雙向) 則是用來「修訂」工作,並在用戶端與伺服器之間同步處理。這表示任意數量的資料可在傳送時盡快轉移,但仍然會有一些流量控制,這是因為同步處理方法會強制用戶端在執行更多工作之前停止。

改善 Instance 通訊協定效能的方法之一,就是允許批次處理:與其每次在畫布中加入新的 AddLine(...);,在等候回覆,並在下一行中再次執行該行時,只要將多行資料批次處理成單一叫用來發出新的 AddLines(...); 呼叫即可。用戶端現在可以決定如何妥善區隔大量線條。

在一般情況下,我們會發現在伺服器和用戶端完全未同步的情況下:用戶端可以使用無限制的 AddLines(...); 呼叫在伺服器中淹水,而伺服器也可能以類似的方式,以比伺服器可處理的更多 -> OnDrawn(...); 事件大量地淹沒用戶端。這兩個問題的解決方法是新增簡單的 Ready() -> (); 方法,以便同步處理。每當用戶端準備好接收下一個繪圖更新時,就會呼叫此方法,從伺服器的回應中表示用戶端可以繼續處理更多要求。

我們現在對雙向的流量控製做了一些控制。通訊協定現在會導入動態饋給轉送模式,以便在某些同步處理「修訂」呼叫之前,允許許多未控制的呼叫,進而觸發伺服器上的實際工作。這樣可避免用戶端對伺服器造成過多工作負荷。同樣地,伺服器也無法再傳送未受限的 -> OnDrawn(...); 事件:每個事件都必須遵循來自用戶端的信號 (Ready() -> (); 呼叫),表示它已準備好執行更多工作。這就是所謂的節流事件模式

具體的實作必須手動套用以下部分規則:如果用戶端收到未透過 Ready() -> (); 方法要求的 -> OnDrawn(...); 事件,就必須關閉連線。

FIDL、CML 和領域介面的定義如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.canvas.clientrequesteddraw;

/// A point in 2D space.
type Point = struct {
    x int64;
    y int64;
};

/// A line in 2D space.
alias Line = array<Point, 2>;

/// A bounding box in 2D space. This is the result of "drawing" operations on our canvas, and what
/// the server reports back to the client. These bounds are sufficient to contain all of the
/// lines (inclusive) on a canvas at a given time.
type BoundingBox = struct {
    top_left Point;
    bottom_right Point;
};

/// Manages a single instance of a canvas. Each session of this protocol is responsible for a new
/// canvas.
@discoverable
open protocol Instance {
    /// Add multiple lines to the canvas. We are able to reduce protocol chatter and the number of
    /// requests needed by batching instead of calling the simpler `AddLine(...)` one line at a
    /// time.
    flexible AddLines(struct {
        lines vector<Line>;
    });

    /// Rather than the server randomly performing draws, or trying to guess when to do so, the
    /// client must explicitly ask for them. This creates a bit of extra chatter with the additional
    /// method invocation, but allows much greater client-side control of when the canvas is "ready"
    /// for a view update, thereby eliminating unnecessary draws.
    ///
    /// This method also has the benefit of "throttling" the `-> OnDrawn(...)` event - rather than
    /// allowing a potentially unlimited flood of `-> OnDrawn(...)` calls, we now have the runtime
    /// enforced semantic that each `-> OnDrawn(...)` call must follow a unique `Ready() -> ()` call
    /// from the client. An unprompted `-> OnDrawn(...)` is invalid, and should cause the channel to
    /// immediately close.
    flexible Ready() -> ();

    /// Update the client with the latest drawing state. The server makes no guarantees about how
    /// often this event occurs - it could occur multiple times per board state, for example.
    flexible -> OnDrawn(BoundingBox);
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.canvas.clientrequesteddraw.Instance" },
    ],
    config: {
        // A script for the client to follow. Entries in the script may take one of two forms: a
        // pair of signed-integer coordinates like "-2,15:4,5", or the string "READY". The former
        // builds a local vector sent via a single `AddLines(...)` call, while the latter sends a
        // `Ready() -> ()` call pauses execution until the next `->OnDrawn(...)` event is received.
        //
        // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great
        //   deal of string parsing if we were able to use a vector of `union { Point; Ready}` here.
        script: {
            type: "vector",
            max_count: 100,
            element: {
                type: "string",
                max_size: 64,
            },
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.canvas.clientrequesteddraw.Instance" },
    ],
    expose: [
        {
            protocol: "examples.canvas.clientrequesteddraw.Instance",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.canvas.clientrequesteddraw.Instance",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{format_err, Context as _, Error},
    config::Config,
    fidl_examples_canvas_clientrequesteddraw::{InstanceEvent, InstanceMarker, Point},
    fuchsia_component::client::connect_to_protocol,
    futures::TryStreamExt,
    std::{thread, time},
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send Instance requests
    // across the channel.
    let instance = connect_to_protocol::<InstanceMarker>()?;
    println!("Outgoing connection enabled");

    let mut batched_lines = Vec::<[Point; 2]>::new();
    for action in config.script.into_iter() {
        // If the next action in the script is to "PUSH", send a batch of lines to the server.
        if action == "PUSH" {
            instance.add_lines(&batched_lines).context("Could not send lines")?;
            println!("AddLines request sent");
            batched_lines.clear();
            continue;
        }

        // If the next action in the script is to "WAIT", block until an OnDrawn event is received
        // from the server.
        if action == "WAIT" {
            let mut event_stream = instance.take_event_stream();
            loop {
                match event_stream
                    .try_next()
                    .await
                    .context("Error getting event response from proxy")?
                    .ok_or_else(|| format_err!("Proxy sent no events"))?
                {
                    InstanceEvent::OnDrawn { top_left, bottom_right } => {
                        println!(
                            "OnDrawn event received: top_left: {:?}, bottom_right: {:?}",
                            top_left, bottom_right
                        );
                        break;
                    }
                    InstanceEvent::_UnknownEvent { ordinal, .. } => {
                        println!("Received an unknown event with ordinal {ordinal}");
                    }
                }
            }

            // Now, inform the server that we are ready to receive more updates whenever they are
            // ready for us.
            println!("Ready request sent");
            instance.ready().await.context("Could not send ready call")?;
            println!("Ready success");
            continue;
        }

        // Add a line to the next batch. Parse the string input, making two points out of it.
        let mut points = action
            .split(":")
            .map(|point| {
                let integers = point
                    .split(",")
                    .map(|integer| integer.parse::<i64>().unwrap())
                    .collect::<Vec<i64>>();
                Point { x: integers[0], y: integers[1] }
            })
            .collect::<Vec<Point>>();

        // Assemble a line from the two points.
        let from = points.pop().ok_or(format_err!("line requires 2 points, but has 0"))?;
        let to = points.pop().ok_or(format_err!("line requires 2 points, but has 1"))?;
        let mut line: [Point; 2] = [from, to];

        // Batch a line for drawing to the canvas using the two points provided.
        println!("AddLines batching line: {:?}", &mut line);
        batched_lines.push(line);
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{anyhow, Context as _, Error},
    fidl::endpoints::RequestStream as _,
    fidl_examples_canvas_clientrequesteddraw::{
        BoundingBox, InstanceRequest, InstanceRequestStream, Point,
    },
    fuchsia_async::{Time, Timer},
    fuchsia_component::server::ServiceFs,
    fuchsia_zircon::{self as zx},
    futures::future::join,
    futures::prelude::*,
    std::sync::{Arc, Mutex},
};

// A struct that stores the two things we care about for this example: the bounding box the lines
// that have been added thus far, and bit to track whether or not there have been changes since the
// last `OnDrawn` event.
#[derive(Debug)]
struct CanvasState {
    // Tracks whether there has been a change since the last send, to prevent redundant updates.
    changed: bool,
    // Tracks whether or not the client has declared itself ready to receive more updated.
    ready: bool,
    bounding_box: BoundingBox,
}

/// Handler for the `AddLines` method.
fn add_lines(state: &mut CanvasState, lines: Vec<[Point; 2]>) {
    // Update the bounding box to account for the new lines we've just "added" to the canvas.
    let bounds = &mut state.bounding_box;
    for line in lines {
        println!("AddLines printing line: {:?}", line);
        for point in line {
            if point.x < bounds.top_left.x {
                bounds.top_left.x = point.x;
            }
            if point.y > bounds.top_left.y {
                bounds.top_left.y = point.y;
            }
            if point.x > bounds.bottom_right.x {
                bounds.bottom_right.x = point.x;
            }
            if point.y < bounds.bottom_right.y {
                bounds.bottom_right.y = point.y;
            }
        }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next tick.
    state.changed = true
}

/// Creates a new instance of the server, paired to a single client across a zircon channel.
async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> {
    // Create a new in-memory state store for the state of the canvas. The store will live for the
    // lifetime of the connection between the server and this particular client.
    let state = Arc::new(Mutex::new(CanvasState {
        changed: true,
        ready: true,
        bounding_box: BoundingBox {
            top_left: Point { x: 0, y: 0 },
            bottom_right: Point { x: 0, y: 0 },
        },
    }));

    // Take ownership of the control_handle from the stream, which will allow us to push events from
    // a different async task.
    let control_handle = stream.control_handle();

    // A separate watcher task periodically "draws" the canvas, and notifies the client of the new
    // state. We'll need a cloned reference to the canvas state to be accessible from the new
    // task.
    let state_ref = state.clone();
    let update_sender = || async move {
        loop {
            // Our server sends one update per second, but only if the client has declared that it
            // is ready to receive one.
            Timer::new(Time::after(zx::Duration::from_seconds(1))).await;
            let mut state = state_ref.lock().unwrap();
            if !state.changed || !state.ready {
                continue;
            }

            // After acquiring the lock, this is where we would draw the actual lines. Since this is
            // just an example, we'll avoid doing the actual rendering, and simply send the bounding
            // box to the client instead.
            let bounds = state.bounding_box;
            match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) {
                Ok(_) => println!(
                    "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}",
                    bounds.top_left, bounds.bottom_right
                ),
                Err(_) => return,
            }

            // Reset the change and ready trackers.
            state.ready = false;
            state.changed = false;
        }
    };

    // Handle requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    let state_ref = &state;
    let request_handler =
        stream.map(|result| result.context("failed request")).try_for_each(|request| async move {
            // Match based on the method being invoked.
            match request {
                InstanceRequest::AddLines { lines, .. } => {
                    println!("AddLines request received");
                    add_lines(&mut state_ref.lock().unwrap(), lines);
                }
                InstanceRequest::Ready { responder, .. } => {
                    println!("Ready request received");
                    // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();`
                    // event; if two "consecutive" `Ready() -> ();` calls are received, this
                    // interaction has entered an invalid state, and should be aborted immediately.
                    let mut state = state_ref.lock().unwrap();
                    if state.ready == true {
                        return Err(anyhow!("Invalid back-to-back `Ready` requests received"));
                    }

                    state.ready = true;
                    responder.send().context("Error responding")?;
                } //
                InstanceRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        });

    // This line will only be reached if the server errors out. The stream will await indefinitely,
    // thereby creating a long-lived server. Here, we first wait for the updater task to realize the
    // connection has died, then bubble up the error.
    join(request_handler, update_sender()).await.0
}

// A helper enum that allows us to treat a `Instance` service instance as a value.
enum IncomingService {
    Instance(InstanceRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Instance` protocol - this will allow the client to see
    // the server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Instance);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/fidl/new/canvas/client_requested_draw/cpp_natural/client/config.h>

// The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming
// events asynchronously.
class EventHandler : public fidl::AsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> {
 public:
  // Handler for |OnDrawn| events sent from the server.
  void OnDrawn(
      fidl::Event<examples_canvas_clientrequesteddraw::Instance::OnDrawn>& event) override {
    ::examples_canvas_clientrequesteddraw::Point top_left = event.top_left();
    ::examples_canvas_clientrequesteddraw::Point bottom_right = event.bottom_right();
    FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x()
                  << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x()
                  << ", y: " << bottom_right.y() << " }";
    loop_.Quit();
  }

  void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; }

  void handle_unknown_event(
      fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal;
  }

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

 private:
  async::Loop& loop_;
};

// A helper function that takes a coordinate in string form, like "123,-456", and parses it into a
// a struct of the form |{ in64 x; int64 y; }|.
::examples_canvas_clientrequesteddraw::Point ParsePoint(std::string_view input) {
  int64_t x = 0;
  int64_t y = 0;
  size_t index = input.find(',');
  if (index != std::string::npos) {
    std::from_chars(input.data(), input.data() + index, x);
    std::from_chars(input.data() + index + 1, input.data() + input.length(), y);
  }
  return ::examples_canvas_clientrequesteddraw::Point(x, y);
}

using Line = ::std::array<::examples_canvas_clientrequesteddraw::Point, 2>;

// A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it
// into an array of 2 |Point| structs.
Line ParseLine(const std::string& action) {
  auto input = std::string_view(action);
  size_t index = input.find(':');
  if (index != std::string::npos) {
    return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))};
  }
  return {};
}

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop and dispatcher.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the 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<examples_canvas_clientrequesteddraw::Instance>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // Create an instance of the event handler.
  EventHandler event_handler(loop);

  // Create an asynchronous client using the newly-established connection.
  fidl::Client client(std::move(*client_end), dispatcher, &event_handler);
  FX_LOGS(INFO) << "Outgoing connection enabled";

  std::vector<Line> batched_lines;
  for (const auto& action : conf.script()) {
    // If the next action in the script is to "PUSH", send a batch of lines to the server.
    if (action == "PUSH") {
      fit::result<fidl::Error> result = client->AddLines(batched_lines);
      if (!result.is_ok()) {
        // Check that our one-way call was enqueued successfully, and handle the error
        // appropriately. In the case of this example, there is nothing we can do to recover here,
        // except to log an error and exit the program.
        FX_LOGS(ERROR) << "Could not send AddLines request: " << result.error_value();
        return -1;
      }

      batched_lines.clear();
      FX_LOGS(INFO) << "AddLines request sent";
      continue;
    }

    // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received
    // from the server.
    if (action == "WAIT") {
      loop.Run();
      loop.ResetQuit();

      // Now, inform the server that we are ready to receive more updates whenever they are
      // ready for us.
      FX_LOGS(INFO) << "Ready request sent";
      client->Ready().ThenExactlyOnce(
          [&](fidl::Result<examples_canvas_clientrequesteddraw::Instance::Ready> result) {
            // Check if the FIDL call succeeded or not.
            if (result.is_ok()) {
              FX_LOGS(INFO) << "Ready success";
            } else {
              FX_LOGS(ERROR) << "Could not send Ready request: " << result.error_value();
            }

            // Quit the loop, thereby handing control back to the outer loop of actions being
            // iterated over.
            loop.Quit();
          });

      // Run the loop until the callback is resolved, at which point we can continue from here.
      loop.Run();
      loop.ResetQuit();

      continue;
    }

    // Batch a line for drawing to the canvas using the two points provided.
    Line line = ParseLine(action);
    batched_lines.push_back(line);
    FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x() << ", y: " << line[1].y()
                  << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]";
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <src/lib/fxl/macros.h>
#include <src/lib/fxl/memory/weak_ptr.h>

// A struct that stores the two things we care about for this example: the set of lines, and the
// bounding box that contains them.
struct CanvasState {
  // Tracks whether there has been a change since the last send, to prevent redundant updates.
  bool changed = true;
  // Tracks whether or not the client has declared itself ready to receive more updated.
  bool ready = true;
  examples_canvas_clientrequesteddraw::BoundingBox bounding_box;
};

// An implementation of the |Instance| protocol.
class InstanceImpl final : public fidl::Server<examples_canvas_clientrequesteddraw::Instance> {
 public:
  // Bind this implementation to a channel.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end)
      : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)),
        weak_factory_(this) {
    // Start the update timer on startup. Our server sends one update per second
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void OnFidlClosed(fidl::UnbindInfo info) {
    if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) {
      FX_LOGS(ERROR) << "Shutdown unexpectedly";
    }
    delete this;
  }

  void AddLines(AddLinesRequest& request, AddLinesCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "AddLines request received";
    for (const auto& points : request.lines()) {
      FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x()
                    << ", y: " << points[1].y() << " }, Point { x: " << points[0].x()
                    << ", y: " << points[0].y() << " }]";

      // Update the bounding box to account for the new line we've just "added" to the canvas.
      auto& bounds = state_.bounding_box;
      for (const auto& point : points) {
        if (point.x() < bounds.top_left().x()) {
          bounds.top_left().x() = point.x();
        }
        if (point.y() > bounds.top_left().y()) {
          bounds.top_left().y() = point.y();
        }
        if (point.x() > bounds.bottom_right().x()) {
          bounds.bottom_right().x() = point.x();
        }
        if (point.y() < bounds.bottom_right().y()) {
          bounds.bottom_right().y() = point.y();
        }
      }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn|
    // event.
    state_.changed = true;
  }

  void Ready(ReadyCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "Ready request received";

    // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two
    // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid
    // state, and should be aborted immediately.
    if (state_.ready == true) {
      FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received";
    }

    state_.ready = true;
    completer.Reply();
  }

  void handle_unknown_method(
      fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata,
      fidl::UnknownMethodCompleter::Sync& completer) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal;
  }

 private:
  // Each scheduled update waits for the allotted amount of time, sends an update if something has
  // changed, and schedules the next update.
  void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) {
    async::PostDelayedTask(
        dispatcher,
        [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] {
          // Halt execution if the binding has been deallocated already.
          if (!weak) {
            return;
          }

          // Schedule the next update if the binding still exists.
          weak->ScheduleOnDrawnEvent(dispatcher, after);

          // No need to send an update if nothing has changed since the last one, or the client has
          // not yet informed us that it is ready for more updates.
          if (!weak->state_.changed || !weak->state_.ready) {
            return;
          }

          // This is where we would draw the actual lines. Since this is just an example, we'll
          // avoid doing the actual rendering, and simply send the bounding box to the client
          // instead.
          auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box);
          if (!result.is_ok()) {
            return;
          }

          auto top_left = state_.bounding_box.top_left();
          auto bottom_right = state_.bounding_box.bottom_right();
          FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x()
                        << ", y: " << top_left.y()
                        << " }, bottom_right: Point { x: " << bottom_right.x()
                        << ", y: " << bottom_right.y() << " }";

          // Reset the change and ready trackers.
          state_.ready = false;
          state_.changed = false;
        },
        after);
  }

  fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_;
  CanvasState state_ = CanvasState{};

  // Generates weak references to this object, which are appropriate to pass into asynchronous
  // callbacks that need to access this object. The references are automatically invalidated
  // if this object is destroyed.
  fxl::WeakPtrFactory<InstanceImpl> weak_factory_;
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This
  // directory is where the outgoing FIDL protocols are installed so that they can be provided to
  // other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle.
  // The startup handle is a handle provided to every component by the system, so that they can
  // serve capabilities (e.g. FIDL protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to
  // |examples.canvas.clientrequesteddraw.Instance|.
  result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>(
      [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) {
        // Create an instance of our InstanceImpl that destroys itself when the connection closes.
        new InstanceImpl(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string();
    return -1;
  }

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

C++ (有線)

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/fidl/new/canvas/client_requested_draw/cpp_wire/client/config.h>

// The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming
// events asynchronously.
class EventHandler
    : public fidl::WireAsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> {
 public:
  // Handler for |OnDrawn| events sent from the server.
  void OnDrawn(
      fidl::WireEvent<examples_canvas_clientrequesteddraw::Instance::OnDrawn>* event) override {
    ::examples_canvas_clientrequesteddraw::wire::Point top_left = event->top_left;
    ::examples_canvas_clientrequesteddraw::wire::Point bottom_right = event->bottom_right;
    FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x
                  << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x
                  << ", y: " << bottom_right.y << " }";
    loop_.Quit();
  }

  void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; }

  void handle_unknown_event(
      fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal;
  }

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

 private:
  async::Loop& loop_;
};

// A helper function that takes a coordinate in string form, like "123,-456", and parses it into a
// a struct of the form |{ in64 x; int64 y; }|.
::examples_canvas_clientrequesteddraw::wire::Point ParsePoint(std::string_view input) {
  int64_t x = 0;
  int64_t y = 0;
  size_t index = input.find(',');
  if (index != std::string::npos) {
    std::from_chars(input.data(), input.data() + index, x);
    std::from_chars(input.data() + index + 1, input.data() + input.length(), y);
  }
  return ::examples_canvas_clientrequesteddraw::wire::Point{.x = x, .y = y};
}

using Line = ::fidl::Array<::examples_canvas_clientrequesteddraw::wire::Point, 2>;

// A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it
// into an array of 2 |Point| structs.
Line ParseLine(const std::string& action) {
  auto input = std::string_view(action);
  size_t index = input.find(':');
  if (index != std::string::npos) {
    return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))};
  }
  return {};
}

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop and dispatcher.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the 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<examples_canvas_clientrequesteddraw::Instance>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // Create an instance of the event handler.
  EventHandler event_handler(loop);

  // Create an asynchronous client using the newly-established connection.
  fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler);
  FX_LOGS(INFO) << "Outgoing connection enabled";

  std::vector<Line> batched_lines;
  for (const auto& action : conf.script()) {
    // If the next action in the script is to "PUSH", send a batch of lines to the server.
    if (action == "PUSH") {
      fidl::Status status = client->AddLines(fidl::VectorView<Line>::FromExternal(batched_lines));
      if (!status.ok()) {
        // Check that our one-way call was enqueued successfully, and handle the error
        // appropriately. In the case of this example, there is nothing we can do to recover here,
        // except to log an error and exit the program.
        FX_LOGS(ERROR) << "Could not send AddLines request: " << status.error();
        return -1;
      }

      batched_lines.clear();
      FX_LOGS(INFO) << "AddLines request sent";
      continue;
    }

    // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received
    // from the server.
    if (action == "WAIT") {
      loop.Run();
      loop.ResetQuit();

      // Now, inform the server that we are ready to receive more updates whenever they are
      // ready for us.
      FX_LOGS(INFO) << "Ready request sent";
      client->Ready().ThenExactlyOnce(
          [&](fidl::WireUnownedResult<examples_canvas_clientrequesteddraw::Instance::Ready>&
                  result) {
            // Check if the FIDL call succeeded or not.
            if (result.ok()) {
              FX_LOGS(INFO) << "Ready success";
            } else {
              FX_LOGS(ERROR) << "Could not send Ready request: " << result.error();
            }

            // Quit the loop, thereby handing control back to the outer loop of actions being
            // iterated over.
            loop.Quit();
          });

      // Run the loop until the callback is resolved, at which point we can continue from here.
      loop.Run();
      loop.ResetQuit();

      continue;
    }

    // Batch a line for drawing to the canvas using the two points provided.
    Line line = ParseLine(action);
    batched_lines.push_back(line);
    FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x << ", y: " << line[1].y
                  << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]";
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <src/lib/fxl/macros.h>
#include <src/lib/fxl/memory/weak_ptr.h>

// A struct that stores the two things we care about for this example: the set of lines, and the
// bounding box that contains them.
struct CanvasState {
  // Tracks whether there has been a change since the last send, to prevent redundant updates.
  bool changed = true;
  // Tracks whether or not the client has declared itself ready to receive more updated.
  bool ready = true;
  examples_canvas_clientrequesteddraw::wire::BoundingBox bounding_box;
};

// An implementation of the |Instance| protocol.
class InstanceImpl final : public fidl::WireServer<examples_canvas_clientrequesteddraw::Instance> {
 public:
  // Bind this implementation to a channel.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end)
      : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)),
        weak_factory_(this) {
    // Start the update timer on startup. Our server sends one update per second
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void OnFidlClosed(fidl::UnbindInfo info) {
    if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) {
      FX_LOGS(ERROR) << "Shutdown unexpectedly";
    }
    delete this;
  }

  void AddLines(AddLinesRequestView request, AddLinesCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "AddLines request received";
    for (const auto& points : request->lines) {
      FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x
                    << ", y: " << points[1].y << " }, Point { x: " << points[0].x
                    << ", y: " << points[0].y << " }]";

      // Update the bounding box to account for the new line we've just "added" to the canvas.
      auto& bounds = state_.bounding_box;
      for (const auto& point : points) {
        if (point.x < bounds.top_left.x) {
          bounds.top_left.x = point.x;
        }
        if (point.y > bounds.top_left.y) {
          bounds.top_left.y = point.y;
        }
        if (point.x > bounds.bottom_right.x) {
          bounds.bottom_right.x = point.x;
        }
        if (point.y < bounds.bottom_right.y) {
          bounds.bottom_right.y = point.y;
        }
      }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn|
    // event.
    state_.changed = true;
  }

  void Ready(ReadyCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "Ready request received";

    // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two
    // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid
    // state, and should be aborted immediately.
    if (state_.ready == true) {
      FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received";
    }

    state_.ready = true;
    completer.Reply();
  }

  void handle_unknown_method(
      fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata,
      fidl::UnknownMethodCompleter::Sync& completer) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal;
  }

 private:
  // Each scheduled update waits for the allotted amount of time, sends an update if something has
  // changed, and schedules the next update.
  void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) {
    async::PostDelayedTask(
        dispatcher,
        [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] {
          // Halt execution if the binding has been deallocated already.
          if (!weak) {
            return;
          }

          // Schedule the next update if the binding still exists.
          weak->ScheduleOnDrawnEvent(dispatcher, after);

          // No need to send an update if nothing has changed since the last one, or the client has
          // not yet informed us that it is ready for more updates.
          if (!weak->state_.changed || !weak->state_.ready) {
            return;
          }

          // This is where we would draw the actual lines. Since this is just an example, we'll
          // avoid doing the actual rendering, and simply send the bounding box to the client
          // instead.
          auto top_left = weak->state_.bounding_box.top_left;
          auto bottom_right = weak->state_.bounding_box.bottom_right;
          fidl::Status status =
              fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right);
          if (!status.ok()) {
            return;
          }
          FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x
                        << ", y: " << top_left.y
                        << " }, bottom_right: Point { x: " << bottom_right.x
                        << ", y: " << bottom_right.y << " }";

          // Reset the change and ready trackers.
          state_.ready = false;
          weak->state_.changed = false;
        },
        after);
  }

  fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_;
  CanvasState state_ = CanvasState{};

  // Generates weak references to this object, which are appropriate to pass into asynchronous
  // callbacks that need to access this object. The references are automatically invalidated
  // if this object is destroyed.
  fxl::WeakPtrFactory<InstanceImpl> weak_factory_;
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This
  // directory is where the outgoing FIDL protocols are installed so that they can be provided to
  // other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle.
  // The startup handle is a handle provided to every component by the system, so that they can
  // serve capabilities (e.g. FIDL protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to
  // |examples.canvas.clientrequesteddraw.Instance|.
  result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>(
      [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) {
        // Create an instance of our InstanceImpl that destroys itself when the connection closes.
        new InstanceImpl(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string();
    return -1;
  }

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

HLCPP

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <lib/async-loop/cpp/loop.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/canvas/clientrequesteddraw/cpp/fidl.h>
#include <examples/fidl/new/canvas/client_requested_draw/hlcpp/client/config.h>

// A helper function that takes a coordinate in string form, like "123,-456", and parses it into a
// a struct of the form |{ in64 x; int64 y; }|.
::examples::canvas::clientrequesteddraw::Point ParsePoint(std::string_view input) {
  int64_t x = 0;
  int64_t y = 0;
  size_t index = input.find(',');
  if (index != std::string::npos) {
    std::from_chars(input.data(), input.data() + index, x);
    std::from_chars(input.data() + index + 1, input.data() + input.length(), y);
  }
  return ::examples::canvas::clientrequesteddraw::Point{.x = x, .y = y};
}

using Line = ::std::array<::examples::canvas::clientrequesteddraw::Point, 2>;

// A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it
// into an array of 2 |Point| structs.
Line ParseLine(const std::string& action) {
  auto input = std::string_view(action);
  size_t index = input.find(':');
  if (index != std::string::npos) {
    return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))};
  }
  return {};
}

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the protocol inside the component's namespace, then create an asynchronous client
  // using the newly-established connection.
  examples::canvas::clientrequesteddraw::InstancePtr instance_proxy;
  auto context = sys::ComponentContext::Create();
  context->svc()->Connect(instance_proxy.NewRequest(dispatcher));
  FX_LOGS(INFO) << "Outgoing connection enabled";

  instance_proxy.set_error_handler([&loop](zx_status_t status) {
    FX_LOGS(ERROR) << "Shutdown unexpectedly";
    loop.Quit();
  });

  // Provide a lambda to handle incoming |OnDrawn| events asynchronously.
  instance_proxy.events().OnDrawn =
      [&loop](::examples::canvas::clientrequesteddraw::Point top_left,
              ::examples::canvas::clientrequesteddraw::Point bottom_right) {
        FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x
                      << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x
                      << ", y: " << bottom_right.y << " }";
        loop.Quit();
      };

  instance_proxy.events().handle_unknown_event = [](uint64_t ordinal) {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << ordinal;
  };

  std::vector<Line> batched_lines;
  for (const auto& action : conf.script()) {
    // If the next action in the script is to "PUSH", send a batch of lines to the server.
    if (action == "PUSH") {
      instance_proxy->AddLines(batched_lines);
      batched_lines.clear();
      FX_LOGS(INFO) << "AddLines request sent";
      continue;
    }

    // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received
    // from the server.
    if (action == "WAIT") {
      loop.Run();
      loop.ResetQuit();

      // Now, inform the server that we are ready to receive more updates whenever they are ready
      // for us.
      FX_LOGS(INFO) << "Ready request sent";
      instance_proxy->Ready([&](fpromise::result<void, fidl::FrameworkErr> result) {
        if (result.is_error()) {
          // Check that our flexible two-way call was known to the server and handle the case of an
          // unknown method appropriately. In the case of this example, there is nothing we can do
          // to recover here, except to log an error and exit the program.
          FX_LOGS(ERROR) << "Server does not implement AddLine";
        }

        FX_LOGS(INFO) << "Ready success";

        // Quit the loop, thereby handing control back to the outer loop of actions being iterated
        // over.
        loop.Quit();
      });

      // Run the loop until the callback is resolved, at which point we can continue from here.
      loop.Run();
      loop.ResetQuit();

      continue;
    }

    // Batch a line for drawing to the canvas using the two points provided.
    Line line = ParseLine(action);
    batched_lines.push_back(line);
    FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x << ", y: " << line[1].y
                  << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]";
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async/cpp/task.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <examples/canvas/clientrequesteddraw/cpp/fidl.h>
#include <src/lib/fxl/macros.h>
#include <src/lib/fxl/memory/weak_ptr.h>

// A struct that stores the two things we care about for this example: the set of lines, and the
// bounding box that contains them.
struct CanvasState {
  // Tracks whether there has been a change since the last send, to prevent redundant updates.
  bool changed = true;
  // Tracks whether or not the client has declared itself ready to receive more updated.
  bool ready = true;
  examples::canvas::clientrequesteddraw::BoundingBox bounding_box;
};

using Line = ::std::array<::examples::canvas::clientrequesteddraw::Point, 2>;

// An implementation of the |Instance| protocol.
class InstanceImpl final : public examples::canvas::clientrequesteddraw::Instance {
 public:
  // Bind this implementation to an |InterfaceRequest|.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::InterfaceRequest<examples::canvas::clientrequesteddraw::Instance> request)
      : binding_(fidl::Binding<examples::canvas::clientrequesteddraw::Instance>(this)),
        weak_factory_(this) {
    binding_.Bind(std::move(request), dispatcher);

    // Gracefully handle abrupt shutdowns.
    binding_.set_error_handler([this](zx_status_t status) mutable {
      if (status != ZX_ERR_PEER_CLOSED) {
        FX_LOGS(ERROR) << "Shutdown unexpectedly";
      }
      delete this;
    });

    // Start the update timer on startup. Our server sends one update per second.
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void AddLines(std::vector<Line> lines) override {
    FX_LOGS(INFO) << "AddLines request received";
    for (const auto& points : lines) {
      FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x
                    << ", y: " << points[1].y << " }, Point { x: " << points[0].x
                    << ", y: " << points[0].y << " }]";

      // Update the bounding box to account for the new line we've just "added" to the canvas.
      auto& bounds = state_.bounding_box;
      for (const auto& point : points) {
        if (point.x < bounds.top_left.x) {
          bounds.top_left.x = point.x;
        }
        if (point.y > bounds.top_left.y) {
          bounds.top_left.y = point.y;
        }
        if (point.x > bounds.bottom_right.x) {
          bounds.bottom_right.x = point.x;
        }
        if (point.y < bounds.bottom_right.y) {
          bounds.bottom_right.y = point.y;
        }
      }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next
    // |OnDrawn| event.
    state_.changed = true;
  }

  void Ready(ReadyCallback callback) override {
    FX_LOGS(INFO) << "Ready request received";

    // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if
    // two "consecutive" `Ready() -> ();` calls are received, this interaction has entered an
    // invalid state, and should be aborted immediately.
    if (state_.ready == true) {
      FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received";
    }

    state_.ready = true;
    callback(fpromise::ok());
  }

  void handle_unknown_method(uint64_t ordinal, bool method_has_response) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << ordinal;
  }

 private:
  // Each scheduled update waits for the allotted amount of time, sends an update if something
  // has changed, and schedules the next update.
  void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) {
    async::PostDelayedTask(
        dispatcher,
        [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] {
          // Halt execution if the binding has been deallocated already.
          if (!weak) {
            return;
          }

          // Schedule the next update if the binding still exists.
          weak->ScheduleOnDrawnEvent(dispatcher, after);

          // No need to send an update if nothing has changed since the last one, or the client
          // has not yet informed us that it is ready for more updates.
          if (!weak->state_.changed || !weak->state_.ready) {
            return;
          }

          // This is where we would draw the actual lines. Since this is just an example, we'll
          // avoid doing the actual rendering, and simply send the bounding box to the client
          // instead.
          auto top_left = state_.bounding_box.top_left;
          auto bottom_right = state_.bounding_box.bottom_right;
          binding_.events().OnDrawn(top_left, bottom_right);
          FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x
                        << ", y: " << top_left.y
                        << " }, bottom_right: Point { x: " << bottom_right.x
                        << ", y: " << bottom_right.y << " }";

          // Reset the change and ready trackers.
          state_.ready = false;
          state_.changed = false;
        },
        after);
  }

  fidl::Binding<examples::canvas::clientrequesteddraw::Instance> binding_;
  CanvasState state_ = CanvasState{};

  // Generates weak references to this object, which are appropriate to pass into asynchronous
  // callbacks that need to access this object. The references are automatically invalidated
  // if this object is destroyed.
  fxl::WeakPtrFactory<InstanceImpl> weak_factory_;
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from
  // the client. The following initializes the loop, and obtains the dispatcher, which will be
  // used when binding the server implementation to a channel.
  //
  // Note that unlike the new C++ bindings, HLCPP bindings rely on the async loop being attached
  // to the current thread via the |kAsyncLoopConfigAttachToCurrentThread| configuration.
  async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component.
  // This directory is where the outgoing FIDL protocols are installed so that they can be
  // provided to other components.
  auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory();

  // Register a handler for components trying to connect to
  // |examples.canvas.clientrequesteddraw.Instance|.
  context->outgoing()->AddPublicService(
      fidl::InterfaceRequestHandler<examples::canvas::clientrequesteddraw::Instance>(
          [dispatcher](
              fidl::InterfaceRequest<examples::canvas::clientrequesteddraw::Instance> request) {
            // Create an instance of our |InstanceImpl| that destroys itself when the connection
            // closes.
            new InstanceImpl(dispatcher, std::move(request));
          }));

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

系統產生的名稱

FIDL 方案:產生的名稱

產生的名稱是為匿名類型指派的 FIDL 編譯器名稱。雖然匿名類型無法在 FIDL 檔案中自行命名,但是在產生的繫結輸出內容中,必須有一些參照這些類型的名稱,因此使用者可在繫結語言中建立該類型的例項。

由於 FIDL 編譯器名稱產生演算法使用本機背景資訊 (成員名稱、方法名稱等) 來命名類型,因此可能會遇到名稱衝突。如要解決這類衝突,請將 @generated_name 屬性直接放在類型宣告之前,並指示編譯器應改用哪個名稱。

在這個變數中,我們允許鍵/值存放區將其他鍵/值存放區視為成員。簡單來說,我們把它變成一個樹狀結構。具體做法是將 value 的原始定義替換成使用兩成員 union 的變數:一個變化版本會儲存與先前相同 vector<byte> 類型的分葉節點,另一個變數則會以其他巢狀儲存庫的形式儲存分支節點。

推理

在這裡,我們來看看幾個「選用性」用法,藉此宣告可能存在或不存在的類型。FIDL 中有三種選用變種:

  • 類型一律儲存在線的類型,因此具有透過 null 信封描述「缺失」的內建方法。啟用這些類型的選用功能不會影響訊息的傳輸形狀,只會變更該特定類型有效值。透過新增 :optional 限制,unionvector<T>client_endserver_endzx.Handle 類型都可以設為選用。將 value union 設為選用項目,我們就能以缺少 value 的形式,引入標準「空值」項目。這表示空白的 bytes 以及缺少/空白的 store 屬性是無效的值。
  • 與上述類型不同,struct 版面配置並無額外空間,可儲存空值標頭。因此,郵件必須納入信封中,藉此變更所納入訊息的傳輸模式形狀。為確保此線路修改效果清晰易讀,Item struct 類型必須納入 box<T> 類型範本中。
  • 最後,table 版面配置一律為選用項目。缺少的 table 只是沒有任何成員設定的項目。

樹狀結構是一種自然的自參照資料結構,樹狀結構中的任何節點可能包含包含純資料的分葉 (在本例中為字串),或含有更多節點的子樹狀結構。這需要使用遞迴:Item 的定義現在間接依附於本身!在 FIDL 中代表遞迴類型可能有點困難,尤其是因為目前支援有部分限制。只要自我參照建立的週期中至少有一個選用類型,我們就能支援這類類型。舉例來說,這裡我們將 items struct 成員定義為 box<Item>,進而破壞包含週期。

這些變更也會大量使用「匿名類型」,或宣告內嵌於其唯一使用點,而非自行命名的頂層 type 宣告類型。根據預設,在產生的語言繫結中,匿名類型名稱取自本機背景資訊。舉例來說,新推出的 flexible union 會使用其本身成員的名稱 Value,新引入的 struct 會變成 Store,以此類推。由於此經驗法則有時可能會造成衝突,因此 FIDL 允許作者手動覆寫匿名類型的「產生的名稱」,提供一個跳躍點。方法是透過 @generated_name 屬性變更後端產生的名稱。我們可以在這裡使用一個,其中預計的 Store 類型已重新命名為 NestedStore,以避免與 protocol 宣告使用相同名稱的名稱衝突。

實作

FIDL、CML 和領域介面的定義修改如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.supporttrees;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value strict union {
        // Keep the original `bytes` as one of the options in the new union. All leaf nodes in the
        // tree must be `bytes`, or absent unions (representing empty). Empty byte arrays are
        // disallowed.
        1: bytes vector<byte>:64000;

        // Allows a store within a store, thereby turning our flat key-value store into a tree
        // thereof. Note the use of `@generated_name` to prevent a type-name collision with the
        // `Store` protocol below, and the use of `box<T>` to ensure that there is a break in the
        // chain of recursion, thereby allowing `Item` to include itself in its own definition.
        //
        // This is a table so that added fields, like for example a `hash`, can be easily added in
        // the future.
        2: store @generated_name("nested_store") table {
            1: items vector<box<Item>>;
        };
    }:optional;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// A very basic key-value store.
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.supporttrees.Store" },
    ],
    config: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // A newline separated list nested entries. The first line should be the key
        // for the nested store, and each subsequent entry should be a pointer to a text file
        // containing the string value. The name of that text file (without the `.txt` suffix) will
        // serve as the entries key.
        write_nested: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // A list of keys, all of which will be populated as null entries.
        write_null: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.supporttrees.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.supporttrees.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.supporttrees.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fidl_examples_keyvaluestore_supporttrees::{Item, NestedStore, StoreMarker, Value},
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        let res = store
            .write_item(&Item {
                key: key.clone(),
                value: Some(Box::new(Value::Bytes(value.into_bytes()))),
            })
            .await;
        match res? {
            Ok(_) => println!("WriteItem Success at key: {}", key),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // Add nested entries to the key-value store as well. The entries are strings, where the first
    // line is the key of the entry, and each subsequent entry should be a pointer to a text file
    // containing the string value. The name of that text file (without the `.txt` suffix) will
    // serve as the entries key.
    for spec in config.write_nested.into_iter() {
        let mut items = vec![];
        let mut nested_store = NestedStore::default();
        let mut lines = spec.split("\n");
        let key = lines.next().unwrap();

        // For each entry, make a new entry in the `NestedStore` being built.
        for entry in lines {
            let path = format!("/pkg/data/{}.txt", entry);
            let contents = std::fs::read_to_string(path.clone())
                .with_context(|| format!("Failed to load {path}"))?;
            items.push(Some(Box::new(Item {
                key: entry.to_string(),
                value: Some(Box::new(Value::Bytes(contents.into()))),
            })));
        }
        nested_store.items = Some(items);

        // Send the `NestedStore`, represented as a vector of values.
        let res = store
            .write_item(&Item {
                key: key.to_string(),
                value: Some(Box::new(Value::Store(nested_store))),
            })
            .await;
        match res? {
            Ok(_) => println!("WriteItem Success at key: {}", key),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // Each entry in this list is a null value in the store.
    for key in config.write_null.into_iter() {
        match store.write_item(&Item { key: key.to_string(), value: None }).await? {
            Ok(_) => println!("WriteItem Success at key: {}", key),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fidl_examples_keyvaluestore_supporttrees::{
        Item, StoreRequest, StoreRequestStream, Value, WriteError,
    },
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
    std::cell::RefCell,
    std::collections::hash_map::Entry,
    std::collections::HashMap,
    std::str::from_utf8,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile");
}

// A representation of a key-value store that can contain an arbitrarily deep nesting of other
// key-value stores.
#[allow(dead_code)] // TODO(https://fxbug.dev/318827209)
enum StoreNode {
    Leaf(Option<Vec<u8>>),
    Branch(Box<HashMap<String, StoreNode>>),
}

/// Recursive item writer, which takes a `StoreNode` that may not necessarily be the root node, and
/// writes an entry to it.
fn write_item(
    store: &mut HashMap<String, StoreNode>,
    attempt: Item,
    path: &str,
) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            let key = format!("{}{}", &path, entry.key());
            match attempt.value {
                // Null entries are allowed.
                None => {
                    println!("Wrote value: NONE at key: {}", key);
                    entry.insert(StoreNode::Leaf(None));
                }
                Some(value) => match *value {
                    // If this is a nested store, recursively make a new store to insert at this
                    // position.
                    Value::Store(entry_list) => {
                        // Validate the value - absent stores, items lists with no children, or any
                        // of the elements within that list being empty boxes, are all not allowed.
                        if entry_list.items.is_some() {
                            let items = entry_list.items.unwrap();
                            if !items.is_empty() && items.iter().all(|i| i.is_some()) {
                                let nested_path = format!("{}/", key);
                                let mut nested_store = HashMap::<String, StoreNode>::new();
                                for item in items.into_iter() {
                                    write_item(&mut nested_store, *item.unwrap(), &nested_path)?;
                                }

                                println!("Created branch at key: {}", key);
                                entry.insert(StoreNode::Branch(Box::new(nested_store)));
                                return Ok(());
                            }
                        }

                        println!("Write error: INVALID_VALUE, For key: {}", key);
                        return Err(WriteError::InvalidValue);
                    }

                    // This is a simple leaf node on this branch.
                    Value::Bytes(value) => {
                        // Validate the value.
                        if value.is_empty() {
                            println!("Write error: INVALID_VALUE, For key: {}", key);
                            return Err(WriteError::InvalidValue);
                        }

                        println!("Wrote key: {}, value: {:?}", key, from_utf8(&value).unwrap());
                        entry.insert(StoreNode::Leaf(Some(value)));
                    }
                },
            }
            Ok(())
        }
    }
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    let store = RefCell::new(HashMap::<String, StoreNode>::new());

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.borrow_mut(), attempt, ""))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

處理權利

FIDL 方案:處理權利

FIDL 控制代碼代表某些系統中特有的能力,通常是 Zircon 核心。「處理權限」是能力由 FIDL 可辨識的權限列舉,例如,無論控制代碼代表的資源是否可寫入、檢查、發出訊號等等。

權利會在編碼和解碼時驗證,確保指定的控制代碼具備介面作者針對該標籤分配的一組權限。

如要擴充鍵/值儲存庫來支援匯出備份,最簡單的方法是新增一個停止世界的新方法,將儲存庫的狀態序列化,然後以 FIDL vector<Item> 的形式傳回。但這種做法有兩個缺點其一是它會將備份的所有負擔都放進伺服器上,也就是說,用戶端不必向伺服器要求成本極高的備份作業。其二是需要進行大量複製:用戶端幾乎可以確定,在收到結果後,立即將產生的備份寫入某些備份資料儲存庫,例如檔案或資料庫。對它 (可能非常龐大) 的 FIDL 物件進行解碼,只是使其轉送到任何通訊協定將會執行實際儲存體時,可立即重新進行編碼,實為浪費。

推理

更理想的做法是使用 zircon 的虛擬記憶體物件。我們不必在值區分支版本中不斷複製位元組,而是可以採制 VMO 將備份資料保存在用戶端,然後將其傳送至伺服器,然後把備份傳回我們的目標資料儲存庫,而不必在值區之間進行還原序列化。只要目標資料儲存庫的通訊協定允許接受透過 VMO 傳輸的資料,這就是執行這類高成本作業的首選方法。例如,Fuchsia 的檔案系統確實實作這種模式。這種做法的好處是,在要求伺服器進行昂貴的作業時,會強制用戶端執行一些工作,以減少這兩個服務之間的工作不平衡。

使用 FIDL 資料持續性二進位格式,FIDL 值類型可以保留至任何位元組導向的儲存空間媒介。系統會將新引入的 FIDL 類型 Exportable 保存在 VMO 中。物件會經過編碼並寫入儲存空間 (在此例中是指之後可以儲存為檔案的 VMO),並在需要再次存取資料時將物件解碼,就像之後透過 IPC 使用 FIDL 時,訊息的編碼、傳輸和解碼方式大同小異。

為了安全地執行這項操作,並遵循最低權限原則,我們應限制代表 VMO 的控制代碼權限。輸入處理權,這是 FIDL 的第一種方法,用於描述特定帳號代碼的可用權限。在此情況下,我們允許透過 Export 要求傳遞至伺服器的 empty VMO,進行讀取、查詢大小、調整大小和寫入。當 VMO 傳回時,我們會移除調整大小和寫入的權利,確保沒有任何程序 (包括某些較遠的元件中的惡意人士) 能在系統傳遞資料時修改這些資料。

實作

FIDL、CML 和領域介面的定義如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.supportexports;

using zx;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value vector<byte>:64000;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// An enumeration of things that may go wrong when trying to mint an export.
type ExportError = flexible enum {
    UNKNOWN = 0;
    EMPTY = 1;
    STORAGE_TOO_SMALL = 2;
};

// A data type describing the structure of a single export. We never actually send this data type
// over the wire (we use the file's VMO instead), but whenever data needs to be written to/read from
// its backing storage as persistent FIDL, it will have this schema.
///
/// The items should be sorted in ascending order, following lexicographic ordering of their keys.
type Exportable = table {
    1: items vector<Item>;
};

/// A very basic key-value store - so basic, in fact, that one may only write to it, never read!
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;

    /// Exports the entire store as a persistent [`Exportable`] FIDL object into a VMO provided by
    /// the client.
    ///
    /// By having the client provide (and speculatively size) the VMO, we force the party requesting
    /// the relatively heavy load of generating a backup to acknowledge and bear some of the costs.
    ///
    /// This method operates by having the client supply an empty VMO, which the server then
    /// attempts to fill. Notice that the server removes the `zx.Rights.WRITE` and
    /// `zx.Rights.SET_PROPERTY` rights from the returned VMO - not even the requesting client may
    /// alter the backup once it has been minted by the server.
    flexible Export(resource struct {
        /// Note that the empty VMO has more rights than the filled one being returned: it has
        /// `zx.Rights.WRITE` (via `zx.RIGHTS_IO`) so that the VMO may be filled with exported data,
        /// and `zx.Rights.SET_PROPERTY` (via `zx.RIGHTS_PROPERTY`) so that it may be resized to
        /// truncate any remaining empty buffer.
        empty zx.Handle:<VMO, zx.RIGHTS_BASIC | zx.RIGHTS_PROPERTY | zx.RIGHTS_IO>;
    }) -> (resource struct {
        /// The `zx.Rights.WRITE` and `zx.Rights.SET_PROPERTY` rights have been removed from the now
        /// filled VMO. No one, not even the client that requested the export, is able to modify
        /// this VMO going forward.
        filled zx.Handle:<VMO, zx.RIGHTS_BASIC | zx.Rights.GET_PROPERTY | zx.Rights.READ>;
    }) error ExportError;
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.supportexports.Store" },
    ],
    config: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // The size, in bytes, allotted to the export VMO
        max_export_size: { type: "uint64" },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.supportexports.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.supportexports.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.supportexports.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

use {
    fidl::unpersist,
    fidl_examples_keyvaluestore_supportexports::{Exportable, Item, StoreMarker},
    fuchsia_zircon::Vmo,
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        match store.write_item(&Item { key: key, value: value.into_bytes() }).await? {
            Ok(_) => println!("WriteItem Success"),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // If the `max_export_size` is 0, no export is possible, so just ignore this block. This check
    // isn't strictly necessary, but does avoid extra work down the line.
    if config.max_export_size > 0 {
        // Create a 100Kb VMO to store the resulting export. In a real implementation, we would
        // likely receive the VMO representing the to-be-written file from file system like vfs of
        // fxfs.
        let vmo = Vmo::create(config.max_export_size)?;

        // Send the VMO to the server, to be populated with the current state of the key-value
        // store.
        match store.export(vmo).await? {
            Err(err) => {
                println!("Export Error: {}", err.into_primitive());
            }
            Ok(output) => {
                println!("Export Success");

                // Read the exported data (encoded in byte form as persistent FIDL) from the
                // returned VMO. In a real implementation, instead of reading the VMO, we would
                // merely forward it to some other storage-handling process. Doing this using a VMO,
                // rather than FIDL IPC, would save us frivolous reads and writes at each hop.
                let content_size = output.get_content_size().unwrap();
                let mut encoded_bytes = vec![0; content_size as usize];
                output.read(&mut encoded_bytes, 0)?;

                // Decode the persistent FIDL that was just read from the file.
                let exportable = unpersist::<Exportable>(&encoded_bytes).unwrap();
                let items = exportable.items.expect("must always be set");

                // Log some information about the exported data.
                println!("Printing {} exported entries, which are:", items.len());
                for item in items.iter() {
                    println!("  * {}", item.key);
                }
            }
        };
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
    std::cell::RefCell,
    std::collections::hash_map::Entry,
    std::collections::HashMap,
};

use {
    fidl::{persist, Vmo},
    fidl_examples_keyvaluestore_supportexports::{
        ExportError, Exportable, Item, StoreRequest, StoreRequestStream, WriteError,
    },
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile");
}

/// Handler for the `WriteItem` method.
fn write_item(store: &mut HashMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Validate the value.
    if attempt.value.is_empty() {
        println!("Write error: INVALID_VALUE, For key: {}", attempt.key);
        return Err(WriteError::InvalidValue);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote value at key: {}", entry.key());
            entry.insert(attempt.value);
            Ok(())
        }
    }
}

/// Handler for the `Export` method.
fn export(store: &mut HashMap<String, Vec<u8>>, vmo: Vmo) -> Result<Vmo, ExportError> {
    // Empty stores cannot be exported.
    if store.is_empty() {
        return Err(ExportError::Empty);
    }

    // Build the `Exportable` vector locally. That means iterating over the map, and turning it into
    // a vector of items instead.
    let mut exportable = Exportable::default();
    let mut items = store
        .iter()
        .map(|entry| return Item { key: entry.0.clone(), value: entry.1.clone() })
        .collect::<Vec<Item>>();
    items.sort_by(|a, b| a.key.cmp(&b.key));
    exportable.items = Some(items);

    // Encode the bytes - there is a bug in persistent FIDL if this operation fails. Even if it
    // succeeds, make sure to check that the VMO has enough space to handle the encoded export data.
    let encoded_bytes = persist(&exportable).map_err(|_| ExportError::Unknown)?;
    if encoded_bytes.len() as u64 > vmo.get_content_size().map_err(|_| ExportError::Unknown)? {
        return Err(ExportError::StorageTooSmall);
    }

    // Write the (now encoded) persistent FIDL data to the VMO.
    vmo.set_content_size(&(encoded_bytes.len() as u64)).map_err(|_| ExportError::Unknown)?;
    vmo.write(&encoded_bytes, 0).map_err(|_| ExportError::Unknown)?;
    Ok(vmo)
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    let store = RefCell::new(HashMap::<String, Vec<u8>>::new());

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.borrow_mut(), attempt))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::Export { empty, responder } => {
                    println!("Export request received");

                    responder
                        .send(export(&mut store.borrow_mut(), empty))
                        .context("error sending reply")?;
                    println!("Export response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.keyvaluestore.supportexports/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <examples/fidl/new/key_value_store/support_exports/cpp_natural/client/config.h>
#include <src/lib/files/file.h>
#include <src/lib/fxl/strings/string_printf.h>

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop and dispatcher.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the 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<examples_keyvaluestore_supportexports::Store>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Store| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // Create an asynchronous client using the newly-established connection.
  fidl::Client client(std::move(*client_end), dispatcher);
  FX_LOGS(INFO) << "Outgoing connection enabled";

  for (const auto& action : conf.write_items()) {
    std::string text;
    if (!files::ReadFileToString(fxl::StringPrintf("/pkg/data/%s.txt", action.c_str()), &text)) {
      FX_LOGS(ERROR) << "It looks like the correct `resource` dependency has not been packaged";
      break;
    }

    auto value = std::vector<uint8_t>(text.begin(), text.end());
    client->WriteItem(examples_keyvaluestore_supportexports::Item(action, value))
        .ThenExactlyOnce(
            [&](fidl::Result<examples_keyvaluestore_supportexports::Store::WriteItem> result) {
              // Check if the FIDL call succeeded or not.
              if (!result.is_ok()) {
                if (result.error_value().is_framework_error()) {
                  FX_LOGS(ERROR) << "Unexpected FIDL framework error: " << result.error_value();
                } else {
                  FX_LOGS(INFO) << "WriteItem Error: "
                                << fidl::ToUnderlying(result.error_value().domain_error());
                }
              } else {
                FX_LOGS(INFO) << "WriteItem Success";
              }

              // Quit the loop, thereby handing control back to the outer loop of actions being
              // iterated over.
              loop.Quit();
            });

    // Run the loop until the callback is resolved, at which point we can continue from here.
    loop.Run();
    loop.ResetQuit();
  }

  // If the `max_export_size` is 0, no export is possible, so just ignore this block. This check
  // isn't strictly necessary, but does avoid extra work down the line.
  if (conf.max_export_size() > 0) {
    // Create a 100Kb VMO to store the resulting export. In a real implementation, we would
    // likely receive the VMO representing the to-be-written file from file system like vfs of
    // fxfs.
    zx::vmo vmo;
    if (zx_status_t status = zx::vmo::create(conf.max_export_size(), 0, &vmo); status != ZX_OK) {
      FX_PLOGS(ERROR, status) << "Failed to create VMO";
      return -1;
    }

    client->Export({std::move(vmo)})
        .ThenExactlyOnce(
            [&](fidl::Result<examples_keyvaluestore_supportexports::Store::Export>& result) {
              // Quit the loop, thereby handing control back to the outer loop of actions being
              // iterated over, when we return from this callback.
              loop.Quit();

              if (!result.is_ok()) {
                if (result.error_value().is_framework_error()) {
                  FX_LOGS(ERROR) << "Unexpected FIDL framework error: " << result.error_value();
                } else {
                  FX_LOGS(INFO) << "Export Error: "
                                << fidl::ToUnderlying(result.error_value().domain_error());
                }
                return;
              }

              FX_LOGS(INFO) << "Export Success";
              // Read the exported data (encoded in byte form as persistent FIDL) from the
              // returned VMO. In a real implementation, instead of reading the VMO, we would
              // merely forward it to some other storage-handling process. Doing this using a VMO,
              // rather than FIDL IPC, would save us frivolous reads and writes at each hop.
              size_t content_size = 0;
              zx::vmo vmo = std::move(result->filled());
              if (vmo.get_prop_content_size(&content_size) != ZX_OK) {
                return;
              }
              std::vector<uint8_t> encoded_bytes;
              encoded_bytes.resize(content_size);
              if (vmo.read(encoded_bytes.data(), 0, content_size) != ZX_OK) {
                return;
              }
              // Decode the persistent FIDL that was just read from the file.
              fit::result exportable =
                  fidl::Unpersist<examples_keyvaluestore_supportexports::Exportable>(
                      cpp20::span(encoded_bytes));
              if (exportable.is_error()) {
                FX_LOGS(ERROR) << "Failed to unpersist: " << exportable.error_value();
                return;
              }
              if (!exportable->items().has_value()) {
                FX_LOGS(INFO) << "Expected items to be set";
                return;
              }
              auto& items = exportable->items().value();

              // Log some information about the exported data.
              FX_LOGS(INFO) << "Printing " << items.size() << " exported entries, which are:";
              for (const auto& item : items) {
                FX_LOGS(INFO) << "  * " << item.key();
              }
            });

    // Run the loop until the callback is resolved, at which point we can continue from here.
    loop.Run();
    loop.ResetQuit();
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.keyvaluestore.supportexports/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <algorithm>

#include <re2/re2.h>

// An implementation of the |Store| protocol.
class StoreImpl final : public fidl::Server<examples_keyvaluestore_supportexports::Store> {
 public:
  // Bind this implementation to a channel.
  StoreImpl(async_dispatcher_t* dispatcher,
            fidl::ServerEnd<examples_keyvaluestore_supportexports::Store> server_end)
      : binding_(fidl::BindServer(
            dispatcher, std::move(server_end), this,
            [this](StoreImpl* impl, fidl::UnbindInfo info,
                   fidl::ServerEnd<examples_keyvaluestore_supportexports::Store> server_end) {
              if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) {
                FX_LOGS(ERROR) << "Shutdown unexpectedly";
              }
              delete this;
            })) {}

  void WriteItem(WriteItemRequest& request, WriteItemCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "WriteItem request received";
    auto key = request.attempt().key();
    auto value = request.attempt().value();

    // Validate the key.
    if (!RE2::FullMatch(key, "^[A-Za-z]\\w+[A-Za-z0-9]$")) {
      FX_LOGS(INFO) << "Write error: INVALID_KEY, For key: " << key;
      FX_LOGS(INFO) << "WriteItem response sent";
      return completer.Reply(
          fit::error(examples_keyvaluestore_supportexports::WriteError::kInvalidKey));
    }

    // Validate the value.
    if (value.empty()) {
      FX_LOGS(INFO) << "Write error: INVALID_VALUE, For key: " << key;
      FX_LOGS(INFO) << "WriteItem response sent";
      return completer.Reply(
          fit::error(examples_keyvaluestore_supportexports::WriteError::kInvalidValue));
    }

    if (key_value_store_.find(key) != key_value_store_.end()) {
      FX_LOGS(INFO) << "Write error: ALREADY_EXISTS, For key: " << key;
      FX_LOGS(INFO) << "WriteItem response sent";
      return completer.Reply(
          fit::error(examples_keyvaluestore_supportexports::WriteError::kAlreadyExists));
    }

    // Ensure that the value does not already exist in the store.
    key_value_store_.insert({key, value});
    FX_LOGS(INFO) << "Wrote value at key: " << key;
    FX_LOGS(INFO) << "WriteItem response sent";
    return completer.Reply(fit::ok());
  }

  void Export(ExportRequest& request, ExportCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "Export request received";
    completer.Reply(Export(std::move(request.empty())));
    FX_LOGS(INFO) << "Export response sent";
  }

  void handle_unknown_method(
      fidl::UnknownMethodMetadata<examples_keyvaluestore_supportexports::Store> metadata,
      fidl::UnknownMethodCompleter::Sync& completer) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal;
  }

 private:
  using ExportError = ::examples_keyvaluestore_supportexports::ExportError;
  using Exportable = ::examples_keyvaluestore_supportexports::Exportable;
  using Item = ::examples_keyvaluestore_supportexports::Item;

  fit::result<ExportError, zx::vmo> Export(zx::vmo vmo) {
    if (key_value_store_.empty()) {
      return fit::error(ExportError::kEmpty);
    }
    Exportable exportable;
    std::vector<Item> items;
    items.reserve(key_value_store_.size());
    for (const auto& [k, v] : key_value_store_) {
      items.push_back(Item{{.key = k, .value = v}});
    }
    std::sort(items.begin(), items.end(),
              [](const Item& a, const Item& b) { return a.key() < b.key(); });
    exportable.items(std::move(items));
    fit::result encoded = fidl::Persist(exportable);
    if (encoded.is_error()) {
      FX_LOGS(ERROR) << "Failed to encode in persistence convention: " << encoded.error_value();
      return fit::error(ExportError::kUnknown);
    }
    size_t content_size = 0;
    if (vmo.get_prop_content_size(&content_size) != ZX_OK) {
      return fit::error(ExportError::kUnknown);
    }
    if (encoded->size() > content_size) {
      return fit::error(ExportError::kStorageTooSmall);
    }
    if (vmo.set_prop_content_size(encoded->size()) != ZX_OK) {
      return fit::error(ExportError::kUnknown);
    }
    if (vmo.write(encoded->data(), 0, encoded->size()) != ZX_OK) {
      return fit::error(ExportError::kUnknown);
    }
    return fit::ok(std::move(vmo));
  }

  fidl::ServerBindingRef<examples_keyvaluestore_supportexports::Store> binding_;

  // The map that serves as the per-connection instance of the key-value store.
  std::unordered_map<std::string, std::vector<uint8_t>> key_value_store_ = {};
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This
  // directory is where the outgoing FIDL protocols are installed so that they can be provided to
  // other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle.
  // The startup handle is a handle provided to every component by the system, so that they can
  // serve capabilities (e.g. FIDL protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to |Store|.
  result = outgoing.AddUnmanagedProtocol<examples_keyvaluestore_supportexports::Store>(
      [dispatcher](fidl::ServerEnd<examples_keyvaluestore_supportexports::Store> server_end) {
        // Create an instance of our StoreImpl that destroys itself when the connection closes.
        new StoreImpl(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Store protocol: " << result.status_string();
    return -1;
  }

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

C++ (有線)

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.keyvaluestore.supportexports/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <examples/fidl/new/key_value_store/support_exports/cpp_wire/client/config.h>
#include <src/lib/files/file.h>
#include <src/lib/fxl/strings/string_printf.h>

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop and dispatcher.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the 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<examples_keyvaluestore_supportexports::Store>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Store| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // Create an asynchronous client using the newly-established connection.
  fidl::WireClient client(std::move(*client_end), dispatcher);
  FX_LOGS(INFO) << "Outgoing connection enabled";

  for (const auto& key : conf.write_items()) {
    std::string text;
    if (!files::ReadFileToString(fxl::StringPrintf("/pkg/data/%s.txt", key.c_str()), &text)) {
      FX_LOGS(ERROR) << "It looks like the correct `resource` dependency has not been packaged";
      break;
    }

    auto value = std::vector<uint8_t>(text.begin(), text.end());
    client
        ->WriteItem(
            {fidl::StringView::FromExternal(key), fidl::VectorView<uint8_t>::FromExternal(value)})
        .ThenExactlyOnce(
            [&](fidl::WireUnownedResult<examples_keyvaluestore_supportexports::Store::WriteItem>&
                    result) {
              if (!result.ok()) {
                FX_LOGS(ERROR) << "Unexpected framework error";
              } else if (result->is_error()) {
                FX_LOGS(INFO) << "WriteItem Error: " << fidl::ToUnderlying(result->error_value());
              } else {
                FX_LOGS(INFO) << "WriteItem Success";
              }

              // Quit the loop, thereby handing control back to the outer loop of actions being
              // iterated over.
              loop.Quit();
            });

    // Run the loop until the callback is resolved, at which point we can continue from here.
    loop.Run();
    loop.ResetQuit();
  }

  // If the `max_export_size` is 0, no export is possible, so just ignore this block. This check
  // isn't strictly necessary, but does avoid extra work down the line.
  if (conf.max_export_size() > 0) {
    // Create a 100Kb VMO to store the resulting export. In a real implementation, we would
    // likely receive the VMO representing the to-be-written file from file system like vfs of
    // fxfs.
    zx::vmo vmo;
    if (zx_status_t status = zx::vmo::create(conf.max_export_size(), 0, &vmo); status != ZX_OK) {
      FX_PLOGS(ERROR, status) << "Failed to create VMO";
      return -1;
    }

    client->Export(std::move(vmo))
        .ThenExactlyOnce(
            [&](fidl::WireUnownedResult<examples_keyvaluestore_supportexports::Store::Export>&
                    result) {
              // Quit the loop, thereby handing control back to the outer loop of actions being
              // iterated over, when we return from this callback.
              loop.Quit();

              if (!result.ok()) {
                FX_LOGS(ERROR) << "Unexpected FIDL framework error: " << result.error();
                return;
              }

              if (!result->is_ok()) {
                FX_LOGS(INFO) << "Export Error: " << fidl::ToUnderlying(result->error_value());
                return;
              }

              FX_LOGS(INFO) << "Export Success";
              // Read the exported data (encoded in byte form as persistent FIDL) from the
              // returned VMO. In a real implementation, instead of reading the VMO, we would
              // merely forward it to some other storage-handling process. Doing this using a VMO,
              // rather than FIDL IPC, would save us frivolous reads and writes at each hop.
              size_t content_size = 0;
              zx::vmo vmo = std::move(result->value()->filled);
              if (vmo.get_prop_content_size(&content_size) != ZX_OK) {
                return;
              }
              std::vector<uint8_t> encoded_bytes;
              encoded_bytes.resize(content_size);
              if (vmo.read(encoded_bytes.data(), 0, content_size) != ZX_OK) {
                return;
              }
              // Decode the persistent FIDL that was just read from the file.
              fit::result exportable =
                  fidl::InplaceUnpersist<examples_keyvaluestore_supportexports::wire::Exportable>(
                      cpp20::span(encoded_bytes));
              if (exportable.is_error()) {
                FX_LOGS(ERROR) << "Failed to unpersist: " << exportable.error_value();
                return;
              }
              if (!exportable->has_items()) {
                FX_LOGS(INFO) << "Expected items to be set";
                return;
              }
              auto& items = exportable->items();

              // Log some information about the exported data.
              FX_LOGS(INFO) << "Printing " << items.count() << " exported entries, which are:";
              for (const auto& item : items) {
                FX_LOGS(INFO) << "  * " << item.key.get();
              }
            });

    // Run the loop until the callback is resolved, at which point we can continue from here.
    loop.Run();
    loop.ResetQuit();
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.keyvaluestore.supportexports/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <algorithm>

#include <re2/re2.h>

// An implementation of the |Store| protocol.
class StoreImpl final : public fidl::WireServer<examples_keyvaluestore_supportexports::Store> {
 public:
  // Bind this implementation to a channel.
  StoreImpl(async_dispatcher_t* dispatcher,
            fidl::ServerEnd<examples_keyvaluestore_supportexports::Store> server_end)
      : binding_(fidl::BindServer(
            dispatcher, std::move(server_end), this,
            [this](StoreImpl* impl, fidl::UnbindInfo info,
                   fidl::ServerEnd<examples_keyvaluestore_supportexports::Store> server_end) {
              if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) {
                FX_LOGS(ERROR) << "Shutdown unexpectedly";
              }
              delete this;
            })) {}

  void WriteItem(WriteItemRequestView request, WriteItemCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "WriteItem request received";
    std::string key{request->attempt.key.get()};
    std::vector<uint8_t> value{request->attempt.value.begin(), request->attempt.value.end()};

    // Validate the key.
    if (!RE2::FullMatch(key, "^[A-Za-z]\\w+[A-Za-z0-9]$")) {
      FX_LOGS(INFO) << "Write error: INVALID_KEY, For key: " << key;
      FX_LOGS(INFO) << "WriteItem response sent";
      return completer.Reply(
          fit::error(examples_keyvaluestore_supportexports::WriteError::kInvalidKey));
    }

    // Validate the value.
    if (value.empty()) {
      FX_LOGS(INFO) << "Write error: INVALID_VALUE, For key: " << key;
      FX_LOGS(INFO) << "WriteItem response sent";
      return completer.Reply(
          fit::error(examples_keyvaluestore_supportexports::WriteError::kInvalidValue));
    }

    if (key_value_store_.find(key) != key_value_store_.end()) {
      FX_LOGS(INFO) << "Write error: ALREADY_EXISTS, For key: " << key;
      FX_LOGS(INFO) << "WriteItem response sent";
      return completer.Reply(
          fit::error(examples_keyvaluestore_supportexports::WriteError::kAlreadyExists));
    }

    // Ensure that the value does not already exist in the store.
    key_value_store_.insert({key, value});
    FX_LOGS(INFO) << "Wrote value at key: " << key;
    FX_LOGS(INFO) << "WriteItem response sent";
    return completer.Reply(fit::success());
  }

  void Export(ExportRequestView request, ExportCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "Export request received";
    fit::result result = Export(std::move(request->empty));
    if (result.is_ok()) {
      completer.ReplySuccess(std::move(result.value()));
    } else {
      completer.ReplyError(result.error_value());
    }
    FX_LOGS(INFO) << "Export response sent";
  }

  using ExportError = ::examples_keyvaluestore_supportexports::wire::ExportError;
  using Exportable = ::examples_keyvaluestore_supportexports::wire::Exportable;
  using Item = ::examples_keyvaluestore_supportexports::wire::Item;

  fit::result<ExportError, zx::vmo> Export(zx::vmo vmo) {
    if (key_value_store_.empty()) {
      return fit::error(ExportError::kEmpty);
    }
    fidl::Arena arena;
    fidl::VectorView<Item> items;
    items.Allocate(arena, key_value_store_.size());
    size_t count = 0;
    for (auto& [k, v] : key_value_store_) {
      // Create a wire |Item| object that borrows from |k| and |v|.
      // Since |k| and |v| are references into the long living |key_value_store_|,
      // while |items| only live within the current function scope,
      // this operation is safe.
      items[count] = Item{
          .key = fidl::StringView::FromExternal(k),
          .value = fidl::VectorView<uint8_t>::FromExternal(v),
      };
      count++;
    }
    std::sort(items.begin(), items.end(),
              [](const Item& a, const Item& b) { return a.key.get() < b.key.get(); });
    Exportable exportable = Exportable::Builder(arena).items(items).Build();
    fit::result encoded = fidl::Persist(exportable);
    if (encoded.is_error()) {
      FX_LOGS(ERROR) << "Failed to encode in persistence convention: " << encoded.error_value();
      return fit::error(ExportError::kUnknown);
    }
    size_t content_size = 0;
    if (vmo.get_prop_content_size(&content_size) != ZX_OK) {
      return fit::error(ExportError::kUnknown);
    }
    if (encoded->size() > content_size) {
      return fit::error(ExportError::kStorageTooSmall);
    }
    if (vmo.set_prop_content_size(encoded->size()) != ZX_OK) {
      return fit::error(ExportError::kUnknown);
    }
    if (vmo.write(encoded->data(), 0, encoded->size()) != ZX_OK) {
      return fit::error(ExportError::kUnknown);
    }
    return fit::ok(std::move(vmo));
  }

  void handle_unknown_method(
      fidl::UnknownMethodMetadata<examples_keyvaluestore_supportexports::Store> metadata,
      fidl::UnknownMethodCompleter::Sync& completer) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal;
  }

 private:
  fidl::ServerBindingRef<examples_keyvaluestore_supportexports::Store> binding_;

  // The map that serves as the per-connection instance of the key-value store.
  //
  // Out-of-line references in wire types are always mutable. Thus the
  // |const std::vector<uint8_t>| from the baseline needs to be changed to
  // non-const as we're making a vector view pointing to it during |Export|,
  // even though in practice the value is never mutated.
  std::unordered_map<std::string, std::vector<uint8_t>> key_value_store_ = {};
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This
  // directory is where the outgoing FIDL protocols are installed so that they can be provided to
  // other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle.
  // The startup handle is a handle provided to every component by the system, so that they can
  // serve capabilities (e.g. FIDL protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to |Store|.
  result = outgoing.AddUnmanagedProtocol<examples_keyvaluestore_supportexports::Store>(
      [dispatcher](fidl::ServerEnd<examples_keyvaluestore_supportexports::Store> server_end) {
        // Create an instance of our StoreImpl that destroys itself when the connection closes.
        new StoreImpl(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Store protocol: " << result.status_string();
    return -1;
  }

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

不為人知的雙向方法

FIDL 方案:雙向的不恰當方式

「不可避免的雙向方法」是指無法傳回錯誤值的 FIDL 方法。唯一可能的失敗模式是基礎管道發生失敗 (例如無法連線至其中一個端點)。

在此範例中,您將建立基本的計算機伺服器和用戶端,用於顯示先定義和提供和使用 FIDL 通訊協定所需的基本設定。

首先,請定義介面定義和測試控管工具。介面定義 (.fidl 檔案本身) 是任何新 FIDL 通訊協定的起點。此外,計算機還包含必要的 CML 和領域定義,以建立用戶端伺服器模式,這個模式可以做為專案鷹架,用於任意實作。

請參閱下列 FIDL 代碼:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// The namespace for this FIDL protocol. This namespace is how both consumers (clients) and providers (servers) reference this protocol.
library examples.calculator.baseline;

// @discoverable indicates 'Calculator' is a protocol that will be served under the examples.calculator.baseline libarary namespace. https://fuchsia.dev/fuchsia-src/reference/fidl/language/attributes#discoverable . If @discoverable is missing, it will lead to a compile time error when trying to import the library.
@discoverable
// A limited-functionality calculator 'protocol' that adds and subtracts integers.
open protocol Calculator {
    // Takes as input a struct with two integers, and returns their sum: (a+b)=sum.  This method is infallible (no errors can be generated) as two int32's cannot overflow a result type of int64.
    flexible Add(struct {
        a int32;
        b int32;
    }) -> (struct {
        sum int64;
    });
    // Takes as input a struct with two integers, and returns their difference: (a-b)=difference.  This method is infallible (no errors can be generated) as two int32's cannot overflow a result type of int64.
    flexible Subtract(struct {
        a int32;
        b int32;
    }) -> (struct {
        difference int64;
    });
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.calculator.baseline.Calculator" },
    ],
    config: {},
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.calculator.baseline.Calculator" },
    ],
    expose: [
        {
            protocol: "examples.calculator.baseline.Calculator",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.calculator.baseline.Calculator",
            from: "#server",
            to: "#client",
        },

        // Route logging support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// TODO(https://fxbug.dev/42063075): Rust implementation.

伺服器

// TODO(https://fxbug.dev/42063075): Rust implementation.

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42063075): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42063075): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42063075): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42063075): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42063075): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42063075): HLCPP implementation.

如本例所示,從頭開始建立 FIDL 通訊協定是平台開發人員等特定開發人員的常見情境。不過,其他類型的開發人員也受益於瞭解如何建構 FIDL 通訊協定,即使一般不如此也是如此。這可協助您瞭解 FIDL 的所有資訊如何相輔相成,包括語法、文法、語言功能、如何提供和使用指定的 FIDL 通訊協定,以及建構系統的運作方式。在後續步驟中,遵循此基準的範例會顯示如何擴充現有的 FIDL 通訊協定,這是相當常見的做法。

已命名酬載

FIDL 方案:已命名酬載

已命名酬載是直接用做方法要求或回應酬載的 structtableunion 類型。若方法酬載可能重複,或為 FIDL 檔案中的其他位置使用了已命名的類型,就可以使用這些方法。

在以下範例中,新增的 ReadItem 方法與現有的 WriteItem 方法明顯不同 (使用現有的已命名類型做為酬載),而不是重複的內嵌定義。

推理

原始的唯寫鍵/值存放區現已擴充,可讀取商店外的項目。

實作

適用於 FIDL 和 CML 定義的變更如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.addreaditem;

// Aliases for the key and value. Using aliases helps increase the readability of FIDL files and
// reduces likelihood of errors due to differing constraints.
alias Key = string:128;
alias Value = vector<byte>:64000;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key Key;
    value Value;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// An enumeration of things that may go wrong when trying to read a value out of our store.
type ReadError = flexible enum {
    UNKNOWN = 0;
    NOT_FOUND = 1;
};

/// A very basic key-value store - so basic, in fact, that one may only write to it, never read!
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;

    /// Reads an item from the store.
    flexible ReadItem(struct {
        key Key;
    }) -> (Item) error ReadError;
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.addreaditem.Store" },
    ],
    config: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        read_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.addreaditem.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.addreaditem.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.addreaditem.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

所有語言的用戶端和伺服器實作也有所變更:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fidl_examples_keyvaluestore_addreaditem::{Item, StoreMarker},
    fuchsia_component::client::connect_to_protocol,
    std::{str, thread, time},
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        match store.write_item(&Item { key: key, value: value.into_bytes() }).await? {
            Ok(_) => println!("WriteItem Success"),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // The structured config for this client contains `read_items`, a vector of strings, each of
    // which is meant to be read from the key-value store. We iterate over these keys, attempting to
    // read them in turn.
    for key in config.read_items.into_iter() {
        let res = store.read_item(key.as_str()).await;
        match res.unwrap() {
            Ok(val) => {
                println!("ReadItem Success: key: {}, value: {}", key, str::from_utf8(&val.1)?)
            }
            Err(err) => println!("ReadItem Error: {}", err.into_primitive()),
        }
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fidl_examples_keyvaluestore_addreaditem::{
        Item, ReadError, StoreRequest, StoreRequestStream, WriteError,
    },
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
    std::cell::RefCell,
    std::collections::hash_map::Entry,
    std::collections::HashMap,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z][A-Za-z0-9_\./]{2,62}[A-Za-z0-9]$")
            .expect("Key validation regex failed to compile");
}

/// Handler for the `WriteItem` method.
fn write_item(store: &mut HashMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Validate the value.
    if attempt.value.is_empty() {
        println!("Write error: INVALID_VALUE, For key: {}", attempt.key);
        return Err(WriteError::InvalidValue);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote value at key: {}", entry.key());
            entry.insert(attempt.value);
            Ok(())
        }
    }
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    let store = RefCell::new(HashMap::<String, Vec<u8>>::new());

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.borrow_mut(), attempt))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::ReadItem { key, responder } => {
                    println!("ReadItem request received");

                    // Read the item from the store, returning the appropriate error if it could not be found.
                    responder
                        .send(match store.borrow().get(&key) {
                            Some(found) => {
                                println!("Read value at key: {}", key);
                                Ok((&key, found))
                            }
                            None => {
                                println!("Read error: NOT_FOUND, For key: {}", key);
                                Err(ReadError::NotFound)
                            }
                        })
                        .context("error sending reply")?;
                    println!("ReadItem response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

選填

FIDL 方案:選用

您可以將某些 FIDL 類型設為選用,而不因加入 :optional 限製而變更其所含訊息的線形形狀。此外,table 版面配置一律為選用,但 struct 版面配置一律不會。如要選用 struct,該版面配置必須納入 box<T> 中,藉此變更包含訊息的線形形狀。

基礎類型 選用版本 電線配置是否改變?
struct {...} box<struct {...}>
table {...} table {...}
union {...} union {...}:optional
vector<T> vector<T>:optional
string string:optional
zx.Handle zx.Handle:optional
client_end:P client_end:<P, optional>
server_end:P server_end:<P, optional>

所有其他類型 (bitsenumarray<T, N> 和原始類型) 均無法設為選用。

在這個變數中,我們允許鍵/值存放區將其他鍵/值存放區視為成員。簡單來說,我們把它變成一個樹狀結構。具體做法是將 value 的原始定義替換成使用兩成員 union 的變數:一個變化版本會儲存與先前相同 vector<byte> 類型的分葉節點,另一個變數則會以其他巢狀儲存庫的形式儲存分支節點。

推理

在這裡,我們來看看幾個「選用性」用法,藉此宣告可能存在或不存在的類型。FIDL 中有三種選用變種:

  • 類型一律儲存在線的類型,因此具有透過 null 信封描述「缺失」的內建方法。啟用這些類型的選用功能不會影響訊息的傳輸形狀,只會變更該特定類型有效值。透過新增 :optional 限制,unionvector<T>client_endserver_endzx.Handle 類型都可以設為選用。將 value union 設為選用項目,我們就能以缺少 value 的形式,引入標準「空值」項目。這表示空白的 bytes 以及缺少/空白的 store 屬性是無效的值。
  • 與上述類型不同,struct 版面配置並無額外空間,可儲存空值標頭。因此,郵件必須納入信封中,藉此變更所納入訊息的傳輸模式形狀。為確保此線路修改效果清晰易讀,Item struct 類型必須納入 box<T> 類型範本中。
  • 最後,table 版面配置一律為選用項目。缺少的 table 只是沒有任何成員設定的項目。

樹狀結構是一種自然的自參照資料結構,樹狀結構中的任何節點可能包含包含純資料的分葉 (在本例中為字串),或含有更多節點的子樹狀結構。這需要使用遞迴:Item 的定義現在間接依附於本身!在 FIDL 中代表遞迴類型可能有點困難,尤其是因為目前支援有部分限制。只要自我參照建立的週期中至少有一個選用類型,我們就能支援這類類型。舉例來說,這裡我們將 items struct 成員定義為 box<Item>,進而破壞包含週期。

這些變更也會大量使用「匿名類型」,或宣告內嵌於其唯一使用點,而非自行命名的頂層 type 宣告類型。根據預設,在產生的語言繫結中,匿名類型名稱取自本機背景資訊。舉例來說,新推出的 flexible union 會使用其本身成員的名稱 Value,新引入的 struct 會變成 Store,以此類推。由於此經驗法則有時可能會造成衝突,因此 FIDL 允許作者手動覆寫匿名類型的「產生的名稱」,提供一個跳躍點。方法是透過 @generated_name 屬性變更後端產生的名稱。我們可以在這裡使用一個,其中預計的 Store 類型已重新命名為 NestedStore,以避免與 protocol 宣告使用相同名稱的名稱衝突。

實作

FIDL、CML 和領域介面的定義修改如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.supporttrees;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value strict union {
        // Keep the original `bytes` as one of the options in the new union. All leaf nodes in the
        // tree must be `bytes`, or absent unions (representing empty). Empty byte arrays are
        // disallowed.
        1: bytes vector<byte>:64000;

        // Allows a store within a store, thereby turning our flat key-value store into a tree
        // thereof. Note the use of `@generated_name` to prevent a type-name collision with the
        // `Store` protocol below, and the use of `box<T>` to ensure that there is a break in the
        // chain of recursion, thereby allowing `Item` to include itself in its own definition.
        //
        // This is a table so that added fields, like for example a `hash`, can be easily added in
        // the future.
        2: store @generated_name("nested_store") table {
            1: items vector<box<Item>>;
        };
    }:optional;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// A very basic key-value store.
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.supporttrees.Store" },
    ],
    config: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // A newline separated list nested entries. The first line should be the key
        // for the nested store, and each subsequent entry should be a pointer to a text file
        // containing the string value. The name of that text file (without the `.txt` suffix) will
        // serve as the entries key.
        write_nested: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // A list of keys, all of which will be populated as null entries.
        write_null: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.supporttrees.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.supporttrees.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.supporttrees.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fidl_examples_keyvaluestore_supporttrees::{Item, NestedStore, StoreMarker, Value},
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        let res = store
            .write_item(&Item {
                key: key.clone(),
                value: Some(Box::new(Value::Bytes(value.into_bytes()))),
            })
            .await;
        match res? {
            Ok(_) => println!("WriteItem Success at key: {}", key),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // Add nested entries to the key-value store as well. The entries are strings, where the first
    // line is the key of the entry, and each subsequent entry should be a pointer to a text file
    // containing the string value. The name of that text file (without the `.txt` suffix) will
    // serve as the entries key.
    for spec in config.write_nested.into_iter() {
        let mut items = vec![];
        let mut nested_store = NestedStore::default();
        let mut lines = spec.split("\n");
        let key = lines.next().unwrap();

        // For each entry, make a new entry in the `NestedStore` being built.
        for entry in lines {
            let path = format!("/pkg/data/{}.txt", entry);
            let contents = std::fs::read_to_string(path.clone())
                .with_context(|| format!("Failed to load {path}"))?;
            items.push(Some(Box::new(Item {
                key: entry.to_string(),
                value: Some(Box::new(Value::Bytes(contents.into()))),
            })));
        }
        nested_store.items = Some(items);

        // Send the `NestedStore`, represented as a vector of values.
        let res = store
            .write_item(&Item {
                key: key.to_string(),
                value: Some(Box::new(Value::Store(nested_store))),
            })
            .await;
        match res? {
            Ok(_) => println!("WriteItem Success at key: {}", key),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // Each entry in this list is a null value in the store.
    for key in config.write_null.into_iter() {
        match store.write_item(&Item { key: key.to_string(), value: None }).await? {
            Ok(_) => println!("WriteItem Success at key: {}", key),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fidl_examples_keyvaluestore_supporttrees::{
        Item, StoreRequest, StoreRequestStream, Value, WriteError,
    },
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
    std::cell::RefCell,
    std::collections::hash_map::Entry,
    std::collections::HashMap,
    std::str::from_utf8,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile");
}

// A representation of a key-value store that can contain an arbitrarily deep nesting of other
// key-value stores.
#[allow(dead_code)] // TODO(https://fxbug.dev/318827209)
enum StoreNode {
    Leaf(Option<Vec<u8>>),
    Branch(Box<HashMap<String, StoreNode>>),
}

/// Recursive item writer, which takes a `StoreNode` that may not necessarily be the root node, and
/// writes an entry to it.
fn write_item(
    store: &mut HashMap<String, StoreNode>,
    attempt: Item,
    path: &str,
) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            let key = format!("{}{}", &path, entry.key());
            match attempt.value {
                // Null entries are allowed.
                None => {
                    println!("Wrote value: NONE at key: {}", key);
                    entry.insert(StoreNode::Leaf(None));
                }
                Some(value) => match *value {
                    // If this is a nested store, recursively make a new store to insert at this
                    // position.
                    Value::Store(entry_list) => {
                        // Validate the value - absent stores, items lists with no children, or any
                        // of the elements within that list being empty boxes, are all not allowed.
                        if entry_list.items.is_some() {
                            let items = entry_list.items.unwrap();
                            if !items.is_empty() && items.iter().all(|i| i.is_some()) {
                                let nested_path = format!("{}/", key);
                                let mut nested_store = HashMap::<String, StoreNode>::new();
                                for item in items.into_iter() {
                                    write_item(&mut nested_store, *item.unwrap(), &nested_path)?;
                                }

                                println!("Created branch at key: {}", key);
                                entry.insert(StoreNode::Branch(Box::new(nested_store)));
                                return Ok(());
                            }
                        }

                        println!("Write error: INVALID_VALUE, For key: {}", key);
                        return Err(WriteError::InvalidValue);
                    }

                    // This is a simple leaf node on this branch.
                    Value::Bytes(value) => {
                        // Validate the value.
                        if value.is_empty() {
                            println!("Write error: INVALID_VALUE, For key: {}", key);
                            return Err(WriteError::InvalidValue);
                        }

                        println!("Wrote key: {}, value: {:?}", key, from_utf8(&value).unwrap());
                        entry.insert(StoreNode::Leaf(Some(value)));
                    }
                },
            }
            Ok(())
        }
    }
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    let store = RefCell::new(HashMap::<String, StoreNode>::new());

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.borrow_mut(), attempt, ""))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

分頁模式

FIDL 方案:分頁模式

傳送可能非常龐大的項目清單時,實用的策略是透過分頁模式將清單拆分成多個呼叫。使用分頁可讓傳送端和接收者之間的工作進行更精細的同步處理:傳送者不會同時發出大量清單給接收方,而是一次發出幾個項目,並等待訊息處理完畢後再繼續。

以 FIDL 的術語來說,這表示 FIDL 作者應該將其轉換為 vector<T>:N 的已確認訊息,而不是傳送單一大型 vector<T>,以確保頁面大小和流量控制是 FIDL 合約的一部分。

對鍵/值存放區來說是按順序疊代作業的實用作業,也就是在提供鍵時,依序傳回顯示在其後方的元素清單 (通常是分頁)。

推理

在 FIDL 中,這樣做最好使用疊代器。疊代器通常會以獨立通訊協定的形式實作,此疊代作業可進行此疊代。使用獨立的通訊協定 (因此使用獨立管道) 有許多好處,包括將疊代提取要求從主要通訊協定完成的其他作業中解除交錯。

通訊協定 P 管道連線的用戶端和伺服器端,可分別以 client_end:Pserver_end:P 表示,以 FIDL 資料類型表示。這些類型統稱為「通訊協定結束」,代表透過現有的 FIDL 連線,將 FIDL 用戶端連線至其對應伺服器的其他 (非 @discoverable) 方式!

通訊協定結尾是一般 FIDL 概念的特定執行個體:資源類型。資源類型包含 FIDL 控制代碼,因此對於該類型的使用方式會有額外限制。類型一律不得重複,因為基礎資源會由其他能力管理員 (通常是 Zircon 核心) 中介。在不涉及管理程式的情況下,透過簡單的記憶體內副本複製此類資源是不可能的。為避免這類重複情形,FIDL 中的所有資源類型一律僅限移動。

最後,Iterator 通訊協定的 Get() 方法本身會在傳回酬載上使用大小限制。這樣可限制單次提取可傳輸的資料量,因此可某些測量資源用量控管機制。這也會建立自然的分頁界線:伺服器只需一次準備小型批次,而不是一次列出所有結果的大型傾印。

實作

FIDL、CML 和領域介面的定義如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.additerator;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value vector<byte>:64000;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// An enumeration of things that may go wrong when trying to create an iterator.
type IterateConnectionError = flexible enum {
    /// The starting key was not found.
    UNKNOWN_START_AT = 1;
};

/// A key-value store which supports insertion and iteration.
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;

    /// Iterates over the items in the store, using lexicographic ordering over the keys.
    ///
    /// The [`iterator`] is [pipelined][pipelining] to the server, such that the client can
    /// immediately send requests over the new connection.
    ///
    /// [pipelining]: https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#request-pipelining
    flexible Iterate(resource struct {
        /// If present, requests to start the iteration at this item.
        starting_at string:<128, optional>;

        /// The [`Iterator`] server endpoint. The client creates both ends of the channel and
        /// retains the `client_end` locally to use for pulling iteration pages, while sending the
        /// `server_end` off to be fulfilled by the server.
        iterator server_end:Iterator;
    }) -> () error IterateConnectionError;
};

/// An iterator for the key-value store. Note that this protocol makes no guarantee of atomicity -
/// the values may change between pulls from the iterator. Unlike the `Store` protocol above, this
/// protocol is not `@discoverable`: it is not independently published by the component that
/// implements it, but rather must have one of its two protocol ends transmitted over an existing
/// FIDL connection.
///
/// As is often the case with iterators, the client indicates that they are done with an instance of
/// the iterator by simply closing their end of the connection.
///
/// Since the iterator is associated only with the Iterate method, it is declared as closed rather
/// than open. This is because changes to how iteration works are more likely to require replacing
/// the Iterate method completely (which is fine because that method is flexible) rather than
/// evolving the Iterator protocol.
closed protocol Iterator {
    /// Gets the next batch of keys.
    ///
    /// The client pulls keys rather than having the server proactively push them, to implement
    /// [flow control][flow-control] over the messages.
    ///
    /// [flow-control]:
    ///     https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#prefer_pull_to_push
    strict Get() -> (struct {
        /// A list of keys. If the iterator has reached the end of iteration, the list will be
        /// empty. The client is expected to then close the connection.
        entries vector<string:128>:10;
    });
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.additerator.Store" },
    ],
    config: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // A key to iterate from, after all items in `write_items` have been written.
        iterate_from: {
            type: "string",
            max_size: 64,
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.additerator.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.additerator.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.additerator.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

use {
    fidl::endpoints::create_proxy,
    fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker},
    futures::join,
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        match store.write_item(&Item { key: key, value: value.into_bytes() }).await? {
            Ok(_) => println!("WriteItem Success"),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    if !config.iterate_from.is_empty() {
        // This helper creates a channel, and returns two protocol ends: the `client_end` is already
        // conveniently bound to the correct FIDL protocol, `Iterator`, while the `server_end` is
        // unbound and ready to be sent over the wire.
        let (iterator, server_end) = create_proxy::<IteratorMarker>()?;

        // There is no need to wait for the iterator to connect before sending the first `Get()`
        // request - since we already hold the `client_end` of the connection, we can start queuing
        // requests on it immediately.
        let connect_to_iterator = store.iterate(Some(config.iterate_from.as_str()), server_end);
        let first_get = iterator.get();

        // Wait until both the connection and the first request resolve - an error in either case
        // triggers an immediate resolution of the combined future.
        let (connection, first_page) = join!(connect_to_iterator, first_get);

        // Handle any connection error. If this has occurred, it is impossible for the first `Get()`
        // call to have resolved successfully, so check this error first.
        if let Err(err) = connection.context("Could not connect to Iterator")? {
            println!("Iterator Connection Error: {}", err.into_primitive());
        } else {
            println!("Iterator Connection Success");

            // Consecutively repeat the `Get()` request if the previous response was not empty.
            let mut entries = first_page.context("Could not get page from Iterator")?;
            while !&entries.is_empty() {
                for entry in entries.iter() {
                    println!("Iterator Entry: {}", entry);
                }
                entries = iterator.get().await.context("Could not get page from Iterator")?;
            }
        }
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
};

use {
    fidl_examples_keyvaluestore_additerator::{
        Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest,
        StoreRequestStream, WriteError,
    },
    fuchsia_async as fasync,
    std::collections::btree_map::Entry,
    std::collections::BTreeMap,
    std::ops::Bound::*,
    std::sync::Arc,
    std::sync::Mutex,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile");
}

/// Handler for the `WriteItem` method.
fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Validate the value.
    if attempt.value.is_empty() {
        println!("Write error: INVALID_VALUE, For key: {}", attempt.key);
        return Err(WriteError::InvalidValue);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote value at key: {}", entry.key());
            entry.insert(attempt.value);
            Ok(())
        }
    }
}

/// Handler for the `Iterate` method, which deals with validating that the requested start position
/// exists, and then sets up the asynchronous side channel for the actual iteration to occur over.
fn iterate(
    store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>,
    starting_at: Option<String>,
    stream: IteratorRequestStream,
) -> Result<(), IterateConnectionError> {
    // Validate that the starting key, if supplied, actually exists.
    if let Some(start_key) = starting_at.clone() {
        if !store.lock().unwrap().contains_key(&start_key) {
            return Err(IterateConnectionError::UnknownStartAt);
        }
    }

    // Spawn a detached task. This allows the method call to return while the iteration continues in
    // a separate, unawaited task.
    fasync::Task::spawn(async move {
        // Serve the iteration requests. Note that access to the underlying store is behind a
        // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift,
        // change, or disappear entirely between `Get()` requests.
        stream
            .map(|result| result.context("failed request"))
            .try_fold(
                match starting_at {
                    Some(start_key) => Included(start_key),
                    None => Unbounded,
                },
                |mut lower_bound, request| async {
                    match request {
                        IteratorRequest::Get { responder } => {
                            println!("Iterator page request received");

                            // The `page_size` should be kept in sync with the size constraint on
                            // the iterator's response, as defined in the FIDL protocol.
                            static PAGE_SIZE: usize = 10;

                            // An iterator, beginning at `lower_bound` and tracking the pagination's
                            // progress through iteration as each page is pulled by a client-sent
                            // `Get()` request.
                            let held_store = store.lock().unwrap();
                            let mut entries = held_store.range((lower_bound.clone(), Unbounded));
                            let mut current_page = vec![];
                            for _ in 0..PAGE_SIZE {
                                match entries.next() {
                                    Some(entry) => {
                                        current_page.push(entry.0.clone());
                                    }
                                    None => break,
                                }
                            }

                            // Update the `lower_bound` - either inclusive of the next item in the
                            // iteration, or exclusive of the last seen item if the iteration has
                            // finished. This `lower_bound` will be passed to the next request
                            // handler as its starting point.
                            lower_bound = match entries.next() {
                                Some(next) => Included(next.0.clone()),
                                None => match current_page.last() {
                                    Some(tail) => Excluded(tail.clone()),
                                    None => lower_bound,
                                },
                            };

                            // Send the page. At the end of this scope, the `held_store` lock gets
                            // dropped, and therefore released.
                            responder.send(&current_page).context("error sending reply")?;
                            println!("Iterator page sent");
                        }
                    }
                    Ok(lower_bound)
                },
            )
            .await
            .ok();
    })
    .detach();

    Ok(())
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    //
    // Note that we now use an `Arc<Mutex<BTreeMap>>`, replacing the previous `RefCell<HashMap>`.
    // The `BTreeMap` is used because we want an ordered map, to better facilitate iteration. The
    // `Arc<Mutex<...>>` is used because there are now multiple async tasks accessing the: one main
    // task which handles communication over the protocol, and one additional task per iterator
    // protocol. `Arc<Mutex<...>>` is the simplest way to synchronize concurrent access between
    // these racing tasks.
    let store = &Arc::new(Mutex::new(BTreeMap::<String, Vec<u8>>::new()));

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.clone().lock().unwrap(), attempt))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::Iterate { starting_at, iterator, responder } => {
                    println!("Iterate request received");

                    // The `iterate` handler does a quick check to see that the request is valid,
                    // then spins up a separate worker task to serve the newly minted `Iterator`
                    // protocol instance, allowing this call to return immediately and continue the
                    // request stream with other work.
                    responder
                        .send(iterate(store.clone(), starting_at, iterator.into_stream()?))
                        .context("error sending reply")?;
                    println!("Iterate response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

保留

FIDL 方案:持續性

永久 FIDL 是指在不基礎傳輸的情況下,儲存的線式編碼二進位 FIDL 資料。相反地,系統會使用永久的位元組導向介面 (例如檔案或資料庫項目) 儲存資料一段時間。

如要擴充鍵/值儲存庫來支援匯出備份,最簡單的方法是新增一個停止世界的新方法,將儲存庫的狀態序列化,然後以 FIDL vector<Item> 的形式傳回。但這種做法有兩個缺點其一是它會將備份的所有負擔都放進伺服器上,也就是說,用戶端不必向伺服器要求成本極高的備份作業。其二是需要進行大量複製:用戶端幾乎可以確定,在收到結果後,立即將產生的備份寫入某些備份資料儲存庫,例如檔案或資料庫。對它 (可能非常龐大) 的 FIDL 物件進行解碼,只是使其轉送到任何通訊協定將會執行實際儲存體時,可立即重新進行編碼,實為浪費。

推理

更理想的做法是使用 zircon 的虛擬記憶體物件。我們不必在值區分支版本中不斷複製位元組,而是可以採制 VMO 將備份資料保存在用戶端,然後將其傳送至伺服器,然後把備份傳回我們的目標資料儲存庫,而不必在值區之間進行還原序列化。只要目標資料儲存庫的通訊協定允許接受透過 VMO 傳輸的資料,這就是執行這類高成本作業的首選方法。例如,Fuchsia 的檔案系統確實實作這種模式。這種做法的好處是,在要求伺服器進行昂貴的作業時,會強制用戶端執行一些工作,以減少這兩個服務之間的工作不平衡。

使用 FIDL 資料持續性二進位格式,FIDL 值類型可以保留至任何位元組導向的儲存空間媒介。系統會將新引入的 FIDL 類型 Exportable 保存在 VMO 中。物件會經過編碼並寫入儲存空間 (在此例中是指之後可以儲存為檔案的 VMO),並在需要再次存取資料時將物件解碼,就像之後透過 IPC 使用 FIDL 時,訊息的編碼、傳輸和解碼方式大同小異。

為了安全地執行這項操作,並遵循最低權限原則,我們應限制代表 VMO 的控制代碼權限。輸入處理權,這是 FIDL 的第一種方法,用於描述特定帳號代碼的可用權限。在此情況下,我們允許透過 Export 要求傳遞至伺服器的 empty VMO,進行讀取、查詢大小、調整大小和寫入。當 VMO 傳回時,我們會移除調整大小和寫入的權利,確保沒有任何程序 (包括某些較遠的元件中的惡意人士) 能在系統傳遞資料時修改這些資料。

實作

FIDL、CML 和領域介面的定義如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.supportexports;

using zx;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value vector<byte>:64000;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// An enumeration of things that may go wrong when trying to mint an export.
type ExportError = flexible enum {
    UNKNOWN = 0;
    EMPTY = 1;
    STORAGE_TOO_SMALL = 2;
};

// A data type describing the structure of a single export. We never actually send this data type
// over the wire (we use the file's VMO instead), but whenever data needs to be written to/read from
// its backing storage as persistent FIDL, it will have this schema.
///
/// The items should be sorted in ascending order, following lexicographic ordering of their keys.
type Exportable = table {
    1: items vector<Item>;
};

/// A very basic key-value store - so basic, in fact, that one may only write to it, never read!
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;

    /// Exports the entire store as a persistent [`Exportable`] FIDL object into a VMO provided by
    /// the client.
    ///
    /// By having the client provide (and speculatively size) the VMO, we force the party requesting
    /// the relatively heavy load of generating a backup to acknowledge and bear some of the costs.
    ///
    /// This method operates by having the client supply an empty VMO, which the server then
    /// attempts to fill. Notice that the server removes the `zx.Rights.WRITE` and
    /// `zx.Rights.SET_PROPERTY` rights from the returned VMO - not even the requesting client may
    /// alter the backup once it has been minted by the server.
    flexible Export(resource struct {
        /// Note that the empty VMO has more rights than the filled one being returned: it has
        /// `zx.Rights.WRITE` (via `zx.RIGHTS_IO`) so that the VMO may be filled with exported data,
        /// and `zx.Rights.SET_PROPERTY` (via `zx.RIGHTS_PROPERTY`) so that it may be resized to
        /// truncate any remaining empty buffer.
        empty zx.Handle:<VMO, zx.RIGHTS_BASIC | zx.RIGHTS_PROPERTY | zx.RIGHTS_IO>;
    }) -> (resource struct {
        /// The `zx.Rights.WRITE` and `zx.Rights.SET_PROPERTY` rights have been removed from the now
        /// filled VMO. No one, not even the client that requested the export, is able to modify
        /// this VMO going forward.
        filled zx.Handle:<VMO, zx.RIGHTS_BASIC | zx.Rights.GET_PROPERTY | zx.Rights.READ>;
    }) error ExportError;
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.supportexports.Store" },
    ],
    config: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // The size, in bytes, allotted to the export VMO
        max_export_size: { type: "uint64" },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.supportexports.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.supportexports.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.supportexports.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

use {
    fidl::unpersist,
    fidl_examples_keyvaluestore_supportexports::{Exportable, Item, StoreMarker},
    fuchsia_zircon::Vmo,
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        match store.write_item(&Item { key: key, value: value.into_bytes() }).await? {
            Ok(_) => println!("WriteItem Success"),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // If the `max_export_size` is 0, no export is possible, so just ignore this block. This check
    // isn't strictly necessary, but does avoid extra work down the line.
    if config.max_export_size > 0 {
        // Create a 100Kb VMO to store the resulting export. In a real implementation, we would
        // likely receive the VMO representing the to-be-written file from file system like vfs of
        // fxfs.
        let vmo = Vmo::create(config.max_export_size)?;

        // Send the VMO to the server, to be populated with the current state of the key-value
        // store.
        match store.export(vmo).await? {
            Err(err) => {
                println!("Export Error: {}", err.into_primitive());
            }
            Ok(output) => {
                println!("Export Success");

                // Read the exported data (encoded in byte form as persistent FIDL) from the
                // returned VMO. In a real implementation, instead of reading the VMO, we would
                // merely forward it to some other storage-handling process. Doing this using a VMO,
                // rather than FIDL IPC, would save us frivolous reads and writes at each hop.
                let content_size = output.get_content_size().unwrap();
                let mut encoded_bytes = vec![0; content_size as usize];
                output.read(&mut encoded_bytes, 0)?;

                // Decode the persistent FIDL that was just read from the file.
                let exportable = unpersist::<Exportable>(&encoded_bytes).unwrap();
                let items = exportable.items.expect("must always be set");

                // Log some information about the exported data.
                println!("Printing {} exported entries, which are:", items.len());
                for item in items.iter() {
                    println!("  * {}", item.key);
                }
            }
        };
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
    std::cell::RefCell,
    std::collections::hash_map::Entry,
    std::collections::HashMap,
};

use {
    fidl::{persist, Vmo},
    fidl_examples_keyvaluestore_supportexports::{
        ExportError, Exportable, Item, StoreRequest, StoreRequestStream, WriteError,
    },
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile");
}

/// Handler for the `WriteItem` method.
fn write_item(store: &mut HashMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Validate the value.
    if attempt.value.is_empty() {
        println!("Write error: INVALID_VALUE, For key: {}", attempt.key);
        return Err(WriteError::InvalidValue);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote value at key: {}", entry.key());
            entry.insert(attempt.value);
            Ok(())
        }
    }
}

/// Handler for the `Export` method.
fn export(store: &mut HashMap<String, Vec<u8>>, vmo: Vmo) -> Result<Vmo, ExportError> {
    // Empty stores cannot be exported.
    if store.is_empty() {
        return Err(ExportError::Empty);
    }

    // Build the `Exportable` vector locally. That means iterating over the map, and turning it into
    // a vector of items instead.
    let mut exportable = Exportable::default();
    let mut items = store
        .iter()
        .map(|entry| return Item { key: entry.0.clone(), value: entry.1.clone() })
        .collect::<Vec<Item>>();
    items.sort_by(|a, b| a.key.cmp(&b.key));
    exportable.items = Some(items);

    // Encode the bytes - there is a bug in persistent FIDL if this operation fails. Even if it
    // succeeds, make sure to check that the VMO has enough space to handle the encoded export data.
    let encoded_bytes = persist(&exportable).map_err(|_| ExportError::Unknown)?;
    if encoded_bytes.len() as u64 > vmo.get_content_size().map_err(|_| ExportError::Unknown)? {
        return Err(ExportError::StorageTooSmall);
    }

    // Write the (now encoded) persistent FIDL data to the VMO.
    vmo.set_content_size(&(encoded_bytes.len() as u64)).map_err(|_| ExportError::Unknown)?;
    vmo.write(&encoded_bytes, 0).map_err(|_| ExportError::Unknown)?;
    Ok(vmo)
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    let store = RefCell::new(HashMap::<String, Vec<u8>>::new());

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.borrow_mut(), attempt))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::Export { empty, responder } => {
                    println!("Export request received");

                    responder
                        .send(export(&mut store.borrow_mut(), empty))
                        .context("error sending reply")?;
                    println!("Export response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.keyvaluestore.supportexports/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <examples/fidl/new/key_value_store/support_exports/cpp_natural/client/config.h>
#include <src/lib/files/file.h>
#include <src/lib/fxl/strings/string_printf.h>

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop and dispatcher.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the 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<examples_keyvaluestore_supportexports::Store>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Store| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // Create an asynchronous client using the newly-established connection.
  fidl::Client client(std::move(*client_end), dispatcher);
  FX_LOGS(INFO) << "Outgoing connection enabled";

  for (const auto& action : conf.write_items()) {
    std::string text;
    if (!files::ReadFileToString(fxl::StringPrintf("/pkg/data/%s.txt", action.c_str()), &text)) {
      FX_LOGS(ERROR) << "It looks like the correct `resource` dependency has not been packaged";
      break;
    }

    auto value = std::vector<uint8_t>(text.begin(), text.end());
    client->WriteItem(examples_keyvaluestore_supportexports::Item(action, value))
        .ThenExactlyOnce(
            [&](fidl::Result<examples_keyvaluestore_supportexports::Store::WriteItem> result) {
              // Check if the FIDL call succeeded or not.
              if (!result.is_ok()) {
                if (result.error_value().is_framework_error()) {
                  FX_LOGS(ERROR) << "Unexpected FIDL framework error: " << result.error_value();
                } else {
                  FX_LOGS(INFO) << "WriteItem Error: "
                                << fidl::ToUnderlying(result.error_value().domain_error());
                }
              } else {
                FX_LOGS(INFO) << "WriteItem Success";
              }

              // Quit the loop, thereby handing control back to the outer loop of actions being
              // iterated over.
              loop.Quit();
            });

    // Run the loop until the callback is resolved, at which point we can continue from here.
    loop.Run();
    loop.ResetQuit();
  }

  // If the `max_export_size` is 0, no export is possible, so just ignore this block. This check
  // isn't strictly necessary, but does avoid extra work down the line.
  if (conf.max_export_size() > 0) {
    // Create a 100Kb VMO to store the resulting export. In a real implementation, we would
    // likely receive the VMO representing the to-be-written file from file system like vfs of
    // fxfs.
    zx::vmo vmo;
    if (zx_status_t status = zx::vmo::create(conf.max_export_size(), 0, &vmo); status != ZX_OK) {
      FX_PLOGS(ERROR, status) << "Failed to create VMO";
      return -1;
    }

    client->Export({std::move(vmo)})
        .ThenExactlyOnce(
            [&](fidl::Result<examples_keyvaluestore_supportexports::Store::Export>& result) {
              // Quit the loop, thereby handing control back to the outer loop of actions being
              // iterated over, when we return from this callback.
              loop.Quit();

              if (!result.is_ok()) {
                if (result.error_value().is_framework_error()) {
                  FX_LOGS(ERROR) << "Unexpected FIDL framework error: " << result.error_value();
                } else {
                  FX_LOGS(INFO) << "Export Error: "
                                << fidl::ToUnderlying(result.error_value().domain_error());
                }
                return;
              }

              FX_LOGS(INFO) << "Export Success";
              // Read the exported data (encoded in byte form as persistent FIDL) from the
              // returned VMO. In a real implementation, instead of reading the VMO, we would
              // merely forward it to some other storage-handling process. Doing this using a VMO,
              // rather than FIDL IPC, would save us frivolous reads and writes at each hop.
              size_t content_size = 0;
              zx::vmo vmo = std::move(result->filled());
              if (vmo.get_prop_content_size(&content_size) != ZX_OK) {
                return;
              }
              std::vector<uint8_t> encoded_bytes;
              encoded_bytes.resize(content_size);
              if (vmo.read(encoded_bytes.data(), 0, content_size) != ZX_OK) {
                return;
              }
              // Decode the persistent FIDL that was just read from the file.
              fit::result exportable =
                  fidl::Unpersist<examples_keyvaluestore_supportexports::Exportable>(
                      cpp20::span(encoded_bytes));
              if (exportable.is_error()) {
                FX_LOGS(ERROR) << "Failed to unpersist: " << exportable.error_value();
                return;
              }
              if (!exportable->items().has_value()) {
                FX_LOGS(INFO) << "Expected items to be set";
                return;
              }
              auto& items = exportable->items().value();

              // Log some information about the exported data.
              FX_LOGS(INFO) << "Printing " << items.size() << " exported entries, which are:";
              for (const auto& item : items) {
                FX_LOGS(INFO) << "  * " << item.key();
              }
            });

    // Run the loop until the callback is resolved, at which point we can continue from here.
    loop.Run();
    loop.ResetQuit();
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.keyvaluestore.supportexports/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <algorithm>

#include <re2/re2.h>

// An implementation of the |Store| protocol.
class StoreImpl final : public fidl::Server<examples_keyvaluestore_supportexports::Store> {
 public:
  // Bind this implementation to a channel.
  StoreImpl(async_dispatcher_t* dispatcher,
            fidl::ServerEnd<examples_keyvaluestore_supportexports::Store> server_end)
      : binding_(fidl::BindServer(
            dispatcher, std::move(server_end), this,
            [this](StoreImpl* impl, fidl::UnbindInfo info,
                   fidl::ServerEnd<examples_keyvaluestore_supportexports::Store> server_end) {
              if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) {
                FX_LOGS(ERROR) << "Shutdown unexpectedly";
              }
              delete this;
            })) {}

  void WriteItem(WriteItemRequest& request, WriteItemCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "WriteItem request received";
    auto key = request.attempt().key();
    auto value = request.attempt().value();

    // Validate the key.
    if (!RE2::FullMatch(key, "^[A-Za-z]\\w+[A-Za-z0-9]$")) {
      FX_LOGS(INFO) << "Write error: INVALID_KEY, For key: " << key;
      FX_LOGS(INFO) << "WriteItem response sent";
      return completer.Reply(
          fit::error(examples_keyvaluestore_supportexports::WriteError::kInvalidKey));
    }

    // Validate the value.
    if (value.empty()) {
      FX_LOGS(INFO) << "Write error: INVALID_VALUE, For key: " << key;
      FX_LOGS(INFO) << "WriteItem response sent";
      return completer.Reply(
          fit::error(examples_keyvaluestore_supportexports::WriteError::kInvalidValue));
    }

    if (key_value_store_.find(key) != key_value_store_.end()) {
      FX_LOGS(INFO) << "Write error: ALREADY_EXISTS, For key: " << key;
      FX_LOGS(INFO) << "WriteItem response sent";
      return completer.Reply(
          fit::error(examples_keyvaluestore_supportexports::WriteError::kAlreadyExists));
    }

    // Ensure that the value does not already exist in the store.
    key_value_store_.insert({key, value});
    FX_LOGS(INFO) << "Wrote value at key: " << key;
    FX_LOGS(INFO) << "WriteItem response sent";
    return completer.Reply(fit::ok());
  }

  void Export(ExportRequest& request, ExportCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "Export request received";
    completer.Reply(Export(std::move(request.empty())));
    FX_LOGS(INFO) << "Export response sent";
  }

  void handle_unknown_method(
      fidl::UnknownMethodMetadata<examples_keyvaluestore_supportexports::Store> metadata,
      fidl::UnknownMethodCompleter::Sync& completer) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal;
  }

 private:
  using ExportError = ::examples_keyvaluestore_supportexports::ExportError;
  using Exportable = ::examples_keyvaluestore_supportexports::Exportable;
  using Item = ::examples_keyvaluestore_supportexports::Item;

  fit::result<ExportError, zx::vmo> Export(zx::vmo vmo) {
    if (key_value_store_.empty()) {
      return fit::error(ExportError::kEmpty);
    }
    Exportable exportable;
    std::vector<Item> items;
    items.reserve(key_value_store_.size());
    for (const auto& [k, v] : key_value_store_) {
      items.push_back(Item{{.key = k, .value = v}});
    }
    std::sort(items.begin(), items.end(),
              [](const Item& a, const Item& b) { return a.key() < b.key(); });
    exportable.items(std::move(items));
    fit::result encoded = fidl::Persist(exportable);
    if (encoded.is_error()) {
      FX_LOGS(ERROR) << "Failed to encode in persistence convention: " << encoded.error_value();
      return fit::error(ExportError::kUnknown);
    }
    size_t content_size = 0;
    if (vmo.get_prop_content_size(&content_size) != ZX_OK) {
      return fit::error(ExportError::kUnknown);
    }
    if (encoded->size() > content_size) {
      return fit::error(ExportError::kStorageTooSmall);
    }
    if (vmo.set_prop_content_size(encoded->size()) != ZX_OK) {
      return fit::error(ExportError::kUnknown);
    }
    if (vmo.write(encoded->data(), 0, encoded->size()) != ZX_OK) {
      return fit::error(ExportError::kUnknown);
    }
    return fit::ok(std::move(vmo));
  }

  fidl::ServerBindingRef<examples_keyvaluestore_supportexports::Store> binding_;

  // The map that serves as the per-connection instance of the key-value store.
  std::unordered_map<std::string, std::vector<uint8_t>> key_value_store_ = {};
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This
  // directory is where the outgoing FIDL protocols are installed so that they can be provided to
  // other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle.
  // The startup handle is a handle provided to every component by the system, so that they can
  // serve capabilities (e.g. FIDL protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to |Store|.
  result = outgoing.AddUnmanagedProtocol<examples_keyvaluestore_supportexports::Store>(
      [dispatcher](fidl::ServerEnd<examples_keyvaluestore_supportexports::Store> server_end) {
        // Create an instance of our StoreImpl that destroys itself when the connection closes.
        new StoreImpl(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Store protocol: " << result.status_string();
    return -1;
  }

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

C++ (有線)

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.keyvaluestore.supportexports/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <examples/fidl/new/key_value_store/support_exports/cpp_wire/client/config.h>
#include <src/lib/files/file.h>
#include <src/lib/fxl/strings/string_printf.h>

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop and dispatcher.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the 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<examples_keyvaluestore_supportexports::Store>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Store| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // Create an asynchronous client using the newly-established connection.
  fidl::WireClient client(std::move(*client_end), dispatcher);
  FX_LOGS(INFO) << "Outgoing connection enabled";

  for (const auto& key : conf.write_items()) {
    std::string text;
    if (!files::ReadFileToString(fxl::StringPrintf("/pkg/data/%s.txt", key.c_str()), &text)) {
      FX_LOGS(ERROR) << "It looks like the correct `resource` dependency has not been packaged";
      break;
    }

    auto value = std::vector<uint8_t>(text.begin(), text.end());
    client
        ->WriteItem(
            {fidl::StringView::FromExternal(key), fidl::VectorView<uint8_t>::FromExternal(value)})
        .ThenExactlyOnce(
            [&](fidl::WireUnownedResult<examples_keyvaluestore_supportexports::Store::WriteItem>&
                    result) {
              if (!result.ok()) {
                FX_LOGS(ERROR) << "Unexpected framework error";
              } else if (result->is_error()) {
                FX_LOGS(INFO) << "WriteItem Error: " << fidl::ToUnderlying(result->error_value());
              } else {
                FX_LOGS(INFO) << "WriteItem Success";
              }

              // Quit the loop, thereby handing control back to the outer loop of actions being
              // iterated over.
              loop.Quit();
            });

    // Run the loop until the callback is resolved, at which point we can continue from here.
    loop.Run();
    loop.ResetQuit();
  }

  // If the `max_export_size` is 0, no export is possible, so just ignore this block. This check
  // isn't strictly necessary, but does avoid extra work down the line.
  if (conf.max_export_size() > 0) {
    // Create a 100Kb VMO to store the resulting export. In a real implementation, we would
    // likely receive the VMO representing the to-be-written file from file system like vfs of
    // fxfs.
    zx::vmo vmo;
    if (zx_status_t status = zx::vmo::create(conf.max_export_size(), 0, &vmo); status != ZX_OK) {
      FX_PLOGS(ERROR, status) << "Failed to create VMO";
      return -1;
    }

    client->Export(std::move(vmo))
        .ThenExactlyOnce(
            [&](fidl::WireUnownedResult<examples_keyvaluestore_supportexports::Store::Export>&
                    result) {
              // Quit the loop, thereby handing control back to the outer loop of actions being
              // iterated over, when we return from this callback.
              loop.Quit();

              if (!result.ok()) {
                FX_LOGS(ERROR) << "Unexpected FIDL framework error: " << result.error();
                return;
              }

              if (!result->is_ok()) {
                FX_LOGS(INFO) << "Export Error: " << fidl::ToUnderlying(result->error_value());
                return;
              }

              FX_LOGS(INFO) << "Export Success";
              // Read the exported data (encoded in byte form as persistent FIDL) from the
              // returned VMO. In a real implementation, instead of reading the VMO, we would
              // merely forward it to some other storage-handling process. Doing this using a VMO,
              // rather than FIDL IPC, would save us frivolous reads and writes at each hop.
              size_t content_size = 0;
              zx::vmo vmo = std::move(result->value()->filled);
              if (vmo.get_prop_content_size(&content_size) != ZX_OK) {
                return;
              }
              std::vector<uint8_t> encoded_bytes;
              encoded_bytes.resize(content_size);
              if (vmo.read(encoded_bytes.data(), 0, content_size) != ZX_OK) {
                return;
              }
              // Decode the persistent FIDL that was just read from the file.
              fit::result exportable =
                  fidl::InplaceUnpersist<examples_keyvaluestore_supportexports::wire::Exportable>(
                      cpp20::span(encoded_bytes));
              if (exportable.is_error()) {
                FX_LOGS(ERROR) << "Failed to unpersist: " << exportable.error_value();
                return;
              }
              if (!exportable->has_items()) {
                FX_LOGS(INFO) << "Expected items to be set";
                return;
              }
              auto& items = exportable->items();

              // Log some information about the exported data.
              FX_LOGS(INFO) << "Printing " << items.count() << " exported entries, which are:";
              for (const auto& item : items) {
                FX_LOGS(INFO) << "  * " << item.key.get();
              }
            });

    // Run the loop until the callback is resolved, at which point we can continue from here.
    loop.Run();
    loop.ResetQuit();
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.keyvaluestore.supportexports/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <algorithm>

#include <re2/re2.h>

// An implementation of the |Store| protocol.
class StoreImpl final : public fidl::WireServer<examples_keyvaluestore_supportexports::Store> {
 public:
  // Bind this implementation to a channel.
  StoreImpl(async_dispatcher_t* dispatcher,
            fidl::ServerEnd<examples_keyvaluestore_supportexports::Store> server_end)
      : binding_(fidl::BindServer(
            dispatcher, std::move(server_end), this,
            [this](StoreImpl* impl, fidl::UnbindInfo info,
                   fidl::ServerEnd<examples_keyvaluestore_supportexports::Store> server_end) {
              if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) {
                FX_LOGS(ERROR) << "Shutdown unexpectedly";
              }
              delete this;
            })) {}

  void WriteItem(WriteItemRequestView request, WriteItemCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "WriteItem request received";
    std::string key{request->attempt.key.get()};
    std::vector<uint8_t> value{request->attempt.value.begin(), request->attempt.value.end()};

    // Validate the key.
    if (!RE2::FullMatch(key, "^[A-Za-z]\\w+[A-Za-z0-9]$")) {
      FX_LOGS(INFO) << "Write error: INVALID_KEY, For key: " << key;
      FX_LOGS(INFO) << "WriteItem response sent";
      return completer.Reply(
          fit::error(examples_keyvaluestore_supportexports::WriteError::kInvalidKey));
    }

    // Validate the value.
    if (value.empty()) {
      FX_LOGS(INFO) << "Write error: INVALID_VALUE, For key: " << key;
      FX_LOGS(INFO) << "WriteItem response sent";
      return completer.Reply(
          fit::error(examples_keyvaluestore_supportexports::WriteError::kInvalidValue));
    }

    if (key_value_store_.find(key) != key_value_store_.end()) {
      FX_LOGS(INFO) << "Write error: ALREADY_EXISTS, For key: " << key;
      FX_LOGS(INFO) << "WriteItem response sent";
      return completer.Reply(
          fit::error(examples_keyvaluestore_supportexports::WriteError::kAlreadyExists));
    }

    // Ensure that the value does not already exist in the store.
    key_value_store_.insert({key, value});
    FX_LOGS(INFO) << "Wrote value at key: " << key;
    FX_LOGS(INFO) << "WriteItem response sent";
    return completer.Reply(fit::success());
  }

  void Export(ExportRequestView request, ExportCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "Export request received";
    fit::result result = Export(std::move(request->empty));
    if (result.is_ok()) {
      completer.ReplySuccess(std::move(result.value()));
    } else {
      completer.ReplyError(result.error_value());
    }
    FX_LOGS(INFO) << "Export response sent";
  }

  using ExportError = ::examples_keyvaluestore_supportexports::wire::ExportError;
  using Exportable = ::examples_keyvaluestore_supportexports::wire::Exportable;
  using Item = ::examples_keyvaluestore_supportexports::wire::Item;

  fit::result<ExportError, zx::vmo> Export(zx::vmo vmo) {
    if (key_value_store_.empty()) {
      return fit::error(ExportError::kEmpty);
    }
    fidl::Arena arena;
    fidl::VectorView<Item> items;
    items.Allocate(arena, key_value_store_.size());
    size_t count = 0;
    for (auto& [k, v] : key_value_store_) {
      // Create a wire |Item| object that borrows from |k| and |v|.
      // Since |k| and |v| are references into the long living |key_value_store_|,
      // while |items| only live within the current function scope,
      // this operation is safe.
      items[count] = Item{
          .key = fidl::StringView::FromExternal(k),
          .value = fidl::VectorView<uint8_t>::FromExternal(v),
      };
      count++;
    }
    std::sort(items.begin(), items.end(),
              [](const Item& a, const Item& b) { return a.key.get() < b.key.get(); });
    Exportable exportable = Exportable::Builder(arena).items(items).Build();
    fit::result encoded = fidl::Persist(exportable);
    if (encoded.is_error()) {
      FX_LOGS(ERROR) << "Failed to encode in persistence convention: " << encoded.error_value();
      return fit::error(ExportError::kUnknown);
    }
    size_t content_size = 0;
    if (vmo.get_prop_content_size(&content_size) != ZX_OK) {
      return fit::error(ExportError::kUnknown);
    }
    if (encoded->size() > content_size) {
      return fit::error(ExportError::kStorageTooSmall);
    }
    if (vmo.set_prop_content_size(encoded->size()) != ZX_OK) {
      return fit::error(ExportError::kUnknown);
    }
    if (vmo.write(encoded->data(), 0, encoded->size()) != ZX_OK) {
      return fit::error(ExportError::kUnknown);
    }
    return fit::ok(std::move(vmo));
  }

  void handle_unknown_method(
      fidl::UnknownMethodMetadata<examples_keyvaluestore_supportexports::Store> metadata,
      fidl::UnknownMethodCompleter::Sync& completer) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal;
  }

 private:
  fidl::ServerBindingRef<examples_keyvaluestore_supportexports::Store> binding_;

  // The map that serves as the per-connection instance of the key-value store.
  //
  // Out-of-line references in wire types are always mutable. Thus the
  // |const std::vector<uint8_t>| from the baseline needs to be changed to
  // non-const as we're making a vector view pointing to it during |Export|,
  // even though in practice the value is never mutated.
  std::unordered_map<std::string, std::vector<uint8_t>> key_value_store_ = {};
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This
  // directory is where the outgoing FIDL protocols are installed so that they can be provided to
  // other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle.
  // The startup handle is a handle provided to every component by the system, so that they can
  // serve capabilities (e.g. FIDL protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to |Store|.
  result = outgoing.AddUnmanagedProtocol<examples_keyvaluestore_supportexports::Store>(
      [dispatcher](fidl::ServerEnd<examples_keyvaluestore_supportexports::Store> server_end) {
        // Create an instance of our StoreImpl that destroys itself when the connection closes.
        new StoreImpl(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Store protocol: " << result.status_string();
    return -1;
  }

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

通訊協定結尾

FIDL 方案:通訊協定結束

「通訊協定端」代表管道連線的一個端點,系統會透過該端點說出某些指定的 FIDL 通訊協定。這個連線的伺服器端為 server_end,用戶端則是 client_end

通訊協定結尾有必要的限制,可指定透過連線叫用的 FIDL 通訊協定。例如,client_end:Foo 代表 Zircon 管道的用戶端端點,所有交換的訊息都會符合該 FIDL 通訊協定中定義的方法和事件,而 server_end:Foo 則代表相反端點。

對鍵/值存放區來說是按順序疊代作業的實用作業,也就是在提供鍵時,依序傳回顯示在其後方的元素清單 (通常是分頁)。

推理

在 FIDL 中,這樣做最好使用疊代器。疊代器通常會以獨立通訊協定的形式實作,此疊代作業可進行此疊代。使用獨立的通訊協定 (因此使用獨立管道) 有許多好處,包括將疊代提取要求從主要通訊協定完成的其他作業中解除交錯。

通訊協定 P 管道連線的用戶端和伺服器端,可分別以 client_end:Pserver_end:P 表示,以 FIDL 資料類型表示。這些類型統稱為「通訊協定結束」,代表透過現有的 FIDL 連線,將 FIDL 用戶端連線至其對應伺服器的其他 (非 @discoverable) 方式!

通訊協定結尾是一般 FIDL 概念的特定執行個體:資源類型。資源類型包含 FIDL 控制代碼,因此對於該類型的使用方式會有額外限制。類型一律不得重複,因為基礎資源會由其他能力管理員 (通常是 Zircon 核心) 中介。在不涉及管理程式的情況下,透過簡單的記憶體內副本複製此類資源是不可能的。為避免這類重複情形,FIDL 中的所有資源類型一律僅限移動。

最後,Iterator 通訊協定的 Get() 方法本身會在傳回酬載上使用大小限制。這樣可限制單次提取可傳輸的資料量,因此可某些測量資源用量控管機制。這也會建立自然的分頁界線:伺服器只需一次準備小型批次,而不是一次列出所有結果的大型傾印。

實作

FIDL、CML 和領域介面的定義如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.additerator;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value vector<byte>:64000;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// An enumeration of things that may go wrong when trying to create an iterator.
type IterateConnectionError = flexible enum {
    /// The starting key was not found.
    UNKNOWN_START_AT = 1;
};

/// A key-value store which supports insertion and iteration.
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;

    /// Iterates over the items in the store, using lexicographic ordering over the keys.
    ///
    /// The [`iterator`] is [pipelined][pipelining] to the server, such that the client can
    /// immediately send requests over the new connection.
    ///
    /// [pipelining]: https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#request-pipelining
    flexible Iterate(resource struct {
        /// If present, requests to start the iteration at this item.
        starting_at string:<128, optional>;

        /// The [`Iterator`] server endpoint. The client creates both ends of the channel and
        /// retains the `client_end` locally to use for pulling iteration pages, while sending the
        /// `server_end` off to be fulfilled by the server.
        iterator server_end:Iterator;
    }) -> () error IterateConnectionError;
};

/// An iterator for the key-value store. Note that this protocol makes no guarantee of atomicity -
/// the values may change between pulls from the iterator. Unlike the `Store` protocol above, this
/// protocol is not `@discoverable`: it is not independently published by the component that
/// implements it, but rather must have one of its two protocol ends transmitted over an existing
/// FIDL connection.
///
/// As is often the case with iterators, the client indicates that they are done with an instance of
/// the iterator by simply closing their end of the connection.
///
/// Since the iterator is associated only with the Iterate method, it is declared as closed rather
/// than open. This is because changes to how iteration works are more likely to require replacing
/// the Iterate method completely (which is fine because that method is flexible) rather than
/// evolving the Iterator protocol.
closed protocol Iterator {
    /// Gets the next batch of keys.
    ///
    /// The client pulls keys rather than having the server proactively push them, to implement
    /// [flow control][flow-control] over the messages.
    ///
    /// [flow-control]:
    ///     https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#prefer_pull_to_push
    strict Get() -> (struct {
        /// A list of keys. If the iterator has reached the end of iteration, the list will be
        /// empty. The client is expected to then close the connection.
        entries vector<string:128>:10;
    });
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.additerator.Store" },
    ],
    config: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // A key to iterate from, after all items in `write_items` have been written.
        iterate_from: {
            type: "string",
            max_size: 64,
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.additerator.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.additerator.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.additerator.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

use {
    fidl::endpoints::create_proxy,
    fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker},
    futures::join,
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        match store.write_item(&Item { key: key, value: value.into_bytes() }).await? {
            Ok(_) => println!("WriteItem Success"),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    if !config.iterate_from.is_empty() {
        // This helper creates a channel, and returns two protocol ends: the `client_end` is already
        // conveniently bound to the correct FIDL protocol, `Iterator`, while the `server_end` is
        // unbound and ready to be sent over the wire.
        let (iterator, server_end) = create_proxy::<IteratorMarker>()?;

        // There is no need to wait for the iterator to connect before sending the first `Get()`
        // request - since we already hold the `client_end` of the connection, we can start queuing
        // requests on it immediately.
        let connect_to_iterator = store.iterate(Some(config.iterate_from.as_str()), server_end);
        let first_get = iterator.get();

        // Wait until both the connection and the first request resolve - an error in either case
        // triggers an immediate resolution of the combined future.
        let (connection, first_page) = join!(connect_to_iterator, first_get);

        // Handle any connection error. If this has occurred, it is impossible for the first `Get()`
        // call to have resolved successfully, so check this error first.
        if let Err(err) = connection.context("Could not connect to Iterator")? {
            println!("Iterator Connection Error: {}", err.into_primitive());
        } else {
            println!("Iterator Connection Success");

            // Consecutively repeat the `Get()` request if the previous response was not empty.
            let mut entries = first_page.context("Could not get page from Iterator")?;
            while !&entries.is_empty() {
                for entry in entries.iter() {
                    println!("Iterator Entry: {}", entry);
                }
                entries = iterator.get().await.context("Could not get page from Iterator")?;
            }
        }
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
};

use {
    fidl_examples_keyvaluestore_additerator::{
        Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest,
        StoreRequestStream, WriteError,
    },
    fuchsia_async as fasync,
    std::collections::btree_map::Entry,
    std::collections::BTreeMap,
    std::ops::Bound::*,
    std::sync::Arc,
    std::sync::Mutex,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile");
}

/// Handler for the `WriteItem` method.
fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Validate the value.
    if attempt.value.is_empty() {
        println!("Write error: INVALID_VALUE, For key: {}", attempt.key);
        return Err(WriteError::InvalidValue);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote value at key: {}", entry.key());
            entry.insert(attempt.value);
            Ok(())
        }
    }
}

/// Handler for the `Iterate` method, which deals with validating that the requested start position
/// exists, and then sets up the asynchronous side channel for the actual iteration to occur over.
fn iterate(
    store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>,
    starting_at: Option<String>,
    stream: IteratorRequestStream,
) -> Result<(), IterateConnectionError> {
    // Validate that the starting key, if supplied, actually exists.
    if let Some(start_key) = starting_at.clone() {
        if !store.lock().unwrap().contains_key(&start_key) {
            return Err(IterateConnectionError::UnknownStartAt);
        }
    }

    // Spawn a detached task. This allows the method call to return while the iteration continues in
    // a separate, unawaited task.
    fasync::Task::spawn(async move {
        // Serve the iteration requests. Note that access to the underlying store is behind a
        // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift,
        // change, or disappear entirely between `Get()` requests.
        stream
            .map(|result| result.context("failed request"))
            .try_fold(
                match starting_at {
                    Some(start_key) => Included(start_key),
                    None => Unbounded,
                },
                |mut lower_bound, request| async {
                    match request {
                        IteratorRequest::Get { responder } => {
                            println!("Iterator page request received");

                            // The `page_size` should be kept in sync with the size constraint on
                            // the iterator's response, as defined in the FIDL protocol.
                            static PAGE_SIZE: usize = 10;

                            // An iterator, beginning at `lower_bound` and tracking the pagination's
                            // progress through iteration as each page is pulled by a client-sent
                            // `Get()` request.
                            let held_store = store.lock().unwrap();
                            let mut entries = held_store.range((lower_bound.clone(), Unbounded));
                            let mut current_page = vec![];
                            for _ in 0..PAGE_SIZE {
                                match entries.next() {
                                    Some(entry) => {
                                        current_page.push(entry.0.clone());
                                    }
                                    None => break,
                                }
                            }

                            // Update the `lower_bound` - either inclusive of the next item in the
                            // iteration, or exclusive of the last seen item if the iteration has
                            // finished. This `lower_bound` will be passed to the next request
                            // handler as its starting point.
                            lower_bound = match entries.next() {
                                Some(next) => Included(next.0.clone()),
                                None => match current_page.last() {
                                    Some(tail) => Excluded(tail.clone()),
                                    None => lower_bound,
                                },
                            };

                            // Send the page. At the end of this scope, the `held_store` lock gets
                            // dropped, and therefore released.
                            responder.send(&current_page).context("error sending reply")?;
                            println!("Iterator page sent");
                        }
                    }
                    Ok(lower_bound)
                },
            )
            .await
            .ok();
    })
    .detach();

    Ok(())
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    //
    // Note that we now use an `Arc<Mutex<BTreeMap>>`, replacing the previous `RefCell<HashMap>`.
    // The `BTreeMap` is used because we want an ordered map, to better facilitate iteration. The
    // `Arc<Mutex<...>>` is used because there are now multiple async tasks accessing the: one main
    // task which handles communication over the protocol, and one additional task per iterator
    // protocol. `Arc<Mutex<...>>` is the simplest way to synchronize concurrent access between
    // these racing tasks.
    let store = &Arc::new(Mutex::new(BTreeMap::<String, Vec<u8>>::new()));

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.clone().lock().unwrap(), attempt))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::Iterate { starting_at, iterator, responder } => {
                    println!("Iterate request received");

                    // The `iterate` handler does a quick check to see that the request is valid,
                    // then spins up a separate worker task to serve the newly minted `Iterator`
                    // protocol instance, allowing this call to return immediately and continue the
                    // request stream with other work.
                    responder
                        .send(iterate(store.clone(), starting_at, iterator.into_stream()?))
                        .context("error sending reply")?;
                    println!("Iterate response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

通訊協定

FIDL 方案:通訊協定

「通訊協定」說明瞭可透過管道傳送訊息的一組方法。其說明在用戶端和伺服器之間交換 FIDL 資料時使用的介面。

在此範例中,您將建立基本的計算機伺服器和用戶端,用於顯示先定義和提供和使用 FIDL 通訊協定所需的基本設定。

首先,請定義介面定義和測試控管工具。介面定義 (.fidl 檔案本身) 是任何新 FIDL 通訊協定的起點。此外,計算機還包含必要的 CML 和領域定義,以建立用戶端伺服器模式,這個模式可以做為專案鷹架,用於任意實作。

請參閱下列 FIDL 代碼:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// The namespace for this FIDL protocol. This namespace is how both consumers (clients) and providers (servers) reference this protocol.
library examples.calculator.baseline;

// @discoverable indicates 'Calculator' is a protocol that will be served under the examples.calculator.baseline libarary namespace. https://fuchsia.dev/fuchsia-src/reference/fidl/language/attributes#discoverable . If @discoverable is missing, it will lead to a compile time error when trying to import the library.
@discoverable
// A limited-functionality calculator 'protocol' that adds and subtracts integers.
open protocol Calculator {
    // Takes as input a struct with two integers, and returns their sum: (a+b)=sum.  This method is infallible (no errors can be generated) as two int32's cannot overflow a result type of int64.
    flexible Add(struct {
        a int32;
        b int32;
    }) -> (struct {
        sum int64;
    });
    // Takes as input a struct with two integers, and returns their difference: (a-b)=difference.  This method is infallible (no errors can be generated) as two int32's cannot overflow a result type of int64.
    flexible Subtract(struct {
        a int32;
        b int32;
    }) -> (struct {
        difference int64;
    });
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.calculator.baseline.Calculator" },
    ],
    config: {},
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.calculator.baseline.Calculator" },
    ],
    expose: [
        {
            protocol: "examples.calculator.baseline.Calculator",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.calculator.baseline.Calculator",
            from: "#server",
            to: "#client",
        },

        // Route logging support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// TODO(https://fxbug.dev/42063075): Rust implementation.

伺服器

// TODO(https://fxbug.dev/42063075): Rust implementation.

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42063075): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42063075): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42063075): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42063075): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42063075): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42063075): HLCPP implementation.

如本例所示,從頭開始建立 FIDL 通訊協定是平台開發人員等特定開發人員的常見情境。不過,其他類型的開發人員也受益於瞭解如何建構 FIDL 通訊協定,即使一般不如此也是如此。這可協助您瞭解 FIDL 的所有資訊如何相輔相成,包括語法、文法、語言功能、如何提供和使用指定的 FIDL 通訊協定,以及建構系統的運作方式。在後續步驟中,遵循此基準的範例會顯示如何擴充現有的 FIDL 通訊協定,這是相當常見的做法。

遞迴類型

FIDL 方案:遞迴類型

遞迴類型是指在定義中連帶參照的類型。當類型直接參照本身,或參照其他會連帶納入的類型鏈結時,就有可能會發生這種情況。舉例來說,在傳統版樹狀結構資料結構中,每個節點只能包含資料 (「分葉」) 或更多子節點 (「分支」) 的資料和參照。以第二種情況來說,節點遞迴包含巢狀樹狀結構,可視需要重複上述步驟來提供深度。

FIDL 支援遞迴類型,前提是包含週期中至少有一個鏈結 (也就是導向原始類型的類型定義鏈結) 為選用。如果鏈結中沒有任何類型為選用項目,就無法使用該類型,因為該類型的每個例項至少需要一個類型:ad Infinitum。

在這個變數中,我們允許鍵/值存放區將其他鍵/值存放區視為成員。簡單來說,我們把它變成一個樹狀結構。具體做法是將 value 的原始定義替換成使用兩成員 union 的變數:一個變化版本會儲存與先前相同 vector<byte> 類型的分葉節點,另一個變數則會以其他巢狀儲存庫的形式儲存分支節點。

推理

在這裡,我們來看看幾個「選用性」用法,藉此宣告可能存在或不存在的類型。FIDL 中有三種選用變種:

  • 類型一律儲存在線的類型,因此具有透過 null 信封描述「缺失」的內建方法。啟用這些類型的選用功能不會影響訊息的傳輸形狀,只會變更該特定類型有效值。透過新增 :optional 限制,unionvector<T>client_endserver_endzx.Handle 類型都可以設為選用。將 value union 設為選用項目,我們就能以缺少 value 的形式,引入標準「空值」項目。這表示空白的 bytes 以及缺少/空白的 store 屬性是無效的值。
  • 與上述類型不同,struct 版面配置並無額外空間,可儲存空值標頭。因此,郵件必須納入信封中,藉此變更所納入訊息的傳輸模式形狀。為確保此線路修改效果清晰易讀,Item struct 類型必須納入 box<T> 類型範本中。
  • 最後,table 版面配置一律為選用項目。缺少的 table 只是沒有任何成員設定的項目。

樹狀結構是一種自然的自參照資料結構,樹狀結構中的任何節點可能包含包含純資料的分葉 (在本例中為字串),或含有更多節點的子樹狀結構。這需要使用遞迴:Item 的定義現在間接依附於本身!在 FIDL 中代表遞迴類型可能有點困難,尤其是因為目前支援有部分限制。只要自我參照建立的週期中至少有一個選用類型,我們就能支援這類類型。舉例來說,這裡我們將 items struct 成員定義為 box<Item>,進而破壞包含週期。

這些變更也會大量使用「匿名類型」,或宣告內嵌於其唯一使用點,而非自行命名的頂層 type 宣告類型。根據預設,在產生的語言繫結中,匿名類型名稱取自本機背景資訊。舉例來說,新推出的 flexible union 會使用其本身成員的名稱 Value,新引入的 struct 會變成 Store,以此類推。由於此經驗法則有時可能會造成衝突,因此 FIDL 允許作者手動覆寫匿名類型的「產生的名稱」,提供一個跳躍點。方法是透過 @generated_name 屬性變更後端產生的名稱。我們可以在這裡使用一個,其中預計的 Store 類型已重新命名為 NestedStore,以避免與 protocol 宣告使用相同名稱的名稱衝突。

實作

FIDL、CML 和領域介面的定義修改如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.supporttrees;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value strict union {
        // Keep the original `bytes` as one of the options in the new union. All leaf nodes in the
        // tree must be `bytes`, or absent unions (representing empty). Empty byte arrays are
        // disallowed.
        1: bytes vector<byte>:64000;

        // Allows a store within a store, thereby turning our flat key-value store into a tree
        // thereof. Note the use of `@generated_name` to prevent a type-name collision with the
        // `Store` protocol below, and the use of `box<T>` to ensure that there is a break in the
        // chain of recursion, thereby allowing `Item` to include itself in its own definition.
        //
        // This is a table so that added fields, like for example a `hash`, can be easily added in
        // the future.
        2: store @generated_name("nested_store") table {
            1: items vector<box<Item>>;
        };
    }:optional;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// A very basic key-value store.
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.supporttrees.Store" },
    ],
    config: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // A newline separated list nested entries. The first line should be the key
        // for the nested store, and each subsequent entry should be a pointer to a text file
        // containing the string value. The name of that text file (without the `.txt` suffix) will
        // serve as the entries key.
        write_nested: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // A list of keys, all of which will be populated as null entries.
        write_null: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.supporttrees.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.supporttrees.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.supporttrees.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fidl_examples_keyvaluestore_supporttrees::{Item, NestedStore, StoreMarker, Value},
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        let res = store
            .write_item(&Item {
                key: key.clone(),
                value: Some(Box::new(Value::Bytes(value.into_bytes()))),
            })
            .await;
        match res? {
            Ok(_) => println!("WriteItem Success at key: {}", key),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // Add nested entries to the key-value store as well. The entries are strings, where the first
    // line is the key of the entry, and each subsequent entry should be a pointer to a text file
    // containing the string value. The name of that text file (without the `.txt` suffix) will
    // serve as the entries key.
    for spec in config.write_nested.into_iter() {
        let mut items = vec![];
        let mut nested_store = NestedStore::default();
        let mut lines = spec.split("\n");
        let key = lines.next().unwrap();

        // For each entry, make a new entry in the `NestedStore` being built.
        for entry in lines {
            let path = format!("/pkg/data/{}.txt", entry);
            let contents = std::fs::read_to_string(path.clone())
                .with_context(|| format!("Failed to load {path}"))?;
            items.push(Some(Box::new(Item {
                key: entry.to_string(),
                value: Some(Box::new(Value::Bytes(contents.into()))),
            })));
        }
        nested_store.items = Some(items);

        // Send the `NestedStore`, represented as a vector of values.
        let res = store
            .write_item(&Item {
                key: key.to_string(),
                value: Some(Box::new(Value::Store(nested_store))),
            })
            .await;
        match res? {
            Ok(_) => println!("WriteItem Success at key: {}", key),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // Each entry in this list is a null value in the store.
    for key in config.write_null.into_iter() {
        match store.write_item(&Item { key: key.to_string(), value: None }).await? {
            Ok(_) => println!("WriteItem Success at key: {}", key),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fidl_examples_keyvaluestore_supporttrees::{
        Item, StoreRequest, StoreRequestStream, Value, WriteError,
    },
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
    std::cell::RefCell,
    std::collections::hash_map::Entry,
    std::collections::HashMap,
    std::str::from_utf8,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile");
}

// A representation of a key-value store that can contain an arbitrarily deep nesting of other
// key-value stores.
#[allow(dead_code)] // TODO(https://fxbug.dev/318827209)
enum StoreNode {
    Leaf(Option<Vec<u8>>),
    Branch(Box<HashMap<String, StoreNode>>),
}

/// Recursive item writer, which takes a `StoreNode` that may not necessarily be the root node, and
/// writes an entry to it.
fn write_item(
    store: &mut HashMap<String, StoreNode>,
    attempt: Item,
    path: &str,
) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            let key = format!("{}{}", &path, entry.key());
            match attempt.value {
                // Null entries are allowed.
                None => {
                    println!("Wrote value: NONE at key: {}", key);
                    entry.insert(StoreNode::Leaf(None));
                }
                Some(value) => match *value {
                    // If this is a nested store, recursively make a new store to insert at this
                    // position.
                    Value::Store(entry_list) => {
                        // Validate the value - absent stores, items lists with no children, or any
                        // of the elements within that list being empty boxes, are all not allowed.
                        if entry_list.items.is_some() {
                            let items = entry_list.items.unwrap();
                            if !items.is_empty() && items.iter().all(|i| i.is_some()) {
                                let nested_path = format!("{}/", key);
                                let mut nested_store = HashMap::<String, StoreNode>::new();
                                for item in items.into_iter() {
                                    write_item(&mut nested_store, *item.unwrap(), &nested_path)?;
                                }

                                println!("Created branch at key: {}", key);
                                entry.insert(StoreNode::Branch(Box::new(nested_store)));
                                return Ok(());
                            }
                        }

                        println!("Write error: INVALID_VALUE, For key: {}", key);
                        return Err(WriteError::InvalidValue);
                    }

                    // This is a simple leaf node on this branch.
                    Value::Bytes(value) => {
                        // Validate the value.
                        if value.is_empty() {
                            println!("Write error: INVALID_VALUE, For key: {}", key);
                            return Err(WriteError::InvalidValue);
                        }

                        println!("Wrote key: {}, value: {:?}", key, from_utf8(&value).unwrap());
                        entry.insert(StoreNode::Leaf(Some(value)));
                    }
                },
            }
            Ok(())
        }
    }
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    let store = RefCell::new(HashMap::<String, StoreNode>::new());

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.borrow_mut(), attempt, ""))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

資源類型

FIDL 方案:資源類型

FIDL「資源類型」是一種可間接攜帶控點的類型。FIDL 控制代碼是功能的專屬參照,因此任何含有一項功能的類型都會繼承這個行為:也無法複製。透過這種方式,資源會帶有感染性:如果值類型變成資源類型,所有連帶納入該值的類型也必須執行。

對鍵/值存放區來說是按順序疊代作業的實用作業,也就是在提供鍵時,依序傳回顯示在其後方的元素清單 (通常是分頁)。

推理

在 FIDL 中,這樣做最好使用疊代器。疊代器通常會以獨立通訊協定的形式實作,此疊代作業可進行此疊代。使用獨立的通訊協定 (因此使用獨立管道) 有許多好處,包括將疊代提取要求從主要通訊協定完成的其他作業中解除交錯。

通訊協定 P 管道連線的用戶端和伺服器端,可分別以 client_end:Pserver_end:P 表示,以 FIDL 資料類型表示。這些類型統稱為「通訊協定結束」,代表透過現有的 FIDL 連線,將 FIDL 用戶端連線至其對應伺服器的其他 (非 @discoverable) 方式!

通訊協定結尾是一般 FIDL 概念的特定執行個體:資源類型。資源類型包含 FIDL 控制代碼,因此對於該類型的使用方式會有額外限制。類型一律不得重複,因為基礎資源會由其他能力管理員 (通常是 Zircon 核心) 中介。在不涉及管理程式的情況下,透過簡單的記憶體內副本複製此類資源是不可能的。為避免這類重複情形,FIDL 中的所有資源類型一律僅限移動。

最後,Iterator 通訊協定的 Get() 方法本身會在傳回酬載上使用大小限制。這樣可限制單次提取可傳輸的資料量,因此可某些測量資源用量控管機制。這也會建立自然的分頁界線:伺服器只需一次準備小型批次,而不是一次列出所有結果的大型傾印。

實作

FIDL、CML 和領域介面的定義如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.additerator;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value vector<byte>:64000;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// An enumeration of things that may go wrong when trying to create an iterator.
type IterateConnectionError = flexible enum {
    /// The starting key was not found.
    UNKNOWN_START_AT = 1;
};

/// A key-value store which supports insertion and iteration.
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;

    /// Iterates over the items in the store, using lexicographic ordering over the keys.
    ///
    /// The [`iterator`] is [pipelined][pipelining] to the server, such that the client can
    /// immediately send requests over the new connection.
    ///
    /// [pipelining]: https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#request-pipelining
    flexible Iterate(resource struct {
        /// If present, requests to start the iteration at this item.
        starting_at string:<128, optional>;

        /// The [`Iterator`] server endpoint. The client creates both ends of the channel and
        /// retains the `client_end` locally to use for pulling iteration pages, while sending the
        /// `server_end` off to be fulfilled by the server.
        iterator server_end:Iterator;
    }) -> () error IterateConnectionError;
};

/// An iterator for the key-value store. Note that this protocol makes no guarantee of atomicity -
/// the values may change between pulls from the iterator. Unlike the `Store` protocol above, this
/// protocol is not `@discoverable`: it is not independently published by the component that
/// implements it, but rather must have one of its two protocol ends transmitted over an existing
/// FIDL connection.
///
/// As is often the case with iterators, the client indicates that they are done with an instance of
/// the iterator by simply closing their end of the connection.
///
/// Since the iterator is associated only with the Iterate method, it is declared as closed rather
/// than open. This is because changes to how iteration works are more likely to require replacing
/// the Iterate method completely (which is fine because that method is flexible) rather than
/// evolving the Iterator protocol.
closed protocol Iterator {
    /// Gets the next batch of keys.
    ///
    /// The client pulls keys rather than having the server proactively push them, to implement
    /// [flow control][flow-control] over the messages.
    ///
    /// [flow-control]:
    ///     https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#prefer_pull_to_push
    strict Get() -> (struct {
        /// A list of keys. If the iterator has reached the end of iteration, the list will be
        /// empty. The client is expected to then close the connection.
        entries vector<string:128>:10;
    });
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.additerator.Store" },
    ],
    config: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // A key to iterate from, after all items in `write_items` have been written.
        iterate_from: {
            type: "string",
            max_size: 64,
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.additerator.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.additerator.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.additerator.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

use {
    fidl::endpoints::create_proxy,
    fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker},
    futures::join,
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        match store.write_item(&Item { key: key, value: value.into_bytes() }).await? {
            Ok(_) => println!("WriteItem Success"),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    if !config.iterate_from.is_empty() {
        // This helper creates a channel, and returns two protocol ends: the `client_end` is already
        // conveniently bound to the correct FIDL protocol, `Iterator`, while the `server_end` is
        // unbound and ready to be sent over the wire.
        let (iterator, server_end) = create_proxy::<IteratorMarker>()?;

        // There is no need to wait for the iterator to connect before sending the first `Get()`
        // request - since we already hold the `client_end` of the connection, we can start queuing
        // requests on it immediately.
        let connect_to_iterator = store.iterate(Some(config.iterate_from.as_str()), server_end);
        let first_get = iterator.get();

        // Wait until both the connection and the first request resolve - an error in either case
        // triggers an immediate resolution of the combined future.
        let (connection, first_page) = join!(connect_to_iterator, first_get);

        // Handle any connection error. If this has occurred, it is impossible for the first `Get()`
        // call to have resolved successfully, so check this error first.
        if let Err(err) = connection.context("Could not connect to Iterator")? {
            println!("Iterator Connection Error: {}", err.into_primitive());
        } else {
            println!("Iterator Connection Success");

            // Consecutively repeat the `Get()` request if the previous response was not empty.
            let mut entries = first_page.context("Could not get page from Iterator")?;
            while !&entries.is_empty() {
                for entry in entries.iter() {
                    println!("Iterator Entry: {}", entry);
                }
                entries = iterator.get().await.context("Could not get page from Iterator")?;
            }
        }
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
};

use {
    fidl_examples_keyvaluestore_additerator::{
        Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest,
        StoreRequestStream, WriteError,
    },
    fuchsia_async as fasync,
    std::collections::btree_map::Entry,
    std::collections::BTreeMap,
    std::ops::Bound::*,
    std::sync::Arc,
    std::sync::Mutex,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile");
}

/// Handler for the `WriteItem` method.
fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Validate the value.
    if attempt.value.is_empty() {
        println!("Write error: INVALID_VALUE, For key: {}", attempt.key);
        return Err(WriteError::InvalidValue);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote value at key: {}", entry.key());
            entry.insert(attempt.value);
            Ok(())
        }
    }
}

/// Handler for the `Iterate` method, which deals with validating that the requested start position
/// exists, and then sets up the asynchronous side channel for the actual iteration to occur over.
fn iterate(
    store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>,
    starting_at: Option<String>,
    stream: IteratorRequestStream,
) -> Result<(), IterateConnectionError> {
    // Validate that the starting key, if supplied, actually exists.
    if let Some(start_key) = starting_at.clone() {
        if !store.lock().unwrap().contains_key(&start_key) {
            return Err(IterateConnectionError::UnknownStartAt);
        }
    }

    // Spawn a detached task. This allows the method call to return while the iteration continues in
    // a separate, unawaited task.
    fasync::Task::spawn(async move {
        // Serve the iteration requests. Note that access to the underlying store is behind a
        // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift,
        // change, or disappear entirely between `Get()` requests.
        stream
            .map(|result| result.context("failed request"))
            .try_fold(
                match starting_at {
                    Some(start_key) => Included(start_key),
                    None => Unbounded,
                },
                |mut lower_bound, request| async {
                    match request {
                        IteratorRequest::Get { responder } => {
                            println!("Iterator page request received");

                            // The `page_size` should be kept in sync with the size constraint on
                            // the iterator's response, as defined in the FIDL protocol.
                            static PAGE_SIZE: usize = 10;

                            // An iterator, beginning at `lower_bound` and tracking the pagination's
                            // progress through iteration as each page is pulled by a client-sent
                            // `Get()` request.
                            let held_store = store.lock().unwrap();
                            let mut entries = held_store.range((lower_bound.clone(), Unbounded));
                            let mut current_page = vec![];
                            for _ in 0..PAGE_SIZE {
                                match entries.next() {
                                    Some(entry) => {
                                        current_page.push(entry.0.clone());
                                    }
                                    None => break,
                                }
                            }

                            // Update the `lower_bound` - either inclusive of the next item in the
                            // iteration, or exclusive of the last seen item if the iteration has
                            // finished. This `lower_bound` will be passed to the next request
                            // handler as its starting point.
                            lower_bound = match entries.next() {
                                Some(next) => Included(next.0.clone()),
                                None => match current_page.last() {
                                    Some(tail) => Excluded(tail.clone()),
                                    None => lower_bound,
                                },
                            };

                            // Send the page. At the end of this scope, the `held_store` lock gets
                            // dropped, and therefore released.
                            responder.send(&current_page).context("error sending reply")?;
                            println!("Iterator page sent");
                        }
                    }
                    Ok(lower_bound)
                },
            )
            .await
            .ok();
    })
    .detach();

    Ok(())
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    //
    // Note that we now use an `Arc<Mutex<BTreeMap>>`, replacing the previous `RefCell<HashMap>`.
    // The `BTreeMap` is used because we want an ordered map, to better facilitate iteration. The
    // `Arc<Mutex<...>>` is used because there are now multiple async tasks accessing the: one main
    // task which handles communication over the protocol, and one additional task per iterator
    // protocol. `Arc<Mutex<...>>` is the simplest way to synchronize concurrent access between
    // these racing tasks.
    let store = &Arc::new(Mutex::new(BTreeMap::<String, Vec<u8>>::new()));

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.clone().lock().unwrap(), attempt))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::Iterate { starting_at, iterator, responder } => {
                    println!("Iterate request received");

                    // The `iterate` handler does a quick check to see that the request is valid,
                    // then spins up a separate worker task to serve the newly minted `Iterator`
                    // protocol instance, allowing this call to return immediately and continue the
                    // request stream with other work.
                    responder
                        .send(iterate(store.clone(), starting_at, iterator.into_stream()?))
                        .context("error sending reply")?;
                    println!("Iterate response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

純量類型

FIDL 方案:純量類型

FIDL 的「純量類型」是一種類型,其中包含所有內建基元,再加上內建的非原始 string 類型。

推理

鍵/值儲存庫基準範例實作是很好的起點,但主要缺點是將資料儲存為原始位元組。FIDL 是一種多重輸入的語言。強制將 UTF-8 字串儲存為無類型位元組陣列的資料,會為 *.fidl 檔案讀取器以及使用從這個檔案產生的繫結的程式設計人員清除這項寶貴類型資訊。

實作

這項變更的主要目標是將基準案例的 vector<byte> 類型 value 成員替換成儲存許多可能類型的 union。事實上,此變更是針對 FIDL 的類型進行的良好調查:

  • 所有 FIDL 內建的純量類型都會做為 Value union 中的變體使用:booluint8uint16uint32uint64int8int16int32int64float32float64 (也稱為 FIDL 的原始類型),以及 。string
  • 這個 union 也使用 FIDL 的內建 array<T, N>vector<T> 類型範本。
  • 所有 FIDL 的類型版面配置 (包括 bitsenumtableunionstruct) 至少都在此範例中使用一次。

WriteItem 使用的要求和回應酬載也已從 struct 變更為命名的 table 和內嵌的 flexible union。事實上,這三種版面配置中的任一個,都可以使用要求/回應酬載。除了訊息大小最敏感的情況下,系統會分別使用後者 (分別稱為「資料表酬載」和 *聯集酬載)。這是因為日後能以與二進位檔相容的方式加以擴充,方便日後擴充。

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.usegenericvalues;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value Value;
};

// Because the `Value` must be used both in the request and the response, we give it its own named
// type. The type is a `union` of all possible data types that we take as values, and is marked
// `flexible` to allow for the easy addition of new data types in the future.
type Value = flexible union {
    // Keep the original `bytes` as one of the options in the new union.
    1: bytes vector<byte>:64000;

    // A `string` is very similar to `vector<byte>` on the wire, with the extra constraint that
    // it enforces that it enforces that the byte vector in question is valid UTF-8.
    2: string string:64000;

    // All of FIDL's primitive types.
    3: bool bool;
    4: uint8 uint8;
    5: int8 int8;
    6: uint16 uint16;
    7: int16 int16;
    8: uint32 uint32;
    9: int32 int32;
   10: float32 float32;
   11: uint64 uint64;
   12: int64 int64;
   13: float64 float64;

    // FIDL does not natively support 128-bit integer types, so we have to define our own
    // representations.
   14: uint128 array<uint64, 2>;
};

// Because we now supoprt a richer range of types as values in our store, it is helpful to use a
// `flexible`, and therefore evolvable, `bits` type to store write options.
type WriteOptions = flexible bits : uint8 {
    // This flag allows us to overwrite existing data when there is a collision, rather than failing
    // with an `WriteError.ALREADY_EXISTS`.
    OVERWRITE = 0b1;
    // This flag allows us to concatenate to existing data when there is a collision, rather than
    // failing with an `WriteError.ALREADY_EXISTS`. "Concatenation" means addition for the numeric
    // variants and appending to the `bytes`/`string` variants. If no existing data can be found, we
    // "concatenate" to default values of zero and an empty vector, respectively. Attempting to
    // concatenate to an existing variant of a different type will return a
    // `WriteError.INVALID_VALUE` error.
    CONCAT = 0b10;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// A very basic key-value store.
@discoverable
open protocol Store {
    /// Writes an item to the store.
    ///
    /// Since the value stored in the key-value store can now be different from the input (if the
    /// `WriteOptions.CONCAT` flag is set), we need to return the resulting `Value` to the
    /// requester.
    ///
    /// We use an (anonymous) `table` and a (named) `flexible union` as the request and response
    /// payload, respectively, to allow for easier future evolution. Both of these types are
    /// `flexible`, meaning that adding or removing members is binary-compatible. This makes them
    /// much easier to evolve that the `struct` types that were previously used, which cannot be
    /// changed after release without breaking ABI.
    flexible WriteItem(table {
        1: attempt Item;
        2: options WriteOptions;
    }) -> (Value) error WriteError;
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.usegenericvalues.Store" },
    ],
    config: {
        // A vector of values for every easily representible type in our key-value store. For
        // brevity's sake, the 8, 16, and 32 bit integer types and booleans are omitted.
        //
        // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great
        //   deal of string parsing if we were able to use all FIDL constructs directly here. In
        //   particular, floats and nested types are very difficult to represent, and have been
        //   excluded from this example for the time being.
        set_concat_option: { type: "bool" },
        set_overwrite_option: { type: "bool" },
        write_bytes: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },
        write_strings: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },
        write_uint64s: {
            type: "vector",
            max_count: 16,
            element: { type: "uint64" },
        },
        write_int64s: {
            type: "vector",
            max_count: 16,
            element: { type: "int64" },
        },

        // Note: due to the limitation of structured config not allowing vectors nested in vectors,
        // we only set the lower half of the uint128 for simplicity's sake.
        write_uint128s: {
            type: "vector",
            max_count: 16,
            element: { type: "uint64" },
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.usegenericvalues.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.usegenericvalues.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.usegenericvalues.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fidl_examples_keyvaluestore_usegenericvalues::{
        Item, StoreMarker, StoreProxy, StoreWriteItemRequest, Value, WriteOptions,
    },
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

// A helper function to sequentially write a single item to the key-value store and print a log when
// successful.
async fn write_next_item(
    store: &StoreProxy,
    key: &str,
    value: Value,
    options: WriteOptions,
) -> Result<(), Error> {
    // Create an empty request payload using `::default()`.
    let mut req = StoreWriteItemRequest::default();
    req.options = Some(options);

    // Fill in the `Item` we will be attempting to write.
    println!("WriteItem request sent: key: {}, value: {:?}", &key, &value);
    req.attempt = Some(Item { key: key.to_string(), value: value });

    // Send and async `WriteItem` request to the server.
    match store.write_item(&req).await.context("Error sending request")? {
        Ok(value) => println!("WriteItem response received: {:?}", &value),
        Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
    }
    Ok(())
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // All of our requests will have the same bitflags set. Pull these settings from the config.
    let mut options = WriteOptions::empty();
    options.set(WriteOptions::OVERWRITE, config.set_overwrite_option);
    options.set(WriteOptions::CONCAT, config.set_concat_option);

    // The structured config provides one input for most data types that can be stored in the data
    // store. Iterate through those inputs in the order we see them in the FIDL file.
    //
    // Note that FIDL unions are rendered as enums in Rust; for example, the `Value` union has now
    // become a `Value` Rust enum, with each member taking exactly one argument.
    for value in config.write_bytes.into_iter() {
        write_next_item(&store, "bytes", Value::Bytes(value.into()), options).await?;
    }
    for value in config.write_strings.into_iter() {
        write_next_item(&store, "string", Value::String(value), options).await?;
    }
    for value in config.write_uint64s.into_iter() {
        write_next_item(&store, "uint64", Value::Uint64(value), options).await?;
    }
    for value in config.write_int64s.into_iter() {
        write_next_item(&store, "int64", Value::Int64(value), options).await?;
    }
    for value in config.write_uint128s.into_iter() {
        write_next_item(&store, "uint128", Value::Uint128([0, value]), options).await?;
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
    std::cell::RefCell,
    std::collections::hash_map::Entry,
    std::collections::HashMap,
};

use {
    fidl_examples_keyvaluestore_usegenericvalues::{
        Item, StoreRequest, StoreRequestStream, Value, WriteError, WriteOptions,
    },
    std::collections::hash_map::OccupiedEntry,
    std::ops::Add,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile");
}

/// Sums any numeric type.
fn sum<T: Add + Add<Output = T> + Copy>(operands: [T; 2]) -> T {
    operands[0] + operands[1]
}

/// Clones and inserts an entry, so that the original (now concatenated) copy may be returned in the
/// response.
fn write(inserting: Value, mut entry: OccupiedEntry<'_, String, Value>) -> Value {
    entry.insert(inserting.clone());
    println!("Wrote key: {}, value: {:?}", entry.key(), &inserting);
    inserting
}

/// Handler for the `WriteItem` method.
fn write_item(
    store: &mut HashMap<String, Value>,
    attempt: Item,
    options: &WriteOptions,
) -> Result<Value, WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY for key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            // The `CONCAT` flag supersedes the `OVERWRITE` flag, so check it first.
            if options.contains(WriteOptions::CONCAT) {
                match entry.get() {
                    Value::Bytes(old) => {
                        if let Value::Bytes(new) = attempt.value {
                            let mut combined = old.clone();
                            combined.extend(new);
                            return Ok(write(Value::Bytes(combined), entry));
                        }
                    }
                    Value::String(old) => {
                        if let Value::String(new) = attempt.value {
                            return Ok(write(Value::String(format!("{}{}", old, &new)), entry));
                        }
                    }
                    Value::Uint64(old) => {
                        if let Value::Uint64(new) = attempt.value {
                            return Ok(write(Value::Uint64(sum([*old, new])), entry));
                        }
                    }
                    Value::Int64(old) => {
                        if let Value::Int64(new) = attempt.value {
                            return Ok(write(Value::Int64(sum([*old, new])), entry));
                        }
                    }
                    // Note: only works on the uint64 range in practice.
                    Value::Uint128(old) => {
                        if let Value::Uint128(new) = attempt.value {
                            return Ok(write(Value::Uint128([0, sum([old[1], new[1]])]), entry));
                        }
                    }
                    _ => {
                        panic!("actively unsupported type!")
                    }
                }

                // Only reachable if the type of the would be concatenated value did not match the
                // value already occupying this entry.
                println!("Write error: INVALID_VALUE for key: {}", entry.key());
                return Err(WriteError::InvalidValue);
            }

            // If we're not doing CONCAT, check for OVERWRITE next.
            if options.contains(WriteOptions::OVERWRITE) {
                return Ok(write(attempt.value, entry));
            }

            println!("Write error: ALREADY_EXISTS for key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote key: {}, value: {:?}", entry.key(), &attempt.value);
            entry.insert(attempt.value.clone());
            Ok(attempt.value)
        }
    }
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    let store = RefCell::new(HashMap::<String, Value>::new());

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                // Because we are using a table payload, there is an extra level of indirection. The
                // top-level container for the table itself is always called "payload".
                StoreRequest::WriteItem { payload, responder } => {
                    println!("WriteItem request received");

                    // Error out if either of the request table's members are not set.
                    let attempt = payload.attempt.context("required field 'attempt' is unset")?;
                    let options = payload.options.context("required field 'options' is unset")?;

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(
                            write_item(&mut store.borrow_mut(), attempt, &options)
                                .as_ref()
                                .map_err(|e| *e),
                        )
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

大小限制

FIDL 方案:大小限制

FIDL 向量和字串可以帶有「大小限制」,指定該類型可容納多少成員的限制。以向量來說,這是指儲存在向量中的元素數量,而對於字串而言,則是字串包含的「位元組數」

強烈建議您使用大小限制,因為這會為非受限的類型設定上限。

對鍵/值存放區來說是按順序疊代作業的實用作業,也就是在提供鍵時,依序傳回顯示在其後方的元素清單 (通常是分頁)。

推理

在 FIDL 中,這樣做最好使用疊代器。疊代器通常會以獨立通訊協定的形式實作,此疊代作業可進行此疊代。使用獨立的通訊協定 (因此使用獨立管道) 有許多好處,包括將疊代提取要求從主要通訊協定完成的其他作業中解除交錯。

通訊協定 P 管道連線的用戶端和伺服器端,可分別以 client_end:Pserver_end:P 表示,以 FIDL 資料類型表示。這些類型統稱為「通訊協定結束」,代表透過現有的 FIDL 連線,將 FIDL 用戶端連線至其對應伺服器的其他 (非 @discoverable) 方式!

通訊協定結尾是一般 FIDL 概念的特定執行個體:資源類型。資源類型包含 FIDL 控制代碼,因此對於該類型的使用方式會有額外限制。類型一律不得重複,因為基礎資源會由其他能力管理員 (通常是 Zircon 核心) 中介。在不涉及管理程式的情況下,透過簡單的記憶體內副本複製此類資源是不可能的。為避免這類重複情形,FIDL 中的所有資源類型一律僅限移動。

最後,Iterator 通訊協定的 Get() 方法本身會在傳回酬載上使用大小限制。這樣可限制單次提取可傳輸的資料量,因此可某些測量資源用量控管機制。這也會建立自然的分頁界線:伺服器只需一次準備小型批次,而不是一次列出所有結果的大型傾印。

實作

FIDL、CML 和領域介面的定義如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.additerator;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value vector<byte>:64000;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// An enumeration of things that may go wrong when trying to create an iterator.
type IterateConnectionError = flexible enum {
    /// The starting key was not found.
    UNKNOWN_START_AT = 1;
};

/// A key-value store which supports insertion and iteration.
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;

    /// Iterates over the items in the store, using lexicographic ordering over the keys.
    ///
    /// The [`iterator`] is [pipelined][pipelining] to the server, such that the client can
    /// immediately send requests over the new connection.
    ///
    /// [pipelining]: https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#request-pipelining
    flexible Iterate(resource struct {
        /// If present, requests to start the iteration at this item.
        starting_at string:<128, optional>;

        /// The [`Iterator`] server endpoint. The client creates both ends of the channel and
        /// retains the `client_end` locally to use for pulling iteration pages, while sending the
        /// `server_end` off to be fulfilled by the server.
        iterator server_end:Iterator;
    }) -> () error IterateConnectionError;
};

/// An iterator for the key-value store. Note that this protocol makes no guarantee of atomicity -
/// the values may change between pulls from the iterator. Unlike the `Store` protocol above, this
/// protocol is not `@discoverable`: it is not independently published by the component that
/// implements it, but rather must have one of its two protocol ends transmitted over an existing
/// FIDL connection.
///
/// As is often the case with iterators, the client indicates that they are done with an instance of
/// the iterator by simply closing their end of the connection.
///
/// Since the iterator is associated only with the Iterate method, it is declared as closed rather
/// than open. This is because changes to how iteration works are more likely to require replacing
/// the Iterate method completely (which is fine because that method is flexible) rather than
/// evolving the Iterator protocol.
closed protocol Iterator {
    /// Gets the next batch of keys.
    ///
    /// The client pulls keys rather than having the server proactively push them, to implement
    /// [flow control][flow-control] over the messages.
    ///
    /// [flow-control]:
    ///     https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#prefer_pull_to_push
    strict Get() -> (struct {
        /// A list of keys. If the iterator has reached the end of iteration, the list will be
        /// empty. The client is expected to then close the connection.
        entries vector<string:128>:10;
    });
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.additerator.Store" },
    ],
    config: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // A key to iterate from, after all items in `write_items` have been written.
        iterate_from: {
            type: "string",
            max_size: 64,
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.additerator.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.additerator.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.additerator.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

use {
    fidl::endpoints::create_proxy,
    fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker},
    futures::join,
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        match store.write_item(&Item { key: key, value: value.into_bytes() }).await? {
            Ok(_) => println!("WriteItem Success"),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    if !config.iterate_from.is_empty() {
        // This helper creates a channel, and returns two protocol ends: the `client_end` is already
        // conveniently bound to the correct FIDL protocol, `Iterator`, while the `server_end` is
        // unbound and ready to be sent over the wire.
        let (iterator, server_end) = create_proxy::<IteratorMarker>()?;

        // There is no need to wait for the iterator to connect before sending the first `Get()`
        // request - since we already hold the `client_end` of the connection, we can start queuing
        // requests on it immediately.
        let connect_to_iterator = store.iterate(Some(config.iterate_from.as_str()), server_end);
        let first_get = iterator.get();

        // Wait until both the connection and the first request resolve - an error in either case
        // triggers an immediate resolution of the combined future.
        let (connection, first_page) = join!(connect_to_iterator, first_get);

        // Handle any connection error. If this has occurred, it is impossible for the first `Get()`
        // call to have resolved successfully, so check this error first.
        if let Err(err) = connection.context("Could not connect to Iterator")? {
            println!("Iterator Connection Error: {}", err.into_primitive());
        } else {
            println!("Iterator Connection Success");

            // Consecutively repeat the `Get()` request if the previous response was not empty.
            let mut entries = first_page.context("Could not get page from Iterator")?;
            while !&entries.is_empty() {
                for entry in entries.iter() {
                    println!("Iterator Entry: {}", entry);
                }
                entries = iterator.get().await.context("Could not get page from Iterator")?;
            }
        }
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
};

use {
    fidl_examples_keyvaluestore_additerator::{
        Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest,
        StoreRequestStream, WriteError,
    },
    fuchsia_async as fasync,
    std::collections::btree_map::Entry,
    std::collections::BTreeMap,
    std::ops::Bound::*,
    std::sync::Arc,
    std::sync::Mutex,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile");
}

/// Handler for the `WriteItem` method.
fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Validate the value.
    if attempt.value.is_empty() {
        println!("Write error: INVALID_VALUE, For key: {}", attempt.key);
        return Err(WriteError::InvalidValue);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote value at key: {}", entry.key());
            entry.insert(attempt.value);
            Ok(())
        }
    }
}

/// Handler for the `Iterate` method, which deals with validating that the requested start position
/// exists, and then sets up the asynchronous side channel for the actual iteration to occur over.
fn iterate(
    store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>,
    starting_at: Option<String>,
    stream: IteratorRequestStream,
) -> Result<(), IterateConnectionError> {
    // Validate that the starting key, if supplied, actually exists.
    if let Some(start_key) = starting_at.clone() {
        if !store.lock().unwrap().contains_key(&start_key) {
            return Err(IterateConnectionError::UnknownStartAt);
        }
    }

    // Spawn a detached task. This allows the method call to return while the iteration continues in
    // a separate, unawaited task.
    fasync::Task::spawn(async move {
        // Serve the iteration requests. Note that access to the underlying store is behind a
        // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift,
        // change, or disappear entirely between `Get()` requests.
        stream
            .map(|result| result.context("failed request"))
            .try_fold(
                match starting_at {
                    Some(start_key) => Included(start_key),
                    None => Unbounded,
                },
                |mut lower_bound, request| async {
                    match request {
                        IteratorRequest::Get { responder } => {
                            println!("Iterator page request received");

                            // The `page_size` should be kept in sync with the size constraint on
                            // the iterator's response, as defined in the FIDL protocol.
                            static PAGE_SIZE: usize = 10;

                            // An iterator, beginning at `lower_bound` and tracking the pagination's
                            // progress through iteration as each page is pulled by a client-sent
                            // `Get()` request.
                            let held_store = store.lock().unwrap();
                            let mut entries = held_store.range((lower_bound.clone(), Unbounded));
                            let mut current_page = vec![];
                            for _ in 0..PAGE_SIZE {
                                match entries.next() {
                                    Some(entry) => {
                                        current_page.push(entry.0.clone());
                                    }
                                    None => break,
                                }
                            }

                            // Update the `lower_bound` - either inclusive of the next item in the
                            // iteration, or exclusive of the last seen item if the iteration has
                            // finished. This `lower_bound` will be passed to the next request
                            // handler as its starting point.
                            lower_bound = match entries.next() {
                                Some(next) => Included(next.0.clone()),
                                None => match current_page.last() {
                                    Some(tail) => Excluded(tail.clone()),
                                    None => lower_bound,
                                },
                            };

                            // Send the page. At the end of this scope, the `held_store` lock gets
                            // dropped, and therefore released.
                            responder.send(&current_page).context("error sending reply")?;
                            println!("Iterator page sent");
                        }
                    }
                    Ok(lower_bound)
                },
            )
            .await
            .ok();
    })
    .detach();

    Ok(())
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    //
    // Note that we now use an `Arc<Mutex<BTreeMap>>`, replacing the previous `RefCell<HashMap>`.
    // The `BTreeMap` is used because we want an ordered map, to better facilitate iteration. The
    // `Arc<Mutex<...>>` is used because there are now multiple async tasks accessing the: one main
    // task which handles communication over the protocol, and one additional task per iterator
    // protocol. `Arc<Mutex<...>>` is the simplest way to synchronize concurrent access between
    // these racing tasks.
    let store = &Arc::new(Mutex::new(BTreeMap::<String, Vec<u8>>::new()));

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.clone().lock().unwrap(), attempt))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::Iterate { starting_at, iterator, responder } => {
                    println!("Iterate request received");

                    // The `iterate` handler does a quick check to see that the request is valid,
                    // then spins up a separate worker task to serve the newly minted `Iterator`
                    // protocol instance, allowing this call to return immediately and continue the
                    // request stream with other work.
                    responder
                        .send(iterate(store.clone(), starting_at, iterator.into_stream()?))
                        .context("error sending reply")?;
                    println!("Iterate response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

結構酬載

FIDL 方案:結構酬載

結構酬載是使用 struct 版面配置的 FIDL 方法酬載。struct 是簡單的型別欄位序列,類似 C 結構體的運作方式。

在此範例中,您將建立基本的計算機伺服器和用戶端,用於顯示先定義和提供和使用 FIDL 通訊協定所需的基本設定。

首先,請定義介面定義和測試控管工具。介面定義 (.fidl 檔案本身) 是任何新 FIDL 通訊協定的起點。此外,計算機還包含必要的 CML 和領域定義,以建立用戶端伺服器模式,這個模式可以做為專案鷹架,用於任意實作。

請參閱下列 FIDL 代碼:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// The namespace for this FIDL protocol. This namespace is how both consumers (clients) and providers (servers) reference this protocol.
library examples.calculator.baseline;

// @discoverable indicates 'Calculator' is a protocol that will be served under the examples.calculator.baseline libarary namespace. https://fuchsia.dev/fuchsia-src/reference/fidl/language/attributes#discoverable . If @discoverable is missing, it will lead to a compile time error when trying to import the library.
@discoverable
// A limited-functionality calculator 'protocol' that adds and subtracts integers.
open protocol Calculator {
    // Takes as input a struct with two integers, and returns their sum: (a+b)=sum.  This method is infallible (no errors can be generated) as two int32's cannot overflow a result type of int64.
    flexible Add(struct {
        a int32;
        b int32;
    }) -> (struct {
        sum int64;
    });
    // Takes as input a struct with two integers, and returns their difference: (a-b)=difference.  This method is infallible (no errors can be generated) as two int32's cannot overflow a result type of int64.
    flexible Subtract(struct {
        a int32;
        b int32;
    }) -> (struct {
        difference int64;
    });
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.calculator.baseline.Calculator" },
    ],
    config: {},
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.calculator.baseline.Calculator" },
    ],
    expose: [
        {
            protocol: "examples.calculator.baseline.Calculator",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.calculator.baseline.Calculator",
            from: "#server",
            to: "#client",
        },

        // Route logging support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// TODO(https://fxbug.dev/42063075): Rust implementation.

伺服器

// TODO(https://fxbug.dev/42063075): Rust implementation.

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42063075): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42063075): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42063075): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42063075): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42063075): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42063075): HLCPP implementation.

如本例所示,從頭開始建立 FIDL 通訊協定是平台開發人員等特定開發人員的常見情境。不過,其他類型的開發人員也受益於瞭解如何建構 FIDL 通訊協定,即使一般不如此也是如此。這可協助您瞭解 FIDL 的所有資訊如何相輔相成,包括語法、文法、語言功能、如何提供和使用指定的 FIDL 通訊協定,以及建構系統的運作方式。在後續步驟中,遵循此基準的範例會顯示如何擴充現有的 FIDL 通訊協定,這是相當常見的做法。

資料表酬載

FIDL 方案:資料表酬載

資料表酬載是使用 table 版面配置的 FIDL 方法酬載。做為方法酬載使用的頂層類型必須使用 structtableunion 其中之一做為版面配置。值得注意的是,部分產生的繫結會「扁平」傳遞給 struct 方法酬載的引數,這樣每個成員都會在呼叫簽章中將其視為函式引數。使用 tableunion 的酬載則不會這麼做,且一律會傳遞名為 payload 的單一引數。

推理

鍵/值儲存庫基準範例實作是很好的起點,但主要缺點是將資料儲存為原始位元組。FIDL 是一種多重輸入的語言。強制將 UTF-8 字串儲存為無類型位元組陣列的資料,會為 *.fidl 檔案讀取器以及使用從這個檔案產生的繫結的程式設計人員清除這項寶貴類型資訊。

實作

這項變更的主要目標是將基準案例的 vector<byte> 類型 value 成員替換成儲存許多可能類型的 union。事實上,此變更是針對 FIDL 的類型進行的良好調查:

  • 所有 FIDL 內建的純量類型都會做為 Value union 中的變體使用:booluint8uint16uint32uint64int8int16int32int64float32float64 (也稱為 FIDL 的原始類型),以及 。string
  • 這個 union 也使用 FIDL 的內建 array<T, N>vector<T> 類型範本。
  • 所有 FIDL 的類型版面配置 (包括 bitsenumtableunionstruct) 至少都在此範例中使用一次。

WriteItem 使用的要求和回應酬載也已從 struct 變更為命名的 table 和內嵌的 flexible union。事實上,這三種版面配置中的任一個,都可以使用要求/回應酬載。除了訊息大小最敏感的情況下,系統會分別使用後者 (分別稱為「資料表酬載」和 *聯集酬載)。這是因為日後能以與二進位檔相容的方式加以擴充,方便日後擴充。

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.usegenericvalues;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value Value;
};

// Because the `Value` must be used both in the request and the response, we give it its own named
// type. The type is a `union` of all possible data types that we take as values, and is marked
// `flexible` to allow for the easy addition of new data types in the future.
type Value = flexible union {
    // Keep the original `bytes` as one of the options in the new union.
    1: bytes vector<byte>:64000;

    // A `string` is very similar to `vector<byte>` on the wire, with the extra constraint that
    // it enforces that it enforces that the byte vector in question is valid UTF-8.
    2: string string:64000;

    // All of FIDL's primitive types.
    3: bool bool;
    4: uint8 uint8;
    5: int8 int8;
    6: uint16 uint16;
    7: int16 int16;
    8: uint32 uint32;
    9: int32 int32;
   10: float32 float32;
   11: uint64 uint64;
   12: int64 int64;
   13: float64 float64;

    // FIDL does not natively support 128-bit integer types, so we have to define our own
    // representations.
   14: uint128 array<uint64, 2>;
};

// Because we now supoprt a richer range of types as values in our store, it is helpful to use a
// `flexible`, and therefore evolvable, `bits` type to store write options.
type WriteOptions = flexible bits : uint8 {
    // This flag allows us to overwrite existing data when there is a collision, rather than failing
    // with an `WriteError.ALREADY_EXISTS`.
    OVERWRITE = 0b1;
    // This flag allows us to concatenate to existing data when there is a collision, rather than
    // failing with an `WriteError.ALREADY_EXISTS`. "Concatenation" means addition for the numeric
    // variants and appending to the `bytes`/`string` variants. If no existing data can be found, we
    // "concatenate" to default values of zero and an empty vector, respectively. Attempting to
    // concatenate to an existing variant of a different type will return a
    // `WriteError.INVALID_VALUE` error.
    CONCAT = 0b10;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// A very basic key-value store.
@discoverable
open protocol Store {
    /// Writes an item to the store.
    ///
    /// Since the value stored in the key-value store can now be different from the input (if the
    /// `WriteOptions.CONCAT` flag is set), we need to return the resulting `Value` to the
    /// requester.
    ///
    /// We use an (anonymous) `table` and a (named) `flexible union` as the request and response
    /// payload, respectively, to allow for easier future evolution. Both of these types are
    /// `flexible`, meaning that adding or removing members is binary-compatible. This makes them
    /// much easier to evolve that the `struct` types that were previously used, which cannot be
    /// changed after release without breaking ABI.
    flexible WriteItem(table {
        1: attempt Item;
        2: options WriteOptions;
    }) -> (Value) error WriteError;
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.usegenericvalues.Store" },
    ],
    config: {
        // A vector of values for every easily representible type in our key-value store. For
        // brevity's sake, the 8, 16, and 32 bit integer types and booleans are omitted.
        //
        // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great
        //   deal of string parsing if we were able to use all FIDL constructs directly here. In
        //   particular, floats and nested types are very difficult to represent, and have been
        //   excluded from this example for the time being.
        set_concat_option: { type: "bool" },
        set_overwrite_option: { type: "bool" },
        write_bytes: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },
        write_strings: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },
        write_uint64s: {
            type: "vector",
            max_count: 16,
            element: { type: "uint64" },
        },
        write_int64s: {
            type: "vector",
            max_count: 16,
            element: { type: "int64" },
        },

        // Note: due to the limitation of structured config not allowing vectors nested in vectors,
        // we only set the lower half of the uint128 for simplicity's sake.
        write_uint128s: {
            type: "vector",
            max_count: 16,
            element: { type: "uint64" },
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.usegenericvalues.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.usegenericvalues.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.usegenericvalues.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fidl_examples_keyvaluestore_usegenericvalues::{
        Item, StoreMarker, StoreProxy, StoreWriteItemRequest, Value, WriteOptions,
    },
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

// A helper function to sequentially write a single item to the key-value store and print a log when
// successful.
async fn write_next_item(
    store: &StoreProxy,
    key: &str,
    value: Value,
    options: WriteOptions,
) -> Result<(), Error> {
    // Create an empty request payload using `::default()`.
    let mut req = StoreWriteItemRequest::default();
    req.options = Some(options);

    // Fill in the `Item` we will be attempting to write.
    println!("WriteItem request sent: key: {}, value: {:?}", &key, &value);
    req.attempt = Some(Item { key: key.to_string(), value: value });

    // Send and async `WriteItem` request to the server.
    match store.write_item(&req).await.context("Error sending request")? {
        Ok(value) => println!("WriteItem response received: {:?}", &value),
        Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
    }
    Ok(())
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // All of our requests will have the same bitflags set. Pull these settings from the config.
    let mut options = WriteOptions::empty();
    options.set(WriteOptions::OVERWRITE, config.set_overwrite_option);
    options.set(WriteOptions::CONCAT, config.set_concat_option);

    // The structured config provides one input for most data types that can be stored in the data
    // store. Iterate through those inputs in the order we see them in the FIDL file.
    //
    // Note that FIDL unions are rendered as enums in Rust; for example, the `Value` union has now
    // become a `Value` Rust enum, with each member taking exactly one argument.
    for value in config.write_bytes.into_iter() {
        write_next_item(&store, "bytes", Value::Bytes(value.into()), options).await?;
    }
    for value in config.write_strings.into_iter() {
        write_next_item(&store, "string", Value::String(value), options).await?;
    }
    for value in config.write_uint64s.into_iter() {
        write_next_item(&store, "uint64", Value::Uint64(value), options).await?;
    }
    for value in config.write_int64s.into_iter() {
        write_next_item(&store, "int64", Value::Int64(value), options).await?;
    }
    for value in config.write_uint128s.into_iter() {
        write_next_item(&store, "uint128", Value::Uint128([0, value]), options).await?;
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
    std::cell::RefCell,
    std::collections::hash_map::Entry,
    std::collections::HashMap,
};

use {
    fidl_examples_keyvaluestore_usegenericvalues::{
        Item, StoreRequest, StoreRequestStream, Value, WriteError, WriteOptions,
    },
    std::collections::hash_map::OccupiedEntry,
    std::ops::Add,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile");
}

/// Sums any numeric type.
fn sum<T: Add + Add<Output = T> + Copy>(operands: [T; 2]) -> T {
    operands[0] + operands[1]
}

/// Clones and inserts an entry, so that the original (now concatenated) copy may be returned in the
/// response.
fn write(inserting: Value, mut entry: OccupiedEntry<'_, String, Value>) -> Value {
    entry.insert(inserting.clone());
    println!("Wrote key: {}, value: {:?}", entry.key(), &inserting);
    inserting
}

/// Handler for the `WriteItem` method.
fn write_item(
    store: &mut HashMap<String, Value>,
    attempt: Item,
    options: &WriteOptions,
) -> Result<Value, WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY for key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            // The `CONCAT` flag supersedes the `OVERWRITE` flag, so check it first.
            if options.contains(WriteOptions::CONCAT) {
                match entry.get() {
                    Value::Bytes(old) => {
                        if let Value::Bytes(new) = attempt.value {
                            let mut combined = old.clone();
                            combined.extend(new);
                            return Ok(write(Value::Bytes(combined), entry));
                        }
                    }
                    Value::String(old) => {
                        if let Value::String(new) = attempt.value {
                            return Ok(write(Value::String(format!("{}{}", old, &new)), entry));
                        }
                    }
                    Value::Uint64(old) => {
                        if let Value::Uint64(new) = attempt.value {
                            return Ok(write(Value::Uint64(sum([*old, new])), entry));
                        }
                    }
                    Value::Int64(old) => {
                        if let Value::Int64(new) = attempt.value {
                            return Ok(write(Value::Int64(sum([*old, new])), entry));
                        }
                    }
                    // Note: only works on the uint64 range in practice.
                    Value::Uint128(old) => {
                        if let Value::Uint128(new) = attempt.value {
                            return Ok(write(Value::Uint128([0, sum([old[1], new[1]])]), entry));
                        }
                    }
                    _ => {
                        panic!("actively unsupported type!")
                    }
                }

                // Only reachable if the type of the would be concatenated value did not match the
                // value already occupying this entry.
                println!("Write error: INVALID_VALUE for key: {}", entry.key());
                return Err(WriteError::InvalidValue);
            }

            // If we're not doing CONCAT, check for OVERWRITE next.
            if options.contains(WriteOptions::OVERWRITE) {
                return Ok(write(attempt.value, entry));
            }

            println!("Write error: ALREADY_EXISTS for key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote key: {}, value: {:?}", entry.key(), &attempt.value);
            entry.insert(attempt.value.clone());
            Ok(attempt.value)
        }
    }
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    let store = RefCell::new(HashMap::<String, Value>::new());

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                // Because we are using a table payload, there is an extra level of indirection. The
                // top-level container for the table itself is always called "payload".
                StoreRequest::WriteItem { payload, responder } => {
                    println!("WriteItem request received");

                    // Error out if either of the request table's members are not set.
                    let attempt = payload.attempt.context("required field 'attempt' is unset")?;
                    let options = payload.options.context("required field 'options' is unset")?;

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(
                            write_item(&mut store.borrow_mut(), attempt, &options)
                                .as_ref()
                                .map_err(|e| *e),
                        )
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

經過調節的事件模式

FIDL 方案:節流事件模式

事件是從伺服器啟動的 FIDL 呼叫。由於這些呼叫沒有內建的用戶端回應,因此不會受到控制:伺服器可能會將大量這類呼叫排入佇列,並對用戶端造成洪水。其中一個解決方法是節流事件模式。此模式涉及新增用戶端呼叫的 FIDL 方法,做為一或多個要同步處理的事件確認點。

在收到來自用戶端的下一個確認呼叫之前,伺服器應避免傳送更多受限事件 (這裡的確切語意對應到實作的通訊協定)。同樣地,如果伺服器傳送的事件數量超過用戶端確認允許的事件,用戶端就應關閉連線。這些限制並未內建於 FIDL 執行階段中,而用戶端/伺服器實作者必須手動進行一些實作,才能確保行為正確無誤。

改善 Instance 通訊協定效能的方法之一,就是允許批次處理:與其每次在畫布中加入新的 AddLine(...);,在等候回覆,並在下一行中再次執行該行時,只要將多行資料批次處理成單一叫用來發出新的 AddLines(...); 呼叫即可。用戶端現在可以決定如何妥善區隔大量線條。

在一般情況下,我們會發現在伺服器和用戶端完全未同步的情況下:用戶端可以使用無限制的 AddLines(...); 呼叫在伺服器中淹水,而伺服器也可能以類似的方式,以比伺服器可處理的更多 -> OnDrawn(...); 事件大量地淹沒用戶端。這兩個問題的解決方法是新增簡單的 Ready() -> (); 方法,以便同步處理。每當用戶端準備好接收下一個繪圖更新時,就會呼叫此方法,從伺服器的回應中表示用戶端可以繼續處理更多要求。

我們現在對雙向的流量控製做了一些控制。通訊協定現在會導入動態饋給轉送模式,以便在某些同步處理「修訂」呼叫之前,允許許多未控制的呼叫,進而觸發伺服器上的實際工作。這樣可避免用戶端對伺服器造成過多工作負荷。同樣地,伺服器也無法再傳送未受限的 -> OnDrawn(...); 事件:每個事件都必須遵循來自用戶端的信號 (Ready() -> (); 呼叫),表示它已準備好執行更多工作。這就是所謂的節流事件模式

具體的實作必須手動套用以下部分規則:如果用戶端收到未透過 Ready() -> (); 方法要求的 -> OnDrawn(...); 事件,就必須關閉連線。

FIDL、CML 和領域介面的定義如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.canvas.clientrequesteddraw;

/// A point in 2D space.
type Point = struct {
    x int64;
    y int64;
};

/// A line in 2D space.
alias Line = array<Point, 2>;

/// A bounding box in 2D space. This is the result of "drawing" operations on our canvas, and what
/// the server reports back to the client. These bounds are sufficient to contain all of the
/// lines (inclusive) on a canvas at a given time.
type BoundingBox = struct {
    top_left Point;
    bottom_right Point;
};

/// Manages a single instance of a canvas. Each session of this protocol is responsible for a new
/// canvas.
@discoverable
open protocol Instance {
    /// Add multiple lines to the canvas. We are able to reduce protocol chatter and the number of
    /// requests needed by batching instead of calling the simpler `AddLine(...)` one line at a
    /// time.
    flexible AddLines(struct {
        lines vector<Line>;
    });

    /// Rather than the server randomly performing draws, or trying to guess when to do so, the
    /// client must explicitly ask for them. This creates a bit of extra chatter with the additional
    /// method invocation, but allows much greater client-side control of when the canvas is "ready"
    /// for a view update, thereby eliminating unnecessary draws.
    ///
    /// This method also has the benefit of "throttling" the `-> OnDrawn(...)` event - rather than
    /// allowing a potentially unlimited flood of `-> OnDrawn(...)` calls, we now have the runtime
    /// enforced semantic that each `-> OnDrawn(...)` call must follow a unique `Ready() -> ()` call
    /// from the client. An unprompted `-> OnDrawn(...)` is invalid, and should cause the channel to
    /// immediately close.
    flexible Ready() -> ();

    /// Update the client with the latest drawing state. The server makes no guarantees about how
    /// often this event occurs - it could occur multiple times per board state, for example.
    flexible -> OnDrawn(BoundingBox);
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.canvas.clientrequesteddraw.Instance" },
    ],
    config: {
        // A script for the client to follow. Entries in the script may take one of two forms: a
        // pair of signed-integer coordinates like "-2,15:4,5", or the string "READY". The former
        // builds a local vector sent via a single `AddLines(...)` call, while the latter sends a
        // `Ready() -> ()` call pauses execution until the next `->OnDrawn(...)` event is received.
        //
        // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great
        //   deal of string parsing if we were able to use a vector of `union { Point; Ready}` here.
        script: {
            type: "vector",
            max_count: 100,
            element: {
                type: "string",
                max_size: 64,
            },
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.canvas.clientrequesteddraw.Instance" },
    ],
    expose: [
        {
            protocol: "examples.canvas.clientrequesteddraw.Instance",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.canvas.clientrequesteddraw.Instance",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{format_err, Context as _, Error},
    config::Config,
    fidl_examples_canvas_clientrequesteddraw::{InstanceEvent, InstanceMarker, Point},
    fuchsia_component::client::connect_to_protocol,
    futures::TryStreamExt,
    std::{thread, time},
};

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send Instance requests
    // across the channel.
    let instance = connect_to_protocol::<InstanceMarker>()?;
    println!("Outgoing connection enabled");

    let mut batched_lines = Vec::<[Point; 2]>::new();
    for action in config.script.into_iter() {
        // If the next action in the script is to "PUSH", send a batch of lines to the server.
        if action == "PUSH" {
            instance.add_lines(&batched_lines).context("Could not send lines")?;
            println!("AddLines request sent");
            batched_lines.clear();
            continue;
        }

        // If the next action in the script is to "WAIT", block until an OnDrawn event is received
        // from the server.
        if action == "WAIT" {
            let mut event_stream = instance.take_event_stream();
            loop {
                match event_stream
                    .try_next()
                    .await
                    .context("Error getting event response from proxy")?
                    .ok_or_else(|| format_err!("Proxy sent no events"))?
                {
                    InstanceEvent::OnDrawn { top_left, bottom_right } => {
                        println!(
                            "OnDrawn event received: top_left: {:?}, bottom_right: {:?}",
                            top_left, bottom_right
                        );
                        break;
                    }
                    InstanceEvent::_UnknownEvent { ordinal, .. } => {
                        println!("Received an unknown event with ordinal {ordinal}");
                    }
                }
            }

            // Now, inform the server that we are ready to receive more updates whenever they are
            // ready for us.
            println!("Ready request sent");
            instance.ready().await.context("Could not send ready call")?;
            println!("Ready success");
            continue;
        }

        // Add a line to the next batch. Parse the string input, making two points out of it.
        let mut points = action
            .split(":")
            .map(|point| {
                let integers = point
                    .split(",")
                    .map(|integer| integer.parse::<i64>().unwrap())
                    .collect::<Vec<i64>>();
                Point { x: integers[0], y: integers[1] }
            })
            .collect::<Vec<Point>>();

        // Assemble a line from the two points.
        let from = points.pop().ok_or(format_err!("line requires 2 points, but has 0"))?;
        let to = points.pop().ok_or(format_err!("line requires 2 points, but has 1"))?;
        let mut line: [Point; 2] = [from, to];

        // Batch a line for drawing to the canvas using the two points provided.
        println!("AddLines batching line: {:?}", &mut line);
        batched_lines.push(line);
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{anyhow, Context as _, Error},
    fidl::endpoints::RequestStream as _,
    fidl_examples_canvas_clientrequesteddraw::{
        BoundingBox, InstanceRequest, InstanceRequestStream, Point,
    },
    fuchsia_async::{Time, Timer},
    fuchsia_component::server::ServiceFs,
    fuchsia_zircon::{self as zx},
    futures::future::join,
    futures::prelude::*,
    std::sync::{Arc, Mutex},
};

// A struct that stores the two things we care about for this example: the bounding box the lines
// that have been added thus far, and bit to track whether or not there have been changes since the
// last `OnDrawn` event.
#[derive(Debug)]
struct CanvasState {
    // Tracks whether there has been a change since the last send, to prevent redundant updates.
    changed: bool,
    // Tracks whether or not the client has declared itself ready to receive more updated.
    ready: bool,
    bounding_box: BoundingBox,
}

/// Handler for the `AddLines` method.
fn add_lines(state: &mut CanvasState, lines: Vec<[Point; 2]>) {
    // Update the bounding box to account for the new lines we've just "added" to the canvas.
    let bounds = &mut state.bounding_box;
    for line in lines {
        println!("AddLines printing line: {:?}", line);
        for point in line {
            if point.x < bounds.top_left.x {
                bounds.top_left.x = point.x;
            }
            if point.y > bounds.top_left.y {
                bounds.top_left.y = point.y;
            }
            if point.x > bounds.bottom_right.x {
                bounds.bottom_right.x = point.x;
            }
            if point.y < bounds.bottom_right.y {
                bounds.bottom_right.y = point.y;
            }
        }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next tick.
    state.changed = true
}

/// Creates a new instance of the server, paired to a single client across a zircon channel.
async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> {
    // Create a new in-memory state store for the state of the canvas. The store will live for the
    // lifetime of the connection between the server and this particular client.
    let state = Arc::new(Mutex::new(CanvasState {
        changed: true,
        ready: true,
        bounding_box: BoundingBox {
            top_left: Point { x: 0, y: 0 },
            bottom_right: Point { x: 0, y: 0 },
        },
    }));

    // Take ownership of the control_handle from the stream, which will allow us to push events from
    // a different async task.
    let control_handle = stream.control_handle();

    // A separate watcher task periodically "draws" the canvas, and notifies the client of the new
    // state. We'll need a cloned reference to the canvas state to be accessible from the new
    // task.
    let state_ref = state.clone();
    let update_sender = || async move {
        loop {
            // Our server sends one update per second, but only if the client has declared that it
            // is ready to receive one.
            Timer::new(Time::after(zx::Duration::from_seconds(1))).await;
            let mut state = state_ref.lock().unwrap();
            if !state.changed || !state.ready {
                continue;
            }

            // After acquiring the lock, this is where we would draw the actual lines. Since this is
            // just an example, we'll avoid doing the actual rendering, and simply send the bounding
            // box to the client instead.
            let bounds = state.bounding_box;
            match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) {
                Ok(_) => println!(
                    "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}",
                    bounds.top_left, bounds.bottom_right
                ),
                Err(_) => return,
            }

            // Reset the change and ready trackers.
            state.ready = false;
            state.changed = false;
        }
    };

    // Handle requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    let state_ref = &state;
    let request_handler =
        stream.map(|result| result.context("failed request")).try_for_each(|request| async move {
            // Match based on the method being invoked.
            match request {
                InstanceRequest::AddLines { lines, .. } => {
                    println!("AddLines request received");
                    add_lines(&mut state_ref.lock().unwrap(), lines);
                }
                InstanceRequest::Ready { responder, .. } => {
                    println!("Ready request received");
                    // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();`
                    // event; if two "consecutive" `Ready() -> ();` calls are received, this
                    // interaction has entered an invalid state, and should be aborted immediately.
                    let mut state = state_ref.lock().unwrap();
                    if state.ready == true {
                        return Err(anyhow!("Invalid back-to-back `Ready` requests received"));
                    }

                    state.ready = true;
                    responder.send().context("Error responding")?;
                } //
                InstanceRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        });

    // This line will only be reached if the server errors out. The stream will await indefinitely,
    // thereby creating a long-lived server. Here, we first wait for the updater task to realize the
    // connection has died, then bubble up the error.
    join(request_handler, update_sender()).await.0
}

// A helper enum that allows us to treat a `Instance` service instance as a value.
enum IncomingService {
    Instance(InstanceRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Instance` protocol - this will allow the client to see
    // the server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Instance);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/fidl/new/canvas/client_requested_draw/cpp_natural/client/config.h>

// The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming
// events asynchronously.
class EventHandler : public fidl::AsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> {
 public:
  // Handler for |OnDrawn| events sent from the server.
  void OnDrawn(
      fidl::Event<examples_canvas_clientrequesteddraw::Instance::OnDrawn>& event) override {
    ::examples_canvas_clientrequesteddraw::Point top_left = event.top_left();
    ::examples_canvas_clientrequesteddraw::Point bottom_right = event.bottom_right();
    FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x()
                  << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x()
                  << ", y: " << bottom_right.y() << " }";
    loop_.Quit();
  }

  void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; }

  void handle_unknown_event(
      fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal;
  }

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

 private:
  async::Loop& loop_;
};

// A helper function that takes a coordinate in string form, like "123,-456", and parses it into a
// a struct of the form |{ in64 x; int64 y; }|.
::examples_canvas_clientrequesteddraw::Point ParsePoint(std::string_view input) {
  int64_t x = 0;
  int64_t y = 0;
  size_t index = input.find(',');
  if (index != std::string::npos) {
    std::from_chars(input.data(), input.data() + index, x);
    std::from_chars(input.data() + index + 1, input.data() + input.length(), y);
  }
  return ::examples_canvas_clientrequesteddraw::Point(x, y);
}

using Line = ::std::array<::examples_canvas_clientrequesteddraw::Point, 2>;

// A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it
// into an array of 2 |Point| structs.
Line ParseLine(const std::string& action) {
  auto input = std::string_view(action);
  size_t index = input.find(':');
  if (index != std::string::npos) {
    return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))};
  }
  return {};
}

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop and dispatcher.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the 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<examples_canvas_clientrequesteddraw::Instance>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // Create an instance of the event handler.
  EventHandler event_handler(loop);

  // Create an asynchronous client using the newly-established connection.
  fidl::Client client(std::move(*client_end), dispatcher, &event_handler);
  FX_LOGS(INFO) << "Outgoing connection enabled";

  std::vector<Line> batched_lines;
  for (const auto& action : conf.script()) {
    // If the next action in the script is to "PUSH", send a batch of lines to the server.
    if (action == "PUSH") {
      fit::result<fidl::Error> result = client->AddLines(batched_lines);
      if (!result.is_ok()) {
        // Check that our one-way call was enqueued successfully, and handle the error
        // appropriately. In the case of this example, there is nothing we can do to recover here,
        // except to log an error and exit the program.
        FX_LOGS(ERROR) << "Could not send AddLines request: " << result.error_value();
        return -1;
      }

      batched_lines.clear();
      FX_LOGS(INFO) << "AddLines request sent";
      continue;
    }

    // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received
    // from the server.
    if (action == "WAIT") {
      loop.Run();
      loop.ResetQuit();

      // Now, inform the server that we are ready to receive more updates whenever they are
      // ready for us.
      FX_LOGS(INFO) << "Ready request sent";
      client->Ready().ThenExactlyOnce(
          [&](fidl::Result<examples_canvas_clientrequesteddraw::Instance::Ready> result) {
            // Check if the FIDL call succeeded or not.
            if (result.is_ok()) {
              FX_LOGS(INFO) << "Ready success";
            } else {
              FX_LOGS(ERROR) << "Could not send Ready request: " << result.error_value();
            }

            // Quit the loop, thereby handing control back to the outer loop of actions being
            // iterated over.
            loop.Quit();
          });

      // Run the loop until the callback is resolved, at which point we can continue from here.
      loop.Run();
      loop.ResetQuit();

      continue;
    }

    // Batch a line for drawing to the canvas using the two points provided.
    Line line = ParseLine(action);
    batched_lines.push_back(line);
    FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x() << ", y: " << line[1].y()
                  << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]";
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <src/lib/fxl/macros.h>
#include <src/lib/fxl/memory/weak_ptr.h>

// A struct that stores the two things we care about for this example: the set of lines, and the
// bounding box that contains them.
struct CanvasState {
  // Tracks whether there has been a change since the last send, to prevent redundant updates.
  bool changed = true;
  // Tracks whether or not the client has declared itself ready to receive more updated.
  bool ready = true;
  examples_canvas_clientrequesteddraw::BoundingBox bounding_box;
};

// An implementation of the |Instance| protocol.
class InstanceImpl final : public fidl::Server<examples_canvas_clientrequesteddraw::Instance> {
 public:
  // Bind this implementation to a channel.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end)
      : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)),
        weak_factory_(this) {
    // Start the update timer on startup. Our server sends one update per second
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void OnFidlClosed(fidl::UnbindInfo info) {
    if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) {
      FX_LOGS(ERROR) << "Shutdown unexpectedly";
    }
    delete this;
  }

  void AddLines(AddLinesRequest& request, AddLinesCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "AddLines request received";
    for (const auto& points : request.lines()) {
      FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x()
                    << ", y: " << points[1].y() << " }, Point { x: " << points[0].x()
                    << ", y: " << points[0].y() << " }]";

      // Update the bounding box to account for the new line we've just "added" to the canvas.
      auto& bounds = state_.bounding_box;
      for (const auto& point : points) {
        if (point.x() < bounds.top_left().x()) {
          bounds.top_left().x() = point.x();
        }
        if (point.y() > bounds.top_left().y()) {
          bounds.top_left().y() = point.y();
        }
        if (point.x() > bounds.bottom_right().x()) {
          bounds.bottom_right().x() = point.x();
        }
        if (point.y() < bounds.bottom_right().y()) {
          bounds.bottom_right().y() = point.y();
        }
      }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn|
    // event.
    state_.changed = true;
  }

  void Ready(ReadyCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "Ready request received";

    // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two
    // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid
    // state, and should be aborted immediately.
    if (state_.ready == true) {
      FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received";
    }

    state_.ready = true;
    completer.Reply();
  }

  void handle_unknown_method(
      fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata,
      fidl::UnknownMethodCompleter::Sync& completer) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal;
  }

 private:
  // Each scheduled update waits for the allotted amount of time, sends an update if something has
  // changed, and schedules the next update.
  void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) {
    async::PostDelayedTask(
        dispatcher,
        [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] {
          // Halt execution if the binding has been deallocated already.
          if (!weak) {
            return;
          }

          // Schedule the next update if the binding still exists.
          weak->ScheduleOnDrawnEvent(dispatcher, after);

          // No need to send an update if nothing has changed since the last one, or the client has
          // not yet informed us that it is ready for more updates.
          if (!weak->state_.changed || !weak->state_.ready) {
            return;
          }

          // This is where we would draw the actual lines. Since this is just an example, we'll
          // avoid doing the actual rendering, and simply send the bounding box to the client
          // instead.
          auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box);
          if (!result.is_ok()) {
            return;
          }

          auto top_left = state_.bounding_box.top_left();
          auto bottom_right = state_.bounding_box.bottom_right();
          FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x()
                        << ", y: " << top_left.y()
                        << " }, bottom_right: Point { x: " << bottom_right.x()
                        << ", y: " << bottom_right.y() << " }";

          // Reset the change and ready trackers.
          state_.ready = false;
          state_.changed = false;
        },
        after);
  }

  fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_;
  CanvasState state_ = CanvasState{};

  // Generates weak references to this object, which are appropriate to pass into asynchronous
  // callbacks that need to access this object. The references are automatically invalidated
  // if this object is destroyed.
  fxl::WeakPtrFactory<InstanceImpl> weak_factory_;
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This
  // directory is where the outgoing FIDL protocols are installed so that they can be provided to
  // other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle.
  // The startup handle is a handle provided to every component by the system, so that they can
  // serve capabilities (e.g. FIDL protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to
  // |examples.canvas.clientrequesteddraw.Instance|.
  result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>(
      [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) {
        // Create an instance of our InstanceImpl that destroys itself when the connection closes.
        new InstanceImpl(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string();
    return -1;
  }

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

C++ (有線)

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/fidl/new/canvas/client_requested_draw/cpp_wire/client/config.h>

// The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming
// events asynchronously.
class EventHandler
    : public fidl::WireAsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> {
 public:
  // Handler for |OnDrawn| events sent from the server.
  void OnDrawn(
      fidl::WireEvent<examples_canvas_clientrequesteddraw::Instance::OnDrawn>* event) override {
    ::examples_canvas_clientrequesteddraw::wire::Point top_left = event->top_left;
    ::examples_canvas_clientrequesteddraw::wire::Point bottom_right = event->bottom_right;
    FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x
                  << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x
                  << ", y: " << bottom_right.y << " }";
    loop_.Quit();
  }

  void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; }

  void handle_unknown_event(
      fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal;
  }

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

 private:
  async::Loop& loop_;
};

// A helper function that takes a coordinate in string form, like "123,-456", and parses it into a
// a struct of the form |{ in64 x; int64 y; }|.
::examples_canvas_clientrequesteddraw::wire::Point ParsePoint(std::string_view input) {
  int64_t x = 0;
  int64_t y = 0;
  size_t index = input.find(',');
  if (index != std::string::npos) {
    std::from_chars(input.data(), input.data() + index, x);
    std::from_chars(input.data() + index + 1, input.data() + input.length(), y);
  }
  return ::examples_canvas_clientrequesteddraw::wire::Point{.x = x, .y = y};
}

using Line = ::fidl::Array<::examples_canvas_clientrequesteddraw::wire::Point, 2>;

// A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it
// into an array of 2 |Point| structs.
Line ParseLine(const std::string& action) {
  auto input = std::string_view(action);
  size_t index = input.find(':');
  if (index != std::string::npos) {
    return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))};
  }
  return {};
}

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop and dispatcher.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the 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<examples_canvas_clientrequesteddraw::Instance>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // Create an instance of the event handler.
  EventHandler event_handler(loop);

  // Create an asynchronous client using the newly-established connection.
  fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler);
  FX_LOGS(INFO) << "Outgoing connection enabled";

  std::vector<Line> batched_lines;
  for (const auto& action : conf.script()) {
    // If the next action in the script is to "PUSH", send a batch of lines to the server.
    if (action == "PUSH") {
      fidl::Status status = client->AddLines(fidl::VectorView<Line>::FromExternal(batched_lines));
      if (!status.ok()) {
        // Check that our one-way call was enqueued successfully, and handle the error
        // appropriately. In the case of this example, there is nothing we can do to recover here,
        // except to log an error and exit the program.
        FX_LOGS(ERROR) << "Could not send AddLines request: " << status.error();
        return -1;
      }

      batched_lines.clear();
      FX_LOGS(INFO) << "AddLines request sent";
      continue;
    }

    // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received
    // from the server.
    if (action == "WAIT") {
      loop.Run();
      loop.ResetQuit();

      // Now, inform the server that we are ready to receive more updates whenever they are
      // ready for us.
      FX_LOGS(INFO) << "Ready request sent";
      client->Ready().ThenExactlyOnce(
          [&](fidl::WireUnownedResult<examples_canvas_clientrequesteddraw::Instance::Ready>&
                  result) {
            // Check if the FIDL call succeeded or not.
            if (result.ok()) {
              FX_LOGS(INFO) << "Ready success";
            } else {
              FX_LOGS(ERROR) << "Could not send Ready request: " << result.error();
            }

            // Quit the loop, thereby handing control back to the outer loop of actions being
            // iterated over.
            loop.Quit();
          });

      // Run the loop until the callback is resolved, at which point we can continue from here.
      loop.Run();
      loop.ResetQuit();

      continue;
    }

    // Batch a line for drawing to the canvas using the two points provided.
    Line line = ParseLine(action);
    batched_lines.push_back(line);
    FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x << ", y: " << line[1].y
                  << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]";
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <src/lib/fxl/macros.h>
#include <src/lib/fxl/memory/weak_ptr.h>

// A struct that stores the two things we care about for this example: the set of lines, and the
// bounding box that contains them.
struct CanvasState {
  // Tracks whether there has been a change since the last send, to prevent redundant updates.
  bool changed = true;
  // Tracks whether or not the client has declared itself ready to receive more updated.
  bool ready = true;
  examples_canvas_clientrequesteddraw::wire::BoundingBox bounding_box;
};

// An implementation of the |Instance| protocol.
class InstanceImpl final : public fidl::WireServer<examples_canvas_clientrequesteddraw::Instance> {
 public:
  // Bind this implementation to a channel.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end)
      : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)),
        weak_factory_(this) {
    // Start the update timer on startup. Our server sends one update per second
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void OnFidlClosed(fidl::UnbindInfo info) {
    if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) {
      FX_LOGS(ERROR) << "Shutdown unexpectedly";
    }
    delete this;
  }

  void AddLines(AddLinesRequestView request, AddLinesCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "AddLines request received";
    for (const auto& points : request->lines) {
      FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x
                    << ", y: " << points[1].y << " }, Point { x: " << points[0].x
                    << ", y: " << points[0].y << " }]";

      // Update the bounding box to account for the new line we've just "added" to the canvas.
      auto& bounds = state_.bounding_box;
      for (const auto& point : points) {
        if (point.x < bounds.top_left.x) {
          bounds.top_left.x = point.x;
        }
        if (point.y > bounds.top_left.y) {
          bounds.top_left.y = point.y;
        }
        if (point.x > bounds.bottom_right.x) {
          bounds.bottom_right.x = point.x;
        }
        if (point.y < bounds.bottom_right.y) {
          bounds.bottom_right.y = point.y;
        }
      }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn|
    // event.
    state_.changed = true;
  }

  void Ready(ReadyCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "Ready request received";

    // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two
    // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid
    // state, and should be aborted immediately.
    if (state_.ready == true) {
      FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received";
    }

    state_.ready = true;
    completer.Reply();
  }

  void handle_unknown_method(
      fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata,
      fidl::UnknownMethodCompleter::Sync& completer) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal;
  }

 private:
  // Each scheduled update waits for the allotted amount of time, sends an update if something has
  // changed, and schedules the next update.
  void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) {
    async::PostDelayedTask(
        dispatcher,
        [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] {
          // Halt execution if the binding has been deallocated already.
          if (!weak) {
            return;
          }

          // Schedule the next update if the binding still exists.
          weak->ScheduleOnDrawnEvent(dispatcher, after);

          // No need to send an update if nothing has changed since the last one, or the client has
          // not yet informed us that it is ready for more updates.
          if (!weak->state_.changed || !weak->state_.ready) {
            return;
          }

          // This is where we would draw the actual lines. Since this is just an example, we'll
          // avoid doing the actual rendering, and simply send the bounding box to the client
          // instead.
          auto top_left = weak->state_.bounding_box.top_left;
          auto bottom_right = weak->state_.bounding_box.bottom_right;
          fidl::Status status =
              fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right);
          if (!status.ok()) {
            return;
          }
          FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x
                        << ", y: " << top_left.y
                        << " }, bottom_right: Point { x: " << bottom_right.x
                        << ", y: " << bottom_right.y << " }";

          // Reset the change and ready trackers.
          state_.ready = false;
          weak->state_.changed = false;
        },
        after);
  }

  fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_;
  CanvasState state_ = CanvasState{};

  // Generates weak references to this object, which are appropriate to pass into asynchronous
  // callbacks that need to access this object. The references are automatically invalidated
  // if this object is destroyed.
  fxl::WeakPtrFactory<InstanceImpl> weak_factory_;
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This
  // directory is where the outgoing FIDL protocols are installed so that they can be provided to
  // other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle.
  // The startup handle is a handle provided to every component by the system, so that they can
  // serve capabilities (e.g. FIDL protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to
  // |examples.canvas.clientrequesteddraw.Instance|.
  result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>(
      [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) {
        // Create an instance of our InstanceImpl that destroys itself when the connection closes.
        new InstanceImpl(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string();
    return -1;
  }

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

HLCPP

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <lib/async-loop/cpp/loop.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/canvas/clientrequesteddraw/cpp/fidl.h>
#include <examples/fidl/new/canvas/client_requested_draw/hlcpp/client/config.h>

// A helper function that takes a coordinate in string form, like "123,-456", and parses it into a
// a struct of the form |{ in64 x; int64 y; }|.
::examples::canvas::clientrequesteddraw::Point ParsePoint(std::string_view input) {
  int64_t x = 0;
  int64_t y = 0;
  size_t index = input.find(',');
  if (index != std::string::npos) {
    std::from_chars(input.data(), input.data() + index, x);
    std::from_chars(input.data() + index + 1, input.data() + input.length(), y);
  }
  return ::examples::canvas::clientrequesteddraw::Point{.x = x, .y = y};
}

using Line = ::std::array<::examples::canvas::clientrequesteddraw::Point, 2>;

// A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it
// into an array of 2 |Point| structs.
Line ParseLine(const std::string& action) {
  auto input = std::string_view(action);
  size_t index = input.find(':');
  if (index != std::string::npos) {
    return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))};
  }
  return {};
}

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the protocol inside the component's namespace, then create an asynchronous client
  // using the newly-established connection.
  examples::canvas::clientrequesteddraw::InstancePtr instance_proxy;
  auto context = sys::ComponentContext::Create();
  context->svc()->Connect(instance_proxy.NewRequest(dispatcher));
  FX_LOGS(INFO) << "Outgoing connection enabled";

  instance_proxy.set_error_handler([&loop](zx_status_t status) {
    FX_LOGS(ERROR) << "Shutdown unexpectedly";
    loop.Quit();
  });

  // Provide a lambda to handle incoming |OnDrawn| events asynchronously.
  instance_proxy.events().OnDrawn =
      [&loop](::examples::canvas::clientrequesteddraw::Point top_left,
              ::examples::canvas::clientrequesteddraw::Point bottom_right) {
        FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x
                      << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x
                      << ", y: " << bottom_right.y << " }";
        loop.Quit();
      };

  instance_proxy.events().handle_unknown_event = [](uint64_t ordinal) {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << ordinal;
  };

  std::vector<Line> batched_lines;
  for (const auto& action : conf.script()) {
    // If the next action in the script is to "PUSH", send a batch of lines to the server.
    if (action == "PUSH") {
      instance_proxy->AddLines(batched_lines);
      batched_lines.clear();
      FX_LOGS(INFO) << "AddLines request sent";
      continue;
    }

    // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received
    // from the server.
    if (action == "WAIT") {
      loop.Run();
      loop.ResetQuit();

      // Now, inform the server that we are ready to receive more updates whenever they are ready
      // for us.
      FX_LOGS(INFO) << "Ready request sent";
      instance_proxy->Ready([&](fpromise::result<void, fidl::FrameworkErr> result) {
        if (result.is_error()) {
          // Check that our flexible two-way call was known to the server and handle the case of an
          // unknown method appropriately. In the case of this example, there is nothing we can do
          // to recover here, except to log an error and exit the program.
          FX_LOGS(ERROR) << "Server does not implement AddLine";
        }

        FX_LOGS(INFO) << "Ready success";

        // Quit the loop, thereby handing control back to the outer loop of actions being iterated
        // over.
        loop.Quit();
      });

      // Run the loop until the callback is resolved, at which point we can continue from here.
      loop.Run();
      loop.ResetQuit();

      continue;
    }

    // Batch a line for drawing to the canvas using the two points provided.
    Line line = ParseLine(action);
    batched_lines.push_back(line);
    FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x << ", y: " << line[1].y
                  << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]";
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async/cpp/task.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <examples/canvas/clientrequesteddraw/cpp/fidl.h>
#include <src/lib/fxl/macros.h>
#include <src/lib/fxl/memory/weak_ptr.h>

// A struct that stores the two things we care about for this example: the set of lines, and the
// bounding box that contains them.
struct CanvasState {
  // Tracks whether there has been a change since the last send, to prevent redundant updates.
  bool changed = true;
  // Tracks whether or not the client has declared itself ready to receive more updated.
  bool ready = true;
  examples::canvas::clientrequesteddraw::BoundingBox bounding_box;
};

using Line = ::std::array<::examples::canvas::clientrequesteddraw::Point, 2>;

// An implementation of the |Instance| protocol.
class InstanceImpl final : public examples::canvas::clientrequesteddraw::Instance {
 public:
  // Bind this implementation to an |InterfaceRequest|.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::InterfaceRequest<examples::canvas::clientrequesteddraw::Instance> request)
      : binding_(fidl::Binding<examples::canvas::clientrequesteddraw::Instance>(this)),
        weak_factory_(this) {
    binding_.Bind(std::move(request), dispatcher);

    // Gracefully handle abrupt shutdowns.
    binding_.set_error_handler([this](zx_status_t status) mutable {
      if (status != ZX_ERR_PEER_CLOSED) {
        FX_LOGS(ERROR) << "Shutdown unexpectedly";
      }
      delete this;
    });

    // Start the update timer on startup. Our server sends one update per second.
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void AddLines(std::vector<Line> lines) override {
    FX_LOGS(INFO) << "AddLines request received";
    for (const auto& points : lines) {
      FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x
                    << ", y: " << points[1].y << " }, Point { x: " << points[0].x
                    << ", y: " << points[0].y << " }]";

      // Update the bounding box to account for the new line we've just "added" to the canvas.
      auto& bounds = state_.bounding_box;
      for (const auto& point : points) {
        if (point.x < bounds.top_left.x) {
          bounds.top_left.x = point.x;
        }
        if (point.y > bounds.top_left.y) {
          bounds.top_left.y = point.y;
        }
        if (point.x > bounds.bottom_right.x) {
          bounds.bottom_right.x = point.x;
        }
        if (point.y < bounds.bottom_right.y) {
          bounds.bottom_right.y = point.y;
        }
      }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next
    // |OnDrawn| event.
    state_.changed = true;
  }

  void Ready(ReadyCallback callback) override {
    FX_LOGS(INFO) << "Ready request received";

    // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if
    // two "consecutive" `Ready() -> ();` calls are received, this interaction has entered an
    // invalid state, and should be aborted immediately.
    if (state_.ready == true) {
      FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received";
    }

    state_.ready = true;
    callback(fpromise::ok());
  }

  void handle_unknown_method(uint64_t ordinal, bool method_has_response) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << ordinal;
  }

 private:
  // Each scheduled update waits for the allotted amount of time, sends an update if something
  // has changed, and schedules the next update.
  void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) {
    async::PostDelayedTask(
        dispatcher,
        [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] {
          // Halt execution if the binding has been deallocated already.
          if (!weak) {
            return;
          }

          // Schedule the next update if the binding still exists.
          weak->ScheduleOnDrawnEvent(dispatcher, after);

          // No need to send an update if nothing has changed since the last one, or the client
          // has not yet informed us that it is ready for more updates.
          if (!weak->state_.changed || !weak->state_.ready) {
            return;
          }

          // This is where we would draw the actual lines. Since this is just an example, we'll
          // avoid doing the actual rendering, and simply send the bounding box to the client
          // instead.
          auto top_left = state_.bounding_box.top_left;
          auto bottom_right = state_.bounding_box.bottom_right;
          binding_.events().OnDrawn(top_left, bottom_right);
          FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x
                        << ", y: " << top_left.y
                        << " }, bottom_right: Point { x: " << bottom_right.x
                        << ", y: " << bottom_right.y << " }";

          // Reset the change and ready trackers.
          state_.ready = false;
          state_.changed = false;
        },
        after);
  }

  fidl::Binding<examples::canvas::clientrequesteddraw::Instance> binding_;
  CanvasState state_ = CanvasState{};

  // Generates weak references to this object, which are appropriate to pass into asynchronous
  // callbacks that need to access this object. The references are automatically invalidated
  // if this object is destroyed.
  fxl::WeakPtrFactory<InstanceImpl> weak_factory_;
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from
  // the client. The following initializes the loop, and obtains the dispatcher, which will be
  // used when binding the server implementation to a channel.
  //
  // Note that unlike the new C++ bindings, HLCPP bindings rely on the async loop being attached
  // to the current thread via the |kAsyncLoopConfigAttachToCurrentThread| configuration.
  async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component.
  // This directory is where the outgoing FIDL protocols are installed so that they can be
  // provided to other components.
  auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory();

  // Register a handler for components trying to connect to
  // |examples.canvas.clientrequesteddraw.Instance|.
  context->outgoing()->AddPublicService(
      fidl::InterfaceRequestHandler<examples::canvas::clientrequesteddraw::Instance>(
          [dispatcher](
              fidl::InterfaceRequest<examples::canvas::clientrequesteddraw::Instance> request) {
            // Create an instance of our |InstanceImpl| that destroys itself when the connection
            // closes.
            new InstanceImpl(dispatcher, std::move(request));
          }));

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

聯合酬載

FIDL 方案:聯集酬載

聯集酬載是使用 union 版面配置的 FIDL 方法酬載。做為方法酬載使用的頂層類型必須使用 structtableunion 其中之一做為版面配置。值得注意的是,部分產生的繫結會「扁平」傳遞給 struct 方法酬載的引數,這樣每個成員都會在呼叫簽章中將其視為函式引數。使用 tableunion 的酬載則不會這麼做,且一律會傳遞名為 payload 的單一引數。

推理

鍵/值儲存庫基準範例實作是很好的起點,但主要缺點是將資料儲存為原始位元組。FIDL 是一種多重輸入的語言。強制將 UTF-8 字串儲存為無類型位元組陣列的資料,會為 *.fidl 檔案讀取器以及使用從這個檔案產生的繫結的程式設計人員清除這項寶貴類型資訊。

實作

這項變更的主要目標是將基準案例的 vector<byte> 類型 value 成員替換成儲存許多可能類型的 union。事實上,此變更是針對 FIDL 的類型進行的良好調查:

  • 所有 FIDL 內建的純量類型都會做為 Value union 中的變體使用:booluint8uint16uint32uint64int8int16int32int64float32float64 (也稱為 FIDL 的原始類型),以及 。string
  • 這個 union 也使用 FIDL 的內建 array<T, N>vector<T> 類型範本。
  • 所有 FIDL 的類型版面配置 (包括 bitsenumtableunionstruct) 至少都在此範例中使用一次。

WriteItem 使用的要求和回應酬載也已從 struct 變更為命名的 table 和內嵌的 flexible union。事實上,這三種版面配置中的任一個,都可以使用要求/回應酬載。除了訊息大小最敏感的情況下,系統會分別使用後者 (分別稱為「資料表酬載」和 *聯集酬載)。這是因為日後能以與二進位檔相容的方式加以擴充,方便日後擴充。

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.keyvaluestore.usegenericvalues;

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value Value;
};

// Because the `Value` must be used both in the request and the response, we give it its own named
// type. The type is a `union` of all possible data types that we take as values, and is marked
// `flexible` to allow for the easy addition of new data types in the future.
type Value = flexible union {
    // Keep the original `bytes` as one of the options in the new union.
    1: bytes vector<byte>:64000;

    // A `string` is very similar to `vector<byte>` on the wire, with the extra constraint that
    // it enforces that it enforces that the byte vector in question is valid UTF-8.
    2: string string:64000;

    // All of FIDL's primitive types.
    3: bool bool;
    4: uint8 uint8;
    5: int8 int8;
    6: uint16 uint16;
    7: int16 int16;
    8: uint32 uint32;
    9: int32 int32;
   10: float32 float32;
   11: uint64 uint64;
   12: int64 int64;
   13: float64 float64;

    // FIDL does not natively support 128-bit integer types, so we have to define our own
    // representations.
   14: uint128 array<uint64, 2>;
};

// Because we now supoprt a richer range of types as values in our store, it is helpful to use a
// `flexible`, and therefore evolvable, `bits` type to store write options.
type WriteOptions = flexible bits : uint8 {
    // This flag allows us to overwrite existing data when there is a collision, rather than failing
    // with an `WriteError.ALREADY_EXISTS`.
    OVERWRITE = 0b1;
    // This flag allows us to concatenate to existing data when there is a collision, rather than
    // failing with an `WriteError.ALREADY_EXISTS`. "Concatenation" means addition for the numeric
    // variants and appending to the `bytes`/`string` variants. If no existing data can be found, we
    // "concatenate" to default values of zero and an empty vector, respectively. Attempting to
    // concatenate to an existing variant of a different type will return a
    // `WriteError.INVALID_VALUE` error.
    CONCAT = 0b10;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// A very basic key-value store.
@discoverable
open protocol Store {
    /// Writes an item to the store.
    ///
    /// Since the value stored in the key-value store can now be different from the input (if the
    /// `WriteOptions.CONCAT` flag is set), we need to return the resulting `Value` to the
    /// requester.
    ///
    /// We use an (anonymous) `table` and a (named) `flexible union` as the request and response
    /// payload, respectively, to allow for easier future evolution. Both of these types are
    /// `flexible`, meaning that adding or removing members is binary-compatible. This makes them
    /// much easier to evolve that the `struct` types that were previously used, which cannot be
    /// changed after release without breaking ABI.
    flexible WriteItem(table {
        1: attempt Item;
        2: options WriteOptions;
    }) -> (Value) error WriteError;
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.usegenericvalues.Store" },
    ],
    config: {
        // A vector of values for every easily representible type in our key-value store. For
        // brevity's sake, the 8, 16, and 32 bit integer types and booleans are omitted.
        //
        // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great
        //   deal of string parsing if we were able to use all FIDL constructs directly here. In
        //   particular, floats and nested types are very difficult to represent, and have been
        //   excluded from this example for the time being.
        set_concat_option: { type: "bool" },
        set_overwrite_option: { type: "bool" },
        write_bytes: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },
        write_strings: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },
        write_uint64s: {
            type: "vector",
            max_count: 16,
            element: { type: "uint64" },
        },
        write_int64s: {
            type: "vector",
            max_count: 16,
            element: { type: "int64" },
        },

        // Note: due to the limitation of structured config not allowing vectors nested in vectors,
        // we only set the lower half of the uint128 for simplicity's sake.
        write_uint128s: {
            type: "vector",
            max_count: 16,
            element: { type: "uint64" },
        },

    },
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.usegenericvalues.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.usegenericvalues.Store",
            from: "self",
        },
    ],
}

運作範圍

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.usegenericvalues.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

接著,用戶端和伺服器實作能以任何支援的語言編寫:

Rust

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    config::Config,
    fidl_examples_keyvaluestore_usegenericvalues::{
        Item, StoreMarker, StoreProxy, StoreWriteItemRequest, Value, WriteOptions,
    },
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

// A helper function to sequentially write a single item to the key-value store and print a log when
// successful.
async fn write_next_item(
    store: &StoreProxy,
    key: &str,
    value: Value,
    options: WriteOptions,
) -> Result<(), Error> {
    // Create an empty request payload using `::default()`.
    let mut req = StoreWriteItemRequest::default();
    req.options = Some(options);

    // Fill in the `Item` we will be attempting to write.
    println!("WriteItem request sent: key: {}, value: {:?}", &key, &value);
    req.attempt = Some(Item { key: key.to_string(), value: value });

    // Send and async `WriteItem` request to the server.
    match store.write_item(&req).await.context("Error sending request")? {
        Ok(value) => println!("WriteItem response received: {:?}", &value),
        Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
    }
    Ok(())
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // All of our requests will have the same bitflags set. Pull these settings from the config.
    let mut options = WriteOptions::empty();
    options.set(WriteOptions::OVERWRITE, config.set_overwrite_option);
    options.set(WriteOptions::CONCAT, config.set_concat_option);

    // The structured config provides one input for most data types that can be stored in the data
    // store. Iterate through those inputs in the order we see them in the FIDL file.
    //
    // Note that FIDL unions are rendered as enums in Rust; for example, the `Value` union has now
    // become a `Value` Rust enum, with each member taking exactly one argument.
    for value in config.write_bytes.into_iter() {
        write_next_item(&store, "bytes", Value::Bytes(value.into()), options).await?;
    }
    for value in config.write_strings.into_iter() {
        write_next_item(&store, "string", Value::String(value), options).await?;
    }
    for value in config.write_uint64s.into_iter() {
        write_next_item(&store, "uint64", Value::Uint64(value), options).await?;
    }
    for value in config.write_int64s.into_iter() {
        write_next_item(&store, "int64", Value::Int64(value), options).await?;
    }
    for value in config.write_uint128s.into_iter() {
        write_next_item(&store, "uint128", Value::Uint128([0, value]), options).await?;
    }

    // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
    // referenced bug has been resolved, we can remove the sleep.
    thread::sleep(time::Duration::from_secs(2));
    Ok(())
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{Context as _, Error},
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
    std::cell::RefCell,
    std::collections::hash_map::Entry,
    std::collections::HashMap,
};

use {
    fidl_examples_keyvaluestore_usegenericvalues::{
        Item, StoreRequest, StoreRequestStream, Value, WriteError, WriteOptions,
    },
    std::collections::hash_map::OccupiedEntry,
    std::ops::Add,
};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile");
}

/// Sums any numeric type.
fn sum<T: Add + Add<Output = T> + Copy>(operands: [T; 2]) -> T {
    operands[0] + operands[1]
}

/// Clones and inserts an entry, so that the original (now concatenated) copy may be returned in the
/// response.
fn write(inserting: Value, mut entry: OccupiedEntry<'_, String, Value>) -> Value {
    entry.insert(inserting.clone());
    println!("Wrote key: {}, value: {:?}", entry.key(), &inserting);
    inserting
}

/// Handler for the `WriteItem` method.
fn write_item(
    store: &mut HashMap<String, Value>,
    attempt: Item,
    options: &WriteOptions,
) -> Result<Value, WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY for key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            // The `CONCAT` flag supersedes the `OVERWRITE` flag, so check it first.
            if options.contains(WriteOptions::CONCAT) {
                match entry.get() {
                    Value::Bytes(old) => {
                        if let Value::Bytes(new) = attempt.value {
                            let mut combined = old.clone();
                            combined.extend(new);
                            return Ok(write(Value::Bytes(combined), entry));
                        }
                    }
                    Value::String(old) => {
                        if let Value::String(new) = attempt.value {
                            return Ok(write(Value::String(format!("{}{}", old, &new)), entry));
                        }
                    }
                    Value::Uint64(old) => {
                        if let Value::Uint64(new) = attempt.value {
                            return Ok(write(Value::Uint64(sum([*old, new])), entry));
                        }
                    }
                    Value::Int64(old) => {
                        if let Value::Int64(new) = attempt.value {
                            return Ok(write(Value::Int64(sum([*old, new])), entry));
                        }
                    }
                    // Note: only works on the uint64 range in practice.
                    Value::Uint128(old) => {
                        if let Value::Uint128(new) = attempt.value {
                            return Ok(write(Value::Uint128([0, sum([old[1], new[1]])]), entry));
                        }
                    }
                    _ => {
                        panic!("actively unsupported type!")
                    }
                }

                // Only reachable if the type of the would be concatenated value did not match the
                // value already occupying this entry.
                println!("Write error: INVALID_VALUE for key: {}", entry.key());
                return Err(WriteError::InvalidValue);
            }

            // If we're not doing CONCAT, check for OVERWRITE next.
            if options.contains(WriteOptions::OVERWRITE) {
                return Ok(write(attempt.value, entry));
            }

            println!("Write error: ALREADY_EXISTS for key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote key: {}, value: {:?}", entry.key(), &attempt.value);
            entry.insert(attempt.value.clone());
            Ok(attempt.value)
        }
    }
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    let store = RefCell::new(HashMap::<String, Value>::new());

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                // Because we are using a table payload, there is an extra level of indirection. The
                // top-level container for the table itself is always called "payload".
                StoreRequest::WriteItem { payload, responder } => {
                    println!("WriteItem request received");

                    // Error out if either of the request table's members are not set.
                    let attempt = payload.attempt.context("required field 'attempt' is unset")?;
                    let options = payload.options.context("required field 'options' is unset")?;

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(
                            write_item(&mut store.borrow_mut(), attempt, &options)
                                .as_ref()
                                .map_err(|e| *e),
                        )
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (有線)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.