这是一个 FIDL 示例目录,旨在通过 实际软件工作流的简化实现。
索引示例
以下示例按顺序演示了有用的 FIDL 概念。
计算器
计算器示例显示了基本构建块 创建第一个 FIDL 协议所需的资源。
键值对存储区
键值对存储示例演示了如何构建 简单的键值对存储区,使用 FIDL 以了解各种数据, 支持所有的语言类型。
画布
画布示例演示了如何构建简单的 2D 使用 FIDL 进行线条渲染画布,以了解常用的数据流 模式。
概念索引
每个“概念”FIDL 语言中至少一项 上一部分中列出的示例。每种解决方案的快速参考 及其实现示例,请参见下文 部分。
确认模式
FIDL 配方:确认模式
确认模式是对方法进行流控制的一种简单方法 采用单向调用的方式不要将该方法保留为一个 而是会变成双向通话,并且没有响应, 俗称为确认。确认信息之所以出现的唯一原因是 表示已经收到邮件的发件人,发件人可以使用此信息 决定如何继续操作。
这种确认的成本会增加到通道上。此模式 也可能导致性能下降 在继续进行下一个调用之前确认。
来回发送不按流量计费的单向通话会产生简单的设计, 都存在潜在的隐患:如果服务器的处理速度 该怎么办呢?例如,客户端可能会加载绘图 由某个文本文件中的数千行信息构成, 全部按顺序显示我们该如何对客户施加背压,以防止 服务器是否因这波更新而应接不暇?
使用确认模式并进行单向调用 AddLine(...);
转换为双向AddLine(...) -> ();
,我们就可以向客户提供反馈。
这样,客户端就可以酌情限制其输出。在本课中,
我们只需让客户端先等待确认,然后再发送下一个
虽然更复杂的设计可以发送
乐观地调整,并且仅在降低收到异步 ACK 的频率时才进行节流
超出预期。
首先,我们需要定义接口定义和自动化测试框架。《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.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}; use config::Config; use fidl_examples_canvas_addlinemetered::{InstanceEvent, InstanceMarker, Point}; use fuchsia_component::client::connect_to_protocol; use futures::TryStreamExt; use 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}; use fidl::endpoints::RequestStream as _; use fidl_examples_canvas_addlinemetered::{ BoundingBox, InstanceRequest, InstanceRequestStream, Point, }; use fuchsia_async::{Time, Timer}; use fuchsia_component::server::ServiceFs; use fuchsia_zircon::{self as zx}; use futures::future::join; use futures::prelude::*; use 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
alias
是一种 FIDL 声明,用于为现有类型分配新名称。
这样做有几个好处:
- 使用
alias
可确保概念只有一个可信来源 别名类型所代表的类型 - 它提供了一种为内容命名的方法,尤其是受约束的类型。
- 对现已添加别名的类型的不同使用可以作为 概念相同。
请务必注意,别名不会传递到生成的
代码编写。也就是说,分配给 alias
的名称
声明绝不会在生成的 FIDL 代码中显示为声明名称。
在此示例中,为 Key
添加 alias
可以让我们在使用
一个定制名称,同时向读者明确说明“key
”的值
针对 Item
类型和 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}; use config::Config; use fidl_examples_keyvaluestore_addreaditem::{Item, StoreMarker}; use fuchsia_component::client::connect_to_protocol; use 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
声明,因为无法通过名称标识类型。
在此变体中,我们允许键值存储区将其他键值存储区视为
成员。简而言之,我们把它变成了一棵树。为此,我们会使用
value
的定义,其中定义使用双成员 union
:一种变体
存储使用与之前相同的 vector<byte>
类型的叶节点,而另一个
以其他嵌套存储区的形式存储分支节点。
推理
在这里,我们可以看到“可选性”的多种用途,通过它可以声明一个类型, 可能会存在。FIDL 中有三种可选性:
- 始终存储的类型
不符合要求
因此有一种内置方式可以描述“缺失”通过
null 信封。正在启用
这些类型的可选性不会影响它们所属的消息的电线形状
它只是更改对特定标签有效的值
类型。
union
、vector<T>
、client_end
、server_end
和zx.Handle
都可以通过添加:optional
约束条件使所有类型都是可选的。 通过将value
union
设置为可选,我们能够引入 null条目,采用不存在的value
的形式。这意味着,空bytes
和不存在/空的store
属性是无效值。 - 与上述类型不同,
struct
布局没有额外的空间, 可以存储 null 标头。因此,需要将其封装在 信封、更改所包含邮件的网线形状 位置为确保这种电线修改效果清晰易读,Item
struct
类型必须封装在box<T>
类型模板中。 - 最后,
table
布局始终是可选的。缺失table
只是一种 且未设置任何成员。
树状结构是自然的自引用数据结构:树中的任何节点都可以
包含具有纯数据(本例中为字符串)的叶,或具有
节点。这需要递归:Item
的定义现在以传递方式传递
需要依赖自身!在 FIDL 中表示递归类型可能有点棘手,
特别是考虑到我们目前获得的支持
受限。只要有
自引用创建的循环中至少有一个可选类型。对于
实例,在这里我们将 items
struct
成员定义为 box<Item>
,
从而打破 include 循环。
这些更改还大量使用了匿名类型,即
声明是内嵌在它们的唯一使用点,而不是直接使用,
它们自己的顶级 type
声明。默认情况下
生成的语言绑定中的类型取自其本地上下文。对于
实例中,新引入的 flexible union
会使用其所属成员的
名称为 Value
,新引入的 struct
将变为 Store
,依此类推。
由于这种启发法有时可能会导致冲突,因此 FIDL 提供了一种逃逸方法。
允许作者手动替换匿名类型生成的
名称。这是通过 @generated_name
属性完成的,该属性允许
更改后端生成的名称在这里,我们可以使用
Store
类型已重命名为 NestedStore
,以防止与
protocol
声明。
实现
FIDL、CML 和 Realm 接口定义修改如下:
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. // Note: For the clarity of this example, allow code to be unused. #![allow(dead_code)] 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. 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
数组通常用于“over”一
底层子类型,该类型用于控制其在线路上的位宽。
推理
键值存储基准 示例 实施是一个很好的起点,但也存在一个主要缺点,那就是数据 存储为原始字节。FIDL 是一种丰富的类型语言。强制用于 例如,要存储为非类型字节数组的 UTF-8 字符串将清除 对于 *.fidl 文件以及 程序员使用由该文件生成的绑定
实现
这项更改的主要目标是替换基准支持请求的vector<byte>
具备 union
的 value
成员,其中存储了多种可能的类型。事实上,作为
填写一份有关 FIDL 的
value 类型已启用
优惠:
- FIDL 的所有内置标量类型都用作
Value
中的变体union
:bool
、uint8
、uint16
、uint32
、uint64
、int8
、int16
、int32
、int64
、float32
和float64
(也称为 FIDL) 基元类型),以及string
。 - 此
union
还使用了 FIDL 的内置array<T, N>
和vector<T>
类型的模板。 - FIDL 的所有类型布局,即
bits
、enum
、table
、union
和struct
,在此示例中至少使用了一次。
用于 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}; use fuchsia_component::server::ServiceFs; use futures::prelude::*; use lazy_static::lazy_static; use regex::Regex; use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::HashMap; use fidl_examples_keyvaluestore_usegenericvalues::{ Item, StoreRequest, StoreRequestStream, Value, WriteError, WriteOptions, }; use std::collections::hash_map::OccupiedEntry; use 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
文件本身)是创建新的 API 时
FIDL 协议。此外,该计算器还包含必要的 CML 和领域
创建可用作项目 ID 的客户端-服务器模式,
用于任意实现的基架。
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 数据类型,表示一个固定的可能列表。 常量,例如扑克牌中的套装或用户的汽车品牌 可从下拉菜单中选择然后,该值列表会映射到 其中的每个值都对应 所列成员。
在下面的示例中,在枚举
完美契合:枚举可能由
方法调用失败。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}; use config::Config; use fidl_examples_keyvaluestore_addreaditem::{Item, StoreMarker}; use fuchsia_component::client::connect_to_protocol; use 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() -> ();
调用)之后,
表明它已经可以做更多工作了。这称为受限的
事件模式。
具体实现必须手动应用其中一些规则:客户端
如果收到其未收到的 -> OnDrawn(...);
事件,则必须关闭连接
通过 Ready() -> ();
方法发出请求。
FIDL、CML 和 Realm 接口的定义如下所示:
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}; use config::Config; use fidl_examples_canvas_clientrequesteddraw::{InstanceEvent, InstanceMarker, Point}; use fuchsia_component::client::connect_to_protocol; use futures::TryStreamExt; use 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}; use fidl::endpoints::RequestStream as _; use fidl_examples_canvas_clientrequesteddraw::{ BoundingBox, InstanceRequest, InstanceRequestStream, Point, }; use fuchsia_async::{Time, Timer}; use fuchsia_component::server::ServiceFs; use fuchsia_zircon::{self as zx}; use futures::future::join; use futures::prelude::*; use 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 信封。正在启用
这些类型的可选性不会影响它们所属的消息的电线形状
它只是更改对特定标签有效的值
类型。
union
、vector<T>
、client_end
、server_end
和zx.Handle
都可以通过添加:optional
约束条件使所有类型都是可选的。 通过将value
union
设置为可选,我们能够引入 null条目,采用不存在的value
的形式。这意味着,空bytes
和不存在/空的store
属性是无效值。 - 与上述类型不同,
struct
布局没有额外的空间, 可以存储 null 标头。因此,需要将其封装在 信封、更改所包含邮件的网线形状 位置为确保这种电线修改效果清晰易读,Item
struct
类型必须封装在box<T>
类型模板中。 - 最后,
table
布局始终是可选的。缺失table
只是一种 且未设置任何成员。
树状结构是自然的自引用数据结构:树中的任何节点都可以
包含具有纯数据(本例中为字符串)的叶,或具有
节点。这需要递归:Item
的定义现在以传递方式传递
需要依赖自身!在 FIDL 中表示递归类型可能有点棘手,
特别是考虑到我们目前获得的支持
受限。只要有
自引用创建的循环中至少有一个可选类型。对于
实例,在这里我们将 items
struct
成员定义为 box<Item>
,
从而打破 include 循环。
这些更改还大量使用了匿名类型,即
声明是内嵌在它们的唯一使用点,而不是直接使用,
它们自己的顶级 type
声明。默认情况下
生成的语言绑定中的类型取自其本地上下文。对于
实例中,新引入的 flexible union
会使用其所属成员的
名称为 Value
,新引入的 struct
将变为 Store
,依此类推。
由于这种启发法有时可能会导致冲突,因此 FIDL 提供了一种逃逸方法。
允许作者手动替换匿名类型生成的
名称。这是通过 @generated_name
属性完成的,该属性允许
更改后端生成的名称在这里,我们可以使用
Store
类型已重命名为 NestedStore
,以防止与
protocol
声明。
实现
FIDL、CML 和 Realm 接口定义修改如下:
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. // Note: For the clarity of this example, allow code to be unused. #![allow(dead_code)] 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. 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>
发回。使用 Google Cloud 时有两个缺点
这种方法。其一是它承担了
服务器上的备份 - 客户端无需支付任何费用
对服务器而言开销非常高其次,它涉及大量的
复制:客户端几乎肯定会写入生成的备份
发送到某个后备数据存储区(如文件或数据库)。
让它解码这个(可能非常大)FIDL 对象,
在将其转发到任何会执行相关协议的协议时,
会造成很大的浪费
推理
更好的解决方案是使用 zircon 的虚拟内存 对象。您不必不断将字节复制回去并 分桶作业中,我们可以创建一个 VMO 来保存 备份数据,将其发送到服务器,然后将其转发回我们的 目标数据存储区,无需在两者之间反序列化。只要目标数据 存储的协议允许接受使用 VMO 传输的数据, 是完成此类高成本操作的首选方式。事实上 例如,Fuchsia 的文件系统就可以实现此确切模式。使用 这种方法会迫使客户在询问 运行代价高昂的操作,最大限度地减少这两者之间的工作不平衡 各方。
FIDL 值类型可以使用
FIDL 数据持久性二进制格式。我们将保留
在 VMO 中新增了 FIDL 类型 Exportable
。系统会对对象进行编码
并写入该存储空间(在本例中,即是一个 VMO,稍后可保存为
并在需要再次访问数据时从该文件中解码,
其编码、传输和解码的方式与以后在发生时
通过 IPC 使用 FIDL。
为了安全地执行此操作并遵循最小权限原则,
我们应该限制代表 VMO 的句柄可能具备的权限。
输入 handle permission,这是 FIDL 用来描述权限的一流方法
可用于特定标识名类型。在本例中,我们允许 empty
VMO
在 Export
请求中传递到服务器,以便查询大小、
调整大小和写入。当 VMO 返回时,我们移除调整大小的权利和
确保任何进程,甚至远远的恶意操作者
组件可以在其在系统中移动时修改此数据。
实现
FIDL、CML 和 Realm 接口的定义如下所示:
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}; use config::Config; use fuchsia_component::client::connect_to_protocol; use std::{thread, time}; use fidl::unpersist; use fidl_examples_keyvaluestore_supportexports::{Exportable, Item, StoreMarker}; use 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}; use fuchsia_component::server::ServiceFs; use futures::prelude::*; use lazy_static::lazy_static; use regex::Regex; use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::HashMap; use fidl::{persist, Vmo}; use 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 方法 值。唯一可能的故障模式是, channel 返回了故障(例如,无法连接到以下其中一个地址) 端点)。
在此示例中,您将创建一个基本的计算器服务器,显示 先定义然后传送和使用 FIDL 协议所需的基本设置。
首先,您需要定义接口定义和自动化测试框架。通过
接口定义(.fidl
文件本身)是创建新的 API 时
FIDL 协议。此外,该计算器还包含必要的 CML 和领域
创建可用作项目 ID 的客户端-服务器模式,
用于任意实现的基架。
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 配方:指定载荷
命名载荷是直接使用的 struct
、table
或 union
类型
作为方法请求或响应载荷。这些容器可以用于
方法有效负载会重复,或者已经是使用的命名类型
放在 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}; use config::Config; use fidl_examples_keyvaluestore_addreaditem::{Item, StoreMarker}; use fuchsia_component::client::connect_to_protocol; use 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> |
否 |
所有其他类型(bits
、enum
、array<T, N>
和基元类型)都不可以
都是可选的
在此变体中,我们允许键值存储区将其他键值存储区视为
成员。简而言之,我们把它变成了一棵树。为此,我们会使用
value
的定义,其中定义使用双成员 union
:一种变体
存储使用与之前相同的 vector<byte>
类型的叶节点,而另一个
以其他嵌套存储区的形式存储分支节点。
推理
在这里,我们可以看到“可选性”的多种用途,通过它可以声明一个类型, 可能会存在。FIDL 中有三种可选性:
- 始终存储的类型
不符合要求
因此有一种内置方式可以描述“缺失”通过
null 信封。正在启用
这些类型的可选性不会影响它们所属的消息的电线形状
它只是更改对特定标签有效的值
类型。
union
、vector<T>
、client_end
、server_end
和zx.Handle
都可以通过添加:optional
约束条件使所有类型都是可选的。 通过将value
union
设置为可选,我们能够引入 null条目,采用不存在的value
的形式。这意味着,空bytes
和不存在/空的store
属性是无效值。 - 与上述类型不同,
struct
布局没有额外的空间, 可以存储 null 标头。因此,需要将其封装在 信封、更改所包含邮件的网线形状 位置为确保这种电线修改效果清晰易读,Item
struct
类型必须封装在box<T>
类型模板中。 - 最后,
table
布局始终是可选的。缺失table
只是一种 且未设置任何成员。
树状结构是自然的自引用数据结构:树中的任何节点都可以
包含具有纯数据(本例中为字符串)的叶,或具有
节点。这需要递归:Item
的定义现在以传递方式传递
需要依赖自身!在 FIDL 中表示递归类型可能有点棘手,
特别是考虑到我们目前获得的支持
受限。只要有
自引用创建的循环中至少有一个可选类型。对于
实例,在这里我们将 items
struct
成员定义为 box<Item>
,
从而打破 include 循环。
这些更改还大量使用了匿名类型,即
声明是内嵌在它们的唯一使用点,而不是直接使用,
它们自己的顶级 type
声明。默认情况下
生成的语言绑定中的类型取自其本地上下文。对于
实例中,新引入的 flexible union
会使用其所属成员的
名称为 Value
,新引入的 struct
将变为 Store
,依此类推。
由于这种启发法有时可能会导致冲突,因此 FIDL 提供了一种逃逸方法。
允许作者手动替换匿名类型生成的
名称。这是通过 @generated_name
属性完成的,该属性允许
更改后端生成的名称在这里,我们可以使用
Store
类型已重命名为 NestedStore
,以防止与
protocol
声明。
实现
FIDL、CML 和 Realm 接口定义修改如下:
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. // Note: For the clarity of this example, allow code to be unused. #![allow(dead_code)] 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. 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 的术语来说,这意味着不是发送单个大型 vector<T>
,
FIDL 作者应改为将其转换为 vector<T>:N
的确认消息,
确保页面大小和流控制是 FIDL 合同的一部分。
按顺序迭代是对键值存储的有用操作: 用于返回在 让它按顺序显示
推理
在 FIDL 中,最好使用迭代器来完成,迭代器通常以 可进行此迭代的单独协议。使用单独的 因此使用单独的频道具有诸多好处,包括 通过 主协议
协议 P
的通道连接的客户端和服务器端可以是
以 FIDL 数据类型表示,以 client_end:P
和 server_end:P
的形式表示,
。这些类型统称为“协议结束”,
代表另一种(非@discoverable
)将 FIDL 客户端连接到
相应的服务器:通过现有的 FIDL 连接!
协议终止是一般 FIDL 概念的具体实例:资源 type。资源类型旨在包含 FIDL 句柄,这必不可少 对类型使用方式的额外限制。类型必须始终为 唯一,因为底层资源由其他 capability Manager 调控 (通常是 Zircon 内核)。通过简单的内存中复制此类资源 不让经理参与,是不可行的。为避免此类重复, FIDL 中的所有资源类型始终只能移动。
最后,Iterator
协议本身的 Get()
方法使用
大小限制。这限制了
在单次拉取中传输,从而可以衡量资源使用情况
控制。它还创建了自然的分页边界:而不是一个巨大的转储。
所有结果,则服务器只需在
。
实现
FIDL、CML 和 Realm 接口的定义如下所示:
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}; use config::Config; use fuchsia_component::client::connect_to_protocol; use std::{thread, time}; use fidl::endpoints::create_proxy; use fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker}; use 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}; use fuchsia_component::server::ServiceFs; use futures::prelude::*; use lazy_static::lazy_static; use regex::Regex; use fidl_examples_keyvaluestore_additerator::{ Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest, StoreRequestStream, WriteError, }; use fuchsia_async as fasync; use std::collections::btree_map::Entry; use std::collections::BTreeMap; use std::ops::Bound::*; use std::sync::{Arc, 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(¤t_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>
发回。使用 Google Cloud 时有两个缺点
这种方法。其一是它承担了
服务器上的备份 - 客户端无需支付任何费用
对服务器而言开销非常高其次,它涉及大量的
复制:客户端几乎肯定会写入生成的备份
发送到某个后备数据存储区(如文件或数据库)。
让它解码这个(可能非常大)FIDL 对象,
在将其转发到任何会执行相关协议的协议时,
会造成很大的浪费
推理
更好的解决方案是使用 zircon 的虚拟内存 对象。您不必不断将字节复制回去并 分桶作业中,我们可以创建一个 VMO 来保存 备份数据,将其发送到服务器,然后将其转发回我们的 目标数据存储区,无需在两者之间反序列化。只要目标数据 存储的协议允许接受使用 VMO 传输的数据, 是完成此类高成本操作的首选方式。事实上 例如,Fuchsia 的文件系统就可以实现此确切模式。使用 这种方法会迫使客户在询问 运行代价高昂的操作,最大限度地减少这两者之间的工作不平衡 各方。
FIDL 值类型可以使用
FIDL 数据持久性二进制格式。我们将保留
在 VMO 中新增了 FIDL 类型 Exportable
。系统会对对象进行编码
并写入该存储空间(在本例中,即是一个 VMO,稍后可保存为
并在需要再次访问数据时从该文件中解码,
其编码、传输和解码的方式与以后在发生时
通过 IPC 使用 FIDL。
为了安全地执行此操作并遵循最小权限原则,
我们应该限制代表 VMO 的句柄可能具备的权限。
输入 handle permission,这是 FIDL 用来描述权限的一流方法
可用于特定标识名类型。在本例中,我们允许 empty
VMO
在 Export
请求中传递到服务器,以便查询大小、
调整大小和写入。当 VMO 返回时,我们移除调整大小的权利和
确保任何进程,甚至远远的恶意操作者
组件可以在其在系统中移动时修改此数据。
实现
FIDL、CML 和 Realm 接口的定义如下所示:
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}; use config::Config; use fuchsia_component::client::connect_to_protocol; use std::{thread, time}; use fidl::unpersist; use fidl_examples_keyvaluestore_supportexports::{Exportable, Item, StoreMarker}; use 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}; use fuchsia_component::server::ServiceFs; use futures::prelude::*; use lazy_static::lazy_static; use regex::Regex; use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::HashMap; use fidl::{persist, Vmo}; use 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
。
协议末尾有一个必需的约束条件,指定
。例如,client_end:Foo
表示客户端
Zircon 通道的端点,在该通道上交换的所有消息都将遵循
该 FIDL 协议中定义的方法和事件,而 server_end:Foo
代表相反的端点。
按顺序迭代是对键值存储的有用操作: 用于返回在 让它按顺序显示
推理
在 FIDL 中,最好使用迭代器来完成,迭代器通常以 可进行此迭代的单独协议。使用单独的 因此使用单独的频道具有诸多好处,包括 通过 主协议
协议 P
的通道连接的客户端和服务器端可以是
以 FIDL 数据类型表示,以 client_end:P
和 server_end:P
的形式表示,
。这些类型统称为“协议结束”,
代表另一种(非@discoverable
)将 FIDL 客户端连接到
相应的服务器:通过现有的 FIDL 连接!
协议终止是一般 FIDL 概念的具体实例:资源 type。资源类型旨在包含 FIDL 句柄,这必不可少 对类型使用方式的额外限制。类型必须始终为 唯一,因为底层资源由其他 capability Manager 调控 (通常是 Zircon 内核)。通过简单的内存中复制此类资源 不让经理参与,是不可行的。为避免此类重复, FIDL 中的所有资源类型始终只能移动。
最后,Iterator
协议本身的 Get()
方法使用
大小限制。这限制了
在单次拉取中传输,从而可以衡量资源使用情况
控制。它还创建了自然的分页边界:而不是一个巨大的转储。
所有结果,则服务器只需在
。
实现
FIDL、CML 和 Realm 接口的定义如下所示:
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}; use config::Config; use fuchsia_component::client::connect_to_protocol; use std::{thread, time}; use fidl::endpoints::create_proxy; use fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker}; use 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}; use fuchsia_component::server::ServiceFs; use futures::prelude::*; use lazy_static::lazy_static; use regex::Regex; use fidl_examples_keyvaluestore_additerator::{ Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest, StoreRequestStream, WriteError, }; use fuchsia_async as fasync; use std::collections::btree_map::Entry; use std::collections::BTreeMap; use std::ops::Bound::*; use std::sync::{Arc, 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(¤t_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
文件本身)是创建新的 API 时
FIDL 协议。此外,该计算器还包含必要的 CML 和领域
创建可用作项目 ID 的客户端-服务器模式,
用于任意实现的基架。
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 配方:递归类型
递归类型是指在自身以传递方式引用自身的类型 定义。当某个类型直接引用自身时,可能会发生这种情况;或者 。对于 举个例子,在 tree 数据结构,每个 节点可以只包含数据(“叶”),也可以是其他节点 子节点(“分支”)。在后一种情况下,该节点以递归方式包含 嵌套树定义,根据需要重复最大深度。
FIDL 支持递归类型,前提是“include”中至少有一条链 循环(换句话说,导致返回到 原始类型)是可选的。如果链中没有类型可选,则返回类型 不可编码,因为该类型的每个实例都至少需要一个 广告无限。
在此变体中,我们允许键值存储区将其他键值存储区视为
成员。简而言之,我们把它变成了一棵树。为此,我们会使用
value
的定义,其中定义使用双成员 union
:一种变体
存储使用与之前相同的 vector<byte>
类型的叶节点,而另一个
以其他嵌套存储区的形式存储分支节点。
推理
在这里,我们可以看到“可选性”的多种用途,通过它可以声明一个类型, 可能会存在。FIDL 中有三种可选性:
- 始终存储的类型
不符合要求
因此有一种内置方式可以描述“缺失”通过
null 信封。正在启用
这些类型的可选性不会影响它们所属的消息的电线形状
它只是更改对特定标签有效的值
类型。
union
、vector<T>
、client_end
、server_end
和zx.Handle
都可以通过添加:optional
约束条件使所有类型都是可选的。 通过将value
union
设置为可选,我们能够引入 null条目,采用不存在的value
的形式。这意味着,空bytes
和不存在/空的store
属性是无效值。 - 与上述类型不同,
struct
布局没有额外的空间, 可以存储 null 标头。因此,需要将其封装在 信封、更改所包含邮件的网线形状 位置为确保这种电线修改效果清晰易读,Item
struct
类型必须封装在box<T>
类型模板中。 - 最后,
table
布局始终是可选的。缺失table
只是一种 且未设置任何成员。
树状结构是自然的自引用数据结构:树中的任何节点都可以
包含具有纯数据(本例中为字符串)的叶,或具有
节点。这需要递归:Item
的定义现在以传递方式传递
需要依赖自身!在 FIDL 中表示递归类型可能有点棘手,
特别是考虑到我们目前获得的支持
受限。只要有
自引用创建的循环中至少有一个可选类型。对于
实例,在这里我们将 items
struct
成员定义为 box<Item>
,
从而打破 include 循环。
这些更改还大量使用了匿名类型,即
声明是内嵌在它们的唯一使用点,而不是直接使用,
它们自己的顶级 type
声明。默认情况下
生成的语言绑定中的类型取自其本地上下文。对于
实例中,新引入的 flexible union
会使用其所属成员的
名称为 Value
,新引入的 struct
将变为 Store
,依此类推。
由于这种启发法有时可能会导致冲突,因此 FIDL 提供了一种逃逸方法。
允许作者手动替换匿名类型生成的
名称。这是通过 @generated_name
属性完成的,该属性允许
更改后端生成的名称在这里,我们可以使用
Store
类型已重命名为 NestedStore
,以防止与
protocol
声明。
实现
FIDL、CML 和 Realm 接口定义修改如下:
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. // Note: For the clarity of this example, allow code to be unused. #![allow(dead_code)] 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. 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 句柄是对 capability 的唯一引用,因此 包含的 会继承此行为:它也无法复制。这样, 资源具有感染力:如果某个值类型成为资源类型,则所有类型 传递包含它的对象也必须如此。
按顺序迭代是对键值存储的有用操作: 用于返回在 让它按顺序显示
推理
在 FIDL 中,最好使用迭代器来完成,迭代器通常以 可进行此迭代的单独协议。使用单独的 因此使用单独的频道具有诸多好处,包括 通过 主协议
协议 P
的通道连接的客户端和服务器端可以是
以 FIDL 数据类型表示,以 client_end:P
和 server_end:P
的形式表示,
。这些类型统称为“协议结束”,
代表另一种(非@discoverable
)将 FIDL 客户端连接到
相应的服务器:通过现有的 FIDL 连接!
协议终止是一般 FIDL 概念的具体实例:资源 type。资源类型旨在包含 FIDL 句柄,这必不可少 对类型使用方式的额外限制。类型必须始终为 唯一,因为底层资源由其他 capability Manager 调控 (通常是 Zircon 内核)。通过简单的内存中复制此类资源 不让经理参与,是不可行的。为避免此类重复, FIDL 中的所有资源类型始终只能移动。
最后,Iterator
协议本身的 Get()
方法使用
大小限制。这限制了
在单次拉取中传输,从而可以衡量资源使用情况
控制。它还创建了自然的分页边界:而不是一个巨大的转储。
所有结果,则服务器只需在
。
实现
FIDL、CML 和 Realm 接口的定义如下所示:
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}; use config::Config; use fuchsia_component::client::connect_to_protocol; use std::{thread, time}; use fidl::endpoints::create_proxy; use fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker}; use 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}; use fuchsia_component::server::ServiceFs; use futures::prelude::*; use lazy_static::lazy_static; use regex::Regex; use fidl_examples_keyvaluestore_additerator::{ Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest, StoreRequestStream, WriteError, }; use fuchsia_async as fasync; use std::collections::btree_map::Entry; use std::collections::BTreeMap; use std::ops::Bound::*; use std::sync::{Arc, 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(¤t_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>
具备 union
的 value
成员,其中存储了多种可能的类型。事实上,作为
填写一份有关 FIDL 的
value 类型已启用
优惠:
- FIDL 的所有内置标量类型都用作
Value
中的变体union
:bool
、uint8
、uint16
、uint32
、uint64
、int8
、int16
、int32
、int64
、float32
和float64
(也称为 FIDL) 基元类型),以及string
。 - 此
union
还使用了 FIDL 的内置array<T, N>
和vector<T>
类型的模板。 - FIDL 的所有类型布局,即
bits
、enum
、table
、union
和struct
,在此示例中至少使用了一次。
用于 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}; use fuchsia_component::server::ServiceFs; use futures::prelude::*; use lazy_static::lazy_static; use regex::Regex; use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::HashMap; use fidl_examples_keyvaluestore_usegenericvalues::{ Item, StoreRequest, StoreRequestStream, Value, WriteError, WriteOptions, }; use std::collections::hash_map::OccupiedEntry; use 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
的通道连接的客户端和服务器端可以是
以 FIDL 数据类型表示,以 client_end:P
和 server_end:P
的形式表示,
。这些类型统称为“协议结束”,
代表另一种(非@discoverable
)将 FIDL 客户端连接到
相应的服务器:通过现有的 FIDL 连接!
协议终止是一般 FIDL 概念的具体实例:资源 type。资源类型旨在包含 FIDL 句柄,这必不可少 对类型使用方式的额外限制。类型必须始终为 唯一,因为底层资源由其他 capability Manager 调控 (通常是 Zircon 内核)。通过简单的内存中复制此类资源 不让经理参与,是不可行的。为避免此类重复, FIDL 中的所有资源类型始终只能移动。
最后,Iterator
协议本身的 Get()
方法使用
大小限制。这限制了
在单次拉取中传输,从而可以衡量资源使用情况
控制。它还创建了自然的分页边界:而不是一个巨大的转储。
所有结果,则服务器只需在
。
实现
FIDL、CML 和 Realm 接口的定义如下所示:
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}; use config::Config; use fuchsia_component::client::connect_to_protocol; use std::{thread, time}; use fidl::endpoints::create_proxy; use fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker}; use 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}; use fuchsia_component::server::ServiceFs; use futures::prelude::*; use lazy_static::lazy_static; use regex::Regex; use fidl_examples_keyvaluestore_additerator::{ Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest, StoreRequestStream, WriteError, }; use fuchsia_async as fasync; use std::collections::btree_map::Entry; use std::collections::BTreeMap; use std::ops::Bound::*; use std::sync::{Arc, 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(¤t_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 方法载荷,它使用
struct
布局。struct
是类型化字段的简单序列,类似于
C 结构体的工作原理。
在此示例中,您将创建一个基本的计算器服务器,显示 先定义然后传送和使用 FIDL 协议所需的基本设置。
首先,您需要定义接口定义和自动化测试框架。通过
接口定义(.fidl
文件本身)是创建新的 API 时
FIDL 协议。此外,该计算器还包含必要的 CML 和领域
创建可用作项目 ID 的客户端-服务器模式,
用于任意实现的基架。
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 方法载荷。通过
用作方法载荷的顶级类型必须使用 struct
、table
或
union
作为其布局。值得注意的是,生成的一些绑定参数
struct
方法载荷,以便每个成员本身都被视为
调用签名中的函数参数。使用 table
或 union
的载荷
绝不这样做,而是始终传递一个名为 payload
的参数。
推理
键值存储基准 示例 实施是一个很好的起点,但也存在一个主要缺点,那就是数据 存储为原始字节。FIDL 是一种丰富的类型语言。强制用于 例如,要存储为非类型字节数组的 UTF-8 字符串将清除 对于 *.fidl 文件以及 程序员使用由该文件生成的绑定
实现
这项更改的主要目标是替换基准支持请求的vector<byte>
具备 union
的 value
成员,其中存储了多种可能的类型。事实上,作为
填写一份有关 FIDL 的
value 类型已启用
优惠:
- FIDL 的所有内置标量类型都用作
Value
中的变体union
:bool
、uint8
、uint16
、uint32
、uint64
、int8
、int16
、int32
、int64
、float32
和float64
(也称为 FIDL) 基元类型),以及string
。 - 此
union
还使用了 FIDL 的内置array<T, N>
和vector<T>
类型的模板。 - FIDL 的所有类型布局,即
bits
、enum
、table
、union
和struct
,在此示例中至少使用了一次。
用于 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}; use fuchsia_component::server::ServiceFs; use futures::prelude::*; use lazy_static::lazy_static; use regex::Regex; use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::HashMap; use fidl_examples_keyvaluestore_usegenericvalues::{ Item, StoreRequest, StoreRequestStream, Value, WriteError, WriteOptions, }; use std::collections::hash_map::OccupiedEntry; use 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() -> ();
调用)之后,
表明它已经可以做更多工作了。这称为受限的
事件模式。
具体实现必须手动应用其中一些规则:客户端
如果收到其未收到的 -> OnDrawn(...);
事件,则必须关闭连接
通过 Ready() -> ();
方法发出请求。
FIDL、CML 和 Realm 接口的定义如下所示:
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}; use config::Config; use fidl_examples_canvas_clientrequesteddraw::{InstanceEvent, InstanceMarker, Point}; use fuchsia_component::client::connect_to_protocol; use futures::TryStreamExt; use 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}; use fidl::endpoints::RequestStream as _; use fidl_examples_canvas_clientrequesteddraw::{ BoundingBox, InstanceRequest, InstanceRequestStream, Point, }; use fuchsia_async::{Time, Timer}; use fuchsia_component::server::ServiceFs; use fuchsia_zircon::{self as zx}; use futures::future::join; use futures::prelude::*; use 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 方法载荷。通过
用作方法载荷的顶级类型必须使用 struct
、table
或
union
作为其布局。值得注意的是,生成的一些绑定参数
struct
方法载荷,以便每个成员本身都被视为
调用签名中的函数参数。使用 table
或 union
的载荷
绝不这样做,而是始终传递一个名为 payload
的参数。
推理
键值存储基准 示例 实施是一个很好的起点,但也存在一个主要缺点,那就是数据 存储为原始字节。FIDL 是一种丰富的类型语言。强制用于 例如,要存储为非类型字节数组的 UTF-8 字符串将清除 对于 *.fidl 文件以及 程序员使用由该文件生成的绑定
实现
这项更改的主要目标是替换基准支持请求的vector<byte>
具备 union
的 value
成员,其中存储了多种可能的类型。事实上,作为
填写一份有关 FIDL 的
value 类型已启用
优惠:
- FIDL 的所有内置标量类型都用作
Value
中的变体union
:bool
、uint8
、uint16
、uint32
、uint64
、int8
、int16
、int32
、int64
、float32
和float64
(也称为 FIDL) 基元类型),以及string
。 - 此
union
还使用了 FIDL 的内置array<T, N>
和vector<T>
类型的模板。 - FIDL 的所有类型布局,即
bits
、enum
、table
、union
和struct
,在此示例中至少使用了一次。
用于 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}; use fuchsia_component::server::ServiceFs; use futures::prelude::*; use lazy_static::lazy_static; use regex::Regex; use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::HashMap; use fidl_examples_keyvaluestore_usegenericvalues::{ Item, StoreRequest, StoreRequestStream, Value, WriteError, WriteOptions, }; use std::collections::hash_map::OccupiedEntry; use 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.