在 Rust 中實作 FIDL 伺服器

必要條件

本教學課程假設您已熟悉如何將程式庫的 FIDL Rust 繫結項目列為 GN 中的依附元件,並將繫結項目匯入 Rust 程式碼,這方面的資訊請參閱 Rust FIDL crates 教學課程。

總覽

本教學課程說明如何實作 FIDL 通訊協定 (fuchsia.examples.Echo),並在 Fuchsia 上執行。這個通訊協定包含每種類型各一個方法:

  • EchoString 是含有回應的方法。
  • SendString 是沒有回應的方法。
  • OnString 是事件。
@discoverable
closed protocol Echo {
    strict EchoString(struct {
        value string:MAX_STRING_LENGTH;
    }) -> (struct {
        response string:MAX_STRING_LENGTH;
    });
    strict SendString(struct {
        value string:MAX_STRING_LENGTH;
    });
    strict -> OnString(struct {
        response string:MAX_STRING_LENGTH;
    });
};

如要進一步瞭解 FIDL 方法和訊息傳遞模式,請參閱 FIDL 概念頁面。

本文件說明如何完成下列工作:

  • 實作 FIDL 通訊協定。
  • 在 Fuchsia 上建構及執行套件。
  • 提供 FIDL 通訊協定。

本教學課程會先建立要提供給 Fuchsia 裝置並執行的元件。然後逐步新增功能,讓伺服器順利啟動。

如果您想自行編寫程式碼,請刪除下列目錄:

rm -r examples/fidl/rust/server/*

建立元件

如何建立元件:

  1. main() 函式新增至 examples/fidl/rust/server/src/main.rs

    fn main() {
      println!("Hello, world!");
    }
    
  2. examples/fidl/rust/server/BUILD.gn 中宣告伺服器的目標:

    import("//build/rust/rustc_binary.gni")
    
    
    # Declare an executable for the server. This produces a binary with the
    # specified output name that can run on Fuchsia.
    rustc_binary("bin") {
      output_name = "fidl_echo_rust_server"
      edition = "2021"
    
      sources = [ "src/main.rs" ]
    }
    
    # Declare a component for the server, which consists of the manifest and the
    # binary that the component will run.
    fuchsia_component("echo-server") {
      component_name = "echo_server"
      manifest = "meta/server.cml"
      deps = [ ":bin" ]
    }
    
    # Declare a package that contains a single component, our server.
    fuchsia_package("echo-rust-server") {
      deps = [ ":echo-server" ]
    }
    

    為了讓伺服器元件啟動並執行,我們定義了三個目標:

    • 用於 Fuchsia 的伺服器原始執行檔。
    • 這個元件會設定為只執行伺服器可執行檔,並使用元件的資訊清單檔案加以說明。
    • 接著,元件會放入套件中,這是 Fuchsia 上的軟體發行單位。在本例中,套件只包含單一元件。

    如要進一步瞭解套件、元件和建構方式,請參閱「建構元件」頁面。

  3. examples/fidl/rust/server/meta/server.cml 中新增元件資訊清單:

    {
        include: [ "syslog/client.shard.cml" ],
    
        // Information about the program to run.
        program: {
            // Use the built-in ELF runner.
            runner: "elf",
    
            // The binary to run for this component.
            binary: "bin/fidl_echo_rust_server",
        },
    
        // Capabilities provided by this component.
        capabilities: [
            { protocol: "fuchsia.examples.Echo" },
        ],
        expose: [
            {
                protocol: "fuchsia.examples.Echo",
                from: "self",
            },
        ],
    }
    
    
  4. 將伺服器新增至建構設定:

    fx set core.x64 --with //examples/fidl/rust/server:echo-rust-server
  5. 建構 Fuchsia 映像檔:

    fx build

實作伺服器

首先,您將實作 Echo 通訊協定的行為。在 Rust 中,這會以程式碼表示,可處理通訊協定的相關聯要求串流類型,在本例中為 EchoRequestStream。這個類型是 Echo 要求的串流,也就是實作 futures::Stream<Item = Result<EchoRequest, fidl::Error>>

您將實作 run_echo_server() 來處理要求串流,這是處理傳入服務要求的非同步函式。它會傳回 Future,在用戶端管道關閉時完成。

新增依附元件

  1. 匯入必要的依附元件:

    // we'll use anyhow to propagate errors that occur when handling the request stream
    use anyhow::{Context as _, Error};
    // the server will need to handle an EchoRequestStream
    use fidl_fuchsia_examples::{EchoRequest, EchoRequestStream};
    // import the futures prelude, which includes things like the Future and Stream traits
    use futures::prelude::*;
    
  2. 將這些依附元件新增為 rustc_binary 目標的建構依附元件。deps 欄位的內容應如下所示:

    deps = [
      "//examples/fidl/fuchsia.examples:fuchsia.examples_rust",
      "//third_party/rust_crates:anyhow",
      "//third_party/rust_crates:futures",
    ]
    

定義 run_echo_server

// An implementation of the Echo stream, which handles a stream of EchoRequests
async fn run_echo_server(stream: EchoRequestStream) -> Result<(), Error> {
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async move {
            match request {
                // Handle each EchoString request by responding with the request
                // value
                EchoRequest::EchoString { value, responder } => {
                    println!("Received EchoString request for string {:?}", value);
                    responder.send(&value).context("error sending response")?;
                    println!("Response sent successfully");
                }
                // Handle each SendString request by sending a single OnString
                // event with the request value
                EchoRequest::SendString { value, control_handle } => {
                    println!("Received SendString request for string {:?}", value);
                    control_handle.send_on_string(&value).context("error sending event")?;
                    println!("Event sent successfully");
                }
            }
            Ok(())
        })
        .await
}

實作內容包含下列元素:

  • 這個程式碼會在每個結果上使用 .context() 方法附加內容,將要求串流中的 fidl:Error 轉換為 anyhow::Error

    // An implementation of the Echo stream, which handles a stream of EchoRequests
    async fn run_echo_server(stream: EchoRequestStream) -> Result<(), Error> {
        stream
            .map(|result| result.context("failed request"))
            .try_for_each(|request| async move {
                match request {
                    // Handle each EchoString request by responding with the request
                    // value
                    EchoRequest::EchoString { value, responder } => {
                        println!("Received EchoString request for string {:?}", value);
                        responder.send(&value).context("error sending response")?;
                        println!("Response sent successfully");
                    }
                    // Handle each SendString request by sending a single OnString
                    // event with the request value
                    EchoRequest::SendString { value, control_handle } => {
                        println!("Received SendString request for string {:?}", value);
                        control_handle.send_on_string(&value).context("error sending event")?;
                        println!("Event sent successfully");
                    }
                }
                Ok(())
            })
            .await
    }
    

    在此階段,Result<EchoRequest, fidl::Error> 的串流會變成 Result<EchoRequest, anyhow::Error> 的串流。

  • 接著,函式會在產生的串流上呼叫 try_for_each,並傳回 Future。這個方法會展開串流中的 Result。如果發生任何失敗,未來會立即傳回該錯誤,而成功的內容會傳遞至結束函式。同樣地,如果結束函式傳回的值解析為失敗,產生的未來值會立即傳回該錯誤:

    // An implementation of the Echo stream, which handles a stream of EchoRequests
    async fn run_echo_server(stream: EchoRequestStream) -> Result<(), Error> {
        stream
            .map(|result| result.context("failed request"))
            .try_for_each(|request| async move {
                match request {
                    // Handle each EchoString request by responding with the request
                    // value
                    EchoRequest::EchoString { value, responder } => {
                        println!("Received EchoString request for string {:?}", value);
                        responder.send(&value).context("error sending response")?;
                        println!("Response sent successfully");
                    }
                    // Handle each SendString request by sending a single OnString
                    // event with the request value
                    EchoRequest::SendString { value, control_handle } => {
                        println!("Received SendString request for string {:?}", value);
                        control_handle.send_on_string(&value).context("error sending event")?;
                        println!("Event sent successfully");
                    }
                }
                Ok(())
            })
            .await
    }
    
  • 結束處理內容會透過比對傳入的 EchoRequest,判斷其為何種要求:

    // An implementation of the Echo stream, which handles a stream of EchoRequests
    async fn run_echo_server(stream: EchoRequestStream) -> Result<(), Error> {
        stream
            .map(|result| result.context("failed request"))
            .try_for_each(|request| async move {
                match request {
                    // Handle each EchoString request by responding with the request
                    // value
                    EchoRequest::EchoString { value, responder } => {
                        println!("Received EchoString request for string {:?}", value);
                        responder.send(&value).context("error sending response")?;
                        println!("Response sent successfully");
                    }
                    // Handle each SendString request by sending a single OnString
                    // event with the request value
                    EchoRequest::SendString { value, control_handle } => {
                        println!("Received SendString request for string {:?}", value);
                        control_handle.send_on_string(&value).context("error sending event")?;
                        println!("Event sent successfully");
                    }
                }
                Ok(())
            })
            .await
    }
    

    這個實作會透過回應輸入內容來處理 EchoString 要求,並透過傳送 OnString 事件來處理 SendString 要求。由於 SendString 是立即執行並忘記的方法,因此要求列舉變化版本會隨附控制項句柄,可用於與伺服器通訊。

    在上述兩種情況中,傳回訊息給用戶端時發生錯誤時,系統會透過新增內容並使用 ? 運算子來傳播錯誤。如果成功到達結束函式,就會傳回 Ok(())

  • 最後,伺服器函式會將從 try_for_each 傳回的 future await 至完成狀態,這會針對每個傳入的要求呼叫結束函式,並在處理完所有要求或遇到任何錯誤時傳回。

您可以執行下列指令,驗證實作方式是否正確:

fx build

放送通訊協定

定義用於處理傳入要求的程式碼後,您需要聆聽與 Echo 伺服器的傳入連線。方法是要求元件管理員將 Echo 通訊協定公開給其他元件。元件管理員隨後會將任何回音通訊協定要求轉送至我們的伺服器。

為滿足這些要求,元件管理員需要通訊協定的名稱,以及在有任何傳入要求時,應呼叫的處理常式,以便連線至符合指定名稱的通訊協定。

新增依附元件

  1. 匯入必要的依附元件:

    // Import the Fuchsia async runtime in order to run the async main function
    use fuchsia_async as fasync;
    // ServiceFs is a filesystem used to connect clients to the Echo service
    use fuchsia_component::server::ServiceFs;
    
  2. 將這些依附元件新增為 rustc_binary 目標的建構依附元件。完整目標如下所示:

    rustc_binary("bin") {
      name = "fidl_echo_rust_server"
      edition = "2021"
    
      deps = [
        "//examples/fidl/fuchsia.examples:fuchsia.examples_rust",
        "//src/lib/fuchsia",
        "//src/lib/fuchsia-component",
        "//third_party/rust_crates:anyhow",
        "//third_party/rust_crates:futures",
      ]
    
      sources = [ "src/main.rs" ]
    }
    
    

定義 main 函式

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

主函式是異步的,因為它會監聽 Echo 伺服器的傳入連線。fuchsia::main 屬性會告知 Fuchsia 非同步執行階段,在單一執行緒上執行 main Future 至完成。

main 也會傳回 Result<(), Error>。如果 ? 其中一行導致 main 傳回 Error,系統會印出 Debug 錯誤,並傳回表示失敗的狀態碼。

初始化 ServiceFs

取得 ServiceFs 的例項,代表包含各種服務的檔案系統。由於伺服器會以單執行緒執行,請使用 ServiceFs::new_local() 而非 ServiceFs::new() (後者可支援多執行緒)。

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

新增 Echo FIDL 服務

請元件管理員公開 Echo FIDL 服務。這個函式呼叫包含兩個部分:

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}
  • 元件管理員必須知道如何處理傳入的連線要求。您可以傳入可接受 fidl::endpoints::RequestStream 的函式,並透過該函式傳回一些新值,藉此指定此值。舉例來說,傳入 |stream: EchoRequestStream| stream 的結束函式會完全有效。常見模式是定義伺服器提供的可能服務的列舉,如以下範例所示:

    enum IncomingService {
        // Host a service protocol.
        Echo(EchoRequestStream),
        // ... more services here
    }
    

    然後將列舉變數「constructor」做為封閉式函式傳遞。如果提供多項服務,這會產生常見的傳回型別 (IncomingService 列舉)。監聽傳入連線時,所有 add_fidl_service 函式封閉的傳回值都會成為 ServiceFs 串流中的元素。

  • 元件管理員也必須知道這項服務可供使用的位置。由於這是傳出服務 (即提供給其他元件的服務),因此服務必須在 /svc 目錄中新增路徑。add_fidl_service 會透過取得與關閉輸入引數相關聯的 SERVICE_NAME,隱含取得這個路徑。在本例中,關閉引數 (IncomingService::Echo) 具有 EchoRequestStream 類型的輸入引數,而該輸入引數具有 "fuchsia.examples.Echo" 的相關聯 SERVICE_NAME。因此,這個呼叫會在 /svc/fuchsia.examples.Echo 中新增項目,用戶端需要搜尋名為 "fuchsia.examples.Echo" 的服務,才能連線至這個伺服器。

放送傳出目錄

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

這個呼叫會將 ServiceFs 繫結至元件的 DirectoryRequest 啟動句柄,並監聽傳入的連線要求。請注意,由於這會從程序的句柄表格中移除句柄,因此每個程序只能呼叫這個函式一次。如果您想將 ServiceFs 提供給其他管道,可以使用 serve_connection 函式。

如要進一步瞭解這項程序,請參閱「通訊協定開啟的生命週期」一文。

監聽傳入的連線

執行 ServiceFs 至完成,以便監聽傳入的連線:

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

這會執行 ServiceFs 未來事件,並同時處理最多 10,000 個傳入要求。傳遞至此呼叫的結束函式是用於處理傳入要求的處理常式 - ServiceFs 會先將傳入連線與提供給 add_fidl_service 的結束函式進行比對,然後在結果 (即 IncomingService) 上呼叫處理常式。處理常式會取得 IncomingService,並在內部要求串流上呼叫 run_echo_server,以處理傳入的 Echo 要求。

這裡會處理兩種要求。ServiceFs 處理的要求串流包含連線至 Echo 伺服器的要求 (也就是說,每個用戶端在連線至伺服器時會提出這類要求一次),而 run_echo_server 處理的要求串流則是 Echo 通訊協定的要求 (也就是說,每個用戶端可能向伺服器提出任意數量的 EchoStringSendString 要求)。許多用戶端可以同時要求連線至 Echo 伺服器,因此這類要求會同時處理。不過,單一用戶端的所有要求都會依序發生,因此並行處理要求並無益處。

測試伺服器

重建:

fx build

然後執行伺服器元件:

ffx component run /core/ffx-laboratory:echo_server fuchsia-pkg://fuchsia.com/echo-rust-server#meta/echo_server.cm

請注意,這個值是由 fuchsia-pkg://決定。

您應該會在裝置記錄 (ffx log) 中看到類似以下的輸出內容:

[ffx-laboratory:echo_server][][I] Listening for incoming connections...

伺服器現已開始執行,並等待傳入的要求。接下來,我們將編寫用於傳送 Echo 通訊協定要求的用戶端。目前,您可以直接終止伺服器元件:

ffx component destroy /core/ffx-laboratory:echo_server