RFC-0158:結構化設定存取者

RFC-0158:結構化設定存取子
狀態已接受
區域
  • 元件架構
說明

使用者面向設定存取子的基本原則和需求。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2021-12-20
審查日期 (年-月-日)2022-05-04

摘要

使用者面向的產生型存取子程式庫實作 RFC-0127 結構化設定的需求、設計理念和高階實作詳細資料。

提振精神

這項 RFC 以結構化設定的 RFC 中非規範性的使用者面向 API 範例為基礎,概述了所產生程式庫的設計原則和限制,開發人員將使用這些程式庫存取設定。

利害關係人

導師:hjfreyer@google.com

審查者:

  • geb@google.com (元件架構)
  • jsankey@google.com (RFC-0127 作者)
  • yifeit@google.com (FIDL)

諮詢對象:xbhatnag@google.com、hjfreyer@google.com、jamesr@google.com

社交化:本 RFC 是元件架構團隊和 FIDL 團隊成員討論的產品,回答了在原型設計 RFC-0127 初始階段時出現的問題。

設計

RFC-0127 描繪了面向使用者的介面,可在程式碼中取用設定值。這項工具會提供專為元件設定產生的靜態型別程式庫,並具有單一進入點,保證會傳回有效設定。

目標

存取器有幾項目標:應傳達無誤性、相對於覆寫是不透明的、可偵錯,並提供 Fuchsia 開發人員熟悉的型別介面,盡可能重複使用現有工具。

Infallible

元件作者必須能夠假設值一律可用,且所有欄位一律會填入值。設定欄位的可為空值性應與輸入宣告相符 (撰寫本文時,設定結構定義不支援可為空值的欄位),且存取子函式應不會出錯,也就是不會傳回錯誤或擲回例外狀況。如果存取子收到無法剖析的設定酬載,應快速失敗並終止元件執行 (例如 abort())。

不透明

根據 RFC-0127,元件架構最終會提供 API,用於在執行階段覆寫元件的設定值。從作者的角度來看,剖析的型別不會說明可能發生的任何覆寫。產生的程式庫不負責處理覆寫。

可偵錯

存取子應提供選項,將元件的設定輸出為檢查階層。這樣一來,設定就能納入當機報告,且只需加入少量程式碼,不必強制元件架構將所有設定值儲存在記憶體中,複製元件本身的副本。

除了產生檢查輸出內容,您也可以在元件管理員的檢查中公開設定。

符合人體工學

產生的程式庫應具有單一頂層型別,其中包含元件的所有設定欄位。應由單一頂層存取子函式傳回。

使用者應只與單一產生程式庫的命名空間或程式庫名稱互動,且可控制該程式庫。

熟悉

為設定結構定義產生的型別,應讓 FIDL 繫結使用者感到熟悉。

語法

請考慮使用 RFC-0146 的語法,採用以下 config 節:

config: {
    check_interval_ns: { type: "int64" },
    data_path: {
        type: "string",
        max_size: 256,
    },
    test_only: { type: "bool" },
},

建構範本

fuchsia.git 中,存取子必須與定義設定鍵的元件位於同一個建構檔案。舉例來說,如果是 Rust 二進位檔:

import("//build/components.gni")
import("//build/rust/rustc_binary.gni")

fuchsia_component("my_component") {
  manifest = "meta/my_component.cml"
  deps = [ ":my_bin" ]
}

fuchsia_structured_config_rust_lib("my_component_config") {
  library_name = "my_component"

  # NOTE: This target internally depends on a target which is generated by the
  # fuchsia_component() template, so the graph is not cyclic:
  #
  # :my_component -> :my_bin -> :config_lib -> :my_component_manifest_compile
  #
  # where :my_component_manifest_compile does not depend on :my_component
  component = ":my_component"

  # By default all fields are included, this Inspect won't have `data_path`
  inspect_skip_fields = ["data_path"]
}

rustc_binary("my_bin") {
  sources = [ "src/main.rs" ]
  deps = [ ":my_component_config" ]
}

fuchsia_structured_config_values("my_config_values") {
  component = ":my_component"
  values_source = "config/my_component.json5"
}

fuchsia_package("my_package") {
  deps = [
    ":my_component",
    ":my_config_values",
  ]
}

當結構化設定可供 OOT 使用時,我們會為樹狀結構外客戶開發類似的建構整合。

C++

#include "src/my_project/my_component/config.h"

int main(int argc, char** argv) {
  auto config = my_component::Config::TakeFromStartupHandle();

  auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory();
  auto inspector_ = std::make_unique<sys::ComponentInspector>(context);
  config.RecordInspect(inspector_.GetRoot().CreateChild("config"));

  if config.test_only() {
    // ...
  }

  std::ifstream file(config.data_path());
  // ...

  while (true) {
    // ...
    std::this_thread::sleep_for(
      std::chrono::nanoseconds(config.check_interval_ns()));
  }
}

對於驅動程式庫,必須提供啟動引數,才能避免程序全域依附元件:

#include "src/my_project/my_component/config.h"

zx_status<> Init(fdf::wire::DriverStartArgs& start_args, /*...*/) {
  auto config = my_component::Config::TakeFromStartArgs(start_args);
  // ...
}

在 fuchsia.git 中建構範本會在 target_gen_dir 中放置標頭,並將其新增至程式庫的 include_dirs,讓使用者能將產生的標頭做為元件的實作標頭

在 SDK 中建構存取子支援時,我們會確保使用者可以設定 include 目錄版面配置,以符合任何樣式指南。

荒漠油廠

#[fuchsia::component]
async fn main() {
    let inspector = fuchsia_inspect::component::inspector();
    let config_node = inspector.root().create_child("config");

    let config = my_component::Config::take_from_args();
    config.record_inspect(&config_node);

    if config.test_only {
        // ...
    }

    let contents = std::fs::read(&config.data_path).unwrap();
    let mut interval = Interval::new(Duration::from_nanos(config.check_interval_ns));
    let _checker = Task::local(async move {
        while let Some(()) = interval.next().await {
            // ... use `contents` ...
        }
    });

    // for completeness, set up an /out directory for our inspect and serve it
    let mut fs = ServiceFs::new_local();
    inspect_runtime::serve(inspector, &mut fs).unwrap();
    fs.take_and_serve_directory_handle().unwrap();
    while let Some(()) = fs.next().await {}
}

請注意,這個範例使用 async 和 VFS 來示範如何提供 Inspect,但您不需要執行器和 VFS 實作項目,即可存取結構化設定值。

實作

版本管理

存取子會從元件架構接收設定值,以及這些值編碼所用的設定結構定義的總和檢查碼 (如需背景資訊,請參閱 CML 中設定的 RFC,未來的 RFC 會指定傳送機制)。存取器必須檢查收到的檢查和是否與產生存取器時的檢查和完全相符,如果不符,則中止元件。這可避免誤解酬載,並做為最後一道防線,防止從不同結構定義編譯的元件二進位檔和/或資訊清單包裝錯誤。

這項設計的影響是,元件的設定結構定義變更時,必須重新編譯。

遭拒的替代方案是讓執行元件與元件管理服務合作,在啟動元件前驗證總和檢查碼。

程式庫內部

根據 RFC-0127,結構化設定酬載會編碼為持續性 FIDL 訊息,並以結構體做為主要物件 (詳細資料將記錄在日後的 RFC 中)。

我們會產生 FIDL 程式庫來剖析編碼訊息,也會產生小型執行階段專屬的包裝函式程式庫,瞭解如何

  1. 從語言或執行元件專屬的執行階段擷取編碼訊息
  2. 根據元件資訊清單檢查編碼訊息的總和檢查碼
  3. 叫用 FIDL 繫結的解碼功能
  4. 將解碼的 FIDL 網域物件轉換為產生的型別

我們已考慮過替代方案,但決定不採用

命名

產生的包裝函式庫會包含預期供使用者存取設定的 API。根據預設,包裝函式的命名空間會與產生包裝函式的 GN 規則目標名稱相同,並可使用 library_name 引數覆寫。

產生的 FIDL 程式庫會從建構範本取得名稱,預設為移除底線的 GN 目標名稱,並附加至平台名稱 (樹狀結構內元件為 fuchsia.)。舉例來說,在上述語法片段中,GN 範本調用會建立 fuchsia.mycomponent 的 FIDL 程式庫名稱。

產生的結構體會命名為 Config,如果這是常見要求的功能,我們可能會允許使用者覆寫該結構體。

設定欄位 ID 的規則表示 CML 中的所有設定鍵都是有效的 FIDL 欄位 ID,不需要進行任何損毀。

產生的 FIDL 程式庫

每個結構化設定結構定義都會編譯為 FIDL 結構,並轉換為結構化設定包裝函式中定義的型別。使用者不會接觸到 FIDL 工具鍊產生的型別。

以上述語法範例來說,這會是:

library fuchsia.mycomponent;

type Config = struct {
    check_interval_ns int64;
    data_path string:256;
    test_only bool;
};

產生的包裝函式程式碼

每個封裝容器都會包含對應至產生的 FIDL 網域物件類型的型別,但會額外提供工廠函式,用於從元件的執行階段擷取設定,以及記錄設定以供檢查的方法。

產生的 C++ 程式庫會提供介面,讓統一 C++ FIDL 繫結中的自然型別使用者感到熟悉。舉例來說,上述語法範例會在 C++ 中產生下列內容:

namespace my_component {
class Config {
public:
  static Config TakeFromStartupHandle() noexcept;
  void RecordInspect(inspect::Node* node);

  const uint64_t& check_interval_ns() const { return check_interval_ns_; }
  uint64_t& check_interval_ns() { return check_interval_ns_; }

  const std::string& data_path() const { return data_path_; }
  std::string& data_path() { return data_path_; }

  const bool& test_only() const { return test_only_; }
  bool& test_only() { return test_only_; }

private:
  // ...
};
};

在 Rust 中,我們會產生類似於單一繫結版本的型別。上述語法範例會為 Rust 元件產生以下內容:

pub struct Config {
    pub check_interval_ns: u64,
    pub data_path: String,
    pub test_only: bool,
}

impl Config {
  pub fn take_from_args() -> Self { ... }
  pub fn record_inspect(&self, node: &fuchsia_inspect::Node) { ... }
}

我們會為每種語言或執行階段環境產生一個存取子程式庫「風味」,這些環境會使用不同的方法來傳遞編碼設定 (執行元件實作的詳細資料將在日後的 RFC 中獲得批准)。舉例來說,即使 C++ 驅動程式和由 ELF 執行元件直接執行的 C++ 元件使用相同語言,兩者存取器程式庫仍有所不同。

語言支援

我們最初會支援 C++ 和 Rust。我們將逐步支援所有目標端支援的語言

依附元件

所產生程式庫的每個依附元件,都代表 OOT 整合人員的稅金,因此應盡可能避免。

樹外支援

最終,樹狀結構外客戶必須產生自己的設定存取子,類似於 FIDL 工具鍊與花瓣建構作業的整合方式。

效能

剖析結構化設定酬載可能會增加元件啟動時間的額外負擔,如果元件先前直接將設定值編譯至其二進位檔,影響會最為顯著。我們預期無論如何,使用者都不會受到影響,因為設定剖析作業應該會在元件首次啟動時執行一次。

FIDL 工具鍊產生的剖析器會在內部重複使用,因此可部分減輕這項潛在影響,且這些剖析器的效能已持續進行基準測試。

我們會監控 TimeToStart 成效指標。

回溯相容性

設定存取子程式庫是從元件的已編譯資訊清單產生,並會嵌入與資訊清單相同的設定結構定義總和檢查碼。元件的設定結構定義、值檔案和存取子程式庫之間,如有任何版本差異,我們都不會提供支援。日後,整體元件架構可能會支援設定值的演變,但從元件的角度來看,一律會收到完整且一致的設定。

安全性考量

元件應信任收到的設定酬載會根據宣告的結構定義正確形成,因為元件架構必須提供相容的設定,才能啟動元件。這項解析和編碼的詳細資料將在後續 RFC 中定義。

我們希望提供不會出錯且不可為空值的存取子,能避免在程式碼中使用預設設定值,方便稽核執行階段執行的實際值,並減少可能發生錯誤設定和後續攻擊的範圍。

隱私權注意事項

日後擴充結構化設定時,可能需要變更才能支援使用 PII,但我們預計不會有任何變更,因此設定存取子項不需要留意。

在產生的檢查輔助程式中,不必遮蓋使用者資料,因為 Archivist 會在讀取值時執行遮蓋作業。使用結構化設定的使用者必須將設定欄位新增至產品的選取器許可清單,才能在當機報告中顯示這些欄位。

測試

為支援開發人員編寫測試,應可在程式碼中建構設定物件。

存取子程式庫的使用情形會納入結構化設定的多語言一致性測試,確保設定可由元件載入、解析、編碼、傳送及剖析,並將結果回報給一致性套件。

說明文件

存取子程式庫的說明文件會提供範例,說明結構化設定支援的每種語言。

缺點、替代方案和未知事項

替代做法:在元件管理服務的檢查功能中公開設定

我們可以在元件管理服務的檢查輸出內容中加入已解析的設定值,而非在存取子程式庫中產生檢查程式碼。我們有先例可循,能以 CPU 統計資料為依據,但追蹤這項功能的資源用量很困難,因此我們偏好「向具有設定值的元件收取記憶體用量費用」的解決方案。

替代方案:使用全域變數存取

我們可以考慮存取子 API,公開對已剖析設定的單一全域例項的存取權。例如,在 Rust 中:

static CONFIG: Lazy<Config> = Lazy::new(|| /* ...retrieve from runner... */);

從資源經濟的角度來看,使用單一全域設定值執行個體很有吸引力,因為這會使用語言功能,確保一次只將元件設定的單一副本例項化到記憶體中。從概念上來說,這也反映了開發人員通常如何看待設定。

不過,許多語言都不建議使用全域/靜態變數 (除非必要),而選擇這種 API 樣式會強制開發人員使用這些變數,即使並非絕對必要。個別元件的開發人員仍可選擇將產生的存取子結果包裝在全域變數中。

此外,部分環境 (例如目前的驅動程式庫架構) 不建議或禁止使用具有隱含初始化的全域變數。產生的設定存取權樣式應盡可能讓不同環境的開發人員熟悉,而使用函式呼叫表示這些不同環境只會因需要額外參數而有所差異。

替代方法:以執行元件為準的驗證

如果是以 ELF 為基礎的元件,您可以在自訂標頭中加入設定檢查碼,並透過載入器服務的協助,由執行元件驗證。這樣一來,元件架構就能在執行元件的程式碼前,驗證編譯後的存取子是否與編碼值相符,並直接向記錄檔回報錯誤,而不必依賴元件正確設定輸出內容,再嘗試讀取設定。不過,我們預期使用 SDK 工具封裝元件時,很少會發生總和檢查碼不符的情況,而且在載入器和執行元件中設計及實作更快速失敗的總和檢查碼驗證步驟,需要花費更多心力。

在建立程序前驗證設定檢查碼的設計,需要處理單一二進位檔用於提供多個元件,但這些元件具有不同設定介面的情況。

我們預期無法驗證總和檢查碼的情況不會很常見,因此將錯誤從這些情況轉移到較早階段的價值有限。我們也提供額外選項,可將存取子程式庫中繼資料納入二進位檔,供產品組裝工具驗證,這樣一來,我們就能在元件開發生命週期的更早階段發現這些錯誤。由於實作前置程序建立驗證的複雜度高、預期改善體驗的價值低,且有機會透過平台外的工具提早發現這些錯誤,因此我們認為在取得新資訊前,不應繼續開發這項功能,以免改變我們對這些取捨的看法。

替代方案:修正檢查碼並將檢查支援項目納入 FIDL 功能

我們可以決定將結構化設定所需的型別雜湊和偵錯功能一般化,並提供給所有 FIDL 使用者。

從長遠來看,這個方法很有希望,因此 CF 和 FIDL 團隊將根據這項 RFC 和其他主題的討論結果,努力讓兩者的技術更緊密地結合。具體來說,我們會討論:

  • 為產生的 FIDL 程式碼設計私有或「元件本機」命名空間,這樣我們就能產生具有額外依附元件的程式碼,同時不影響一般 IPC 用途,還能解決命名和平台版本控管相關問題
  • FIDL 內的「lockstep versioning」選項,可提供工具來防止元件資訊清單和實作二進位檔之間發生偏差
  • 為 FIDL 型別產生額外的檢查偵錯程式碼

FIDL 方面需要一些時間才能支援這些概念,我們選擇不封鎖結構化設定存取子。我們預期未來的 RFC 會說明這些和其他 FIDL 的元件感知功能,並視需要將結構化設定使用者遷移至新的存取子 API。

替代方案:支援 FIDL 繫結和可重複使用的執行階段程式庫

我們可以修改 FIDL 後端,發出額外程式碼來支援結構化設定用途。這可以透過傳遞至後端的旗標 (例如 --enable-config-codegen),或透過傳遞至後端的自訂屬性 (例如 @structured_config_checksum()@structured_config_inspect()) 完成。

這樣我們就能發出檢查碼驗證和檢查偵錯的程式碼,然後從可重複使用的程式庫叫用該功能,這些程式庫會知道每個執行階段將編碼設定傳遞至元件的方法。

這個選項的優點是可減少我們執行的程式碼產生「變體」數量,因為程式碼產生器不必瞭解每個執行階段的設定傳遞方法,相關知識一律可存放在可重複使用的程式庫中。此外,由於現有系統已整合 FIDL 工具鍊,因此也能減少將結構化設定與 OOT 建構系統整合的作業。

不過,這個選項有重大缺點。FIDL 工具鍊目前無法將繫結中的功能標示為不穩定或實驗性。繫結發出的所有程式碼都有相同的穩定性需求,也就是說,只要將結構化設定屬性新增至 SDK 可用的 C++ 後端,所有 SDK 客戶都能立即使用。這與我們逐步推出結構化設定的目標相衝突,因為我們希望在持續向使用者學習的同時,盡可能保留修改 API 的能力。

此外,目前生成的繫結本身是用來實作這些高階概念,因此 FIDL 工具鍊是否應生成繫結層,並依附於 Inspect 等高階 Fuchsia 概念,目前並不清楚。

替代做法:設定的 fidlgen 後端

我們可以定義個別後端,用於產生結構化設定支援程式碼。這可讓我們達成與目前 fidlgen 後端不同的穩定性屬性,並在產生的存取子中加入其他依附元件。

不過,為避免向使用者公開多個產生的命名空間,我們需要在單一命名空間中發出設定支援程式碼和 FIDL 網域物件。產生的這個程式庫無法連結至與相同程式庫的基本/非設定 FIDL 繫結相同的二進位檔。在實務上,我們不會預期使用者會嘗試為特定程式庫產生「config-aware」和「basic」FIDL 繫結,但這會對 FIDL 程式碼產生器造成新的限制,以防範或規避該用途。我們認為,在更廣泛地協調 CF 和 FIDL 技術之前,這項解決方案只是暫時的。我們偏好承擔技術債,以獲得更大的設計和實作自由,同時不會為 FIDL 團隊帶來潛在的維護危害。

替代方案:透過額外層公開 FIDL 程式庫

我們可以產生「基礎」FIDL 繫結,其內容與目前所有繫結相同,然後產生額外的「層」的設定支援程式碼,該程式碼知道如何從元件的執行階段擷取該 FIDL 型別。這樣做可達成我們的實作目標,也就是清楚區隔 FIDL IPC 和其他問題,但會讓使用者公開到兩個產生的命名空間,卻不會帶來任何額外好處。

替代方案:產生不含 FIDL 依附元件的程式庫

我們可以考慮完全不依附 FIDL,並自行產生剖析器。如果我們發現無法讓使用者免於存取器程式庫產生的 FIDL 依附元件影響,這個方法會更具吸引力。

既有技術和參考資料

這項工作在許多方面都與 FIDL 類似,瞭解如何存取 Fuchsia 的二進位格式,對這份 RFC 的讀者很有幫助。

Fuchsia 有許多手寫存取子的設定範例,例如fshost的設定類別,該類別用於剖析自訂的分行符號分隔格式,然後切換至結構化設定的早期原型。

許多語言專屬程式庫都提供動態「設定介面」的「類型剖析器」,例如 argv 或 JSON 設定檔。在 fuchsia.git 中,argh 是用於剖析 argv 的熱門 Rust 程式庫,而 serde/serde_json 則經常以類似方式用於剖析啟動檔案系統和套件中的設定檔。