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,以便在執行階段覆寫元件的設定值。從作者的角度來看,剖析的類型不會描述可能發生的任何覆寫。產生的程式庫不會負責處理覆寫值。

可偵錯

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

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

Ergonomic

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

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

熟悉

針對設定結構描述產生的類型,應讓 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 中建構存取子支援功能時,我們會確保使用者可以設定包含目錄版面配置,以符合他們的任何樣式指南。

荒漠油廠

#[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 {}
}

請注意,這個範例使用異步和 VFS 來示範檢查服務,但執行緒和 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 使用者。

這項做法在長期而言相當有潛力,而隨著我們針對此 RFC 和其他主題進行討論,CF 和 FIDL 團隊將致力於讓各自的技術更緊密地配合。具體而言,我們將討論以下內容:

  • 為產生的 FIDL 程式碼設計私人或「元件區域」命名空間,讓我們能夠產生具有額外依附元件的程式碼,而不影響一般 IPC 用途,並解決命名和平台版本相關問題
  • FIDL 中的「鎖定版本」選項,可提供工具來防止元件資訊清單和實作二進位檔之間出現偏差
  • 為 FIDL 類型產生其他 Inspect 偵錯程式碼

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

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

我們可以修改 FIDL 後端,以便產生其他程式碼,支援結構化設定用途。您可以透過傳遞至後端的標記 (例如 --enable-config-codegen),或透過傳遞至後端的自訂屬性 (例如 @structured_config_checksum()@structured_config_inspect()) 來執行這項操作。

這可讓我們產生用於檢查和偵錯的總和檢查碼,然後從可重複使用的程式庫中叫用該功能,這些程式庫可辨識各個執行階段將經過編碼的設定傳遞至元件的做法。

這個選項的好處是可減少我們執行的 codegen 的「變種」數量,因為程式碼產生器不必瞭解每個執行階段的提供設定方法,這些知識一律可在可重複使用的程式庫中找到。這項工具的另一個優點是,可減少將結構化設定與 OOT 建構系統整合的作業量,因為現有的系統已整合 FIDL 工具鍊。

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

此外,當目前產生的繫結本身用於實作較高層級的 Fuchsia 概念 (例如 Inspect) 時,FIDL 工具鍊是否應產生依附於較高層級 Fuchsia 概念的繫結層,目前尚未明確。

替代做法:設定 fidlgen 後端

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

不過,為避免使用者看到多個產生的命名空間,我們需要在單一命名空間中同時發出設定支援程式碼和 FIDL 網域物件。這個產生的程式庫無法連結至與相同程式庫的基本/非設定 FIDL 繫結相同的二進位檔。實際上,我們不會預期使用者會嘗試為特定程式庫產生「瞭解設定」和「基本」FIDL 繫結,但這會在 FIDL 程式碼產生器上建立新的限制,以防範或因應該用途。我們認為,在我們進一步整合 CF 和 FIDL 技術之前,這項解決方案只是暫時性的。我們傾向於承擔技術債務,以便提供更大的設計和實作彈性,同時不會為 FidL 團隊帶來潛在的維護風險。

替代做法:透過額外層公開 FIDL 程式庫

我們可以產生「基本」FIDL 繫結,其內容與目前的所有繫結相同,然後產生額外的「層級」設定支援程式碼,瞭解如何從元件的執行階段擷取該 FIDL 類型。這麼做可達成我們的實作目標,也就是在 FIDL IPC 和其他關注點之間建立明確的區隔,但會讓使用者公開到兩個產生的命名空間,卻沒有提供任何額外的好處。

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

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

既有技術與參考資料

這項工作在許多方面都與 FIDL 相似,因此閱讀這份 RFC 時,如果能瞭解如何存取 Fuchsia 的二進位格式,將有助於您掌握相關資訊。

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

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