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

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

面向使用者的設定存取子整體理念和需求。

問題
  • 110
變更
作者
審查人員
提交日期 (年/月)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 開發人員熟悉的類型介面,以及盡可能重複使用現有工具。

無所不在

元件作者必須能夠假設這些值隨時都有可用,而且所有欄位均已填入值。設定欄位的「是否可為空值」應與輸入宣告相符 (設定結構定義不支援寫入可為空值欄位時),而且存取子函式應無法理解,即不會傳回錯誤或擲回例外狀況。如果存取子收到無法剖析的設定酬載,應快速執行失敗並終止元件執行作業 (例如 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 中建構存取子支援時,我們會確保使用者能夠根據自身的樣式設定,設定包含目錄版面配置。

Rust

#[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++ 驅動程式擁有的存取子程式庫與 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 程式碼設計私有或「component-local」命名空間,這可讓我們產生包含其他依附元件的程式碼,而不影響一般處理序間通訊 (IPC) 用途,還可解決關於命名和平台版本管理的問題
  • FIDL 內用於「鎖定版本管理」的選項,提供工具防止元件資訊清單與實作二進位檔之間出現衝突
  • 產生 FIDL 類型的額外檢查偵錯程式碼

在 FIDL 端支援這些概念所需的工作需要一些時間,因此我們決定不封鎖結構化設定存取子。我們預期未來的 RFC 將說明上述 FIDL 的其他元件感知功能,並在必要時將結構化設定使用者遷移至新的存取子 API。

其他做法:支援 FIDL 繫結和可重複使用的執行階段程式庫

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

這麼做可讓我們產生檢查碼驗證和檢查偵錯的程式碼,接著從可重複使用的程式庫叫用該項功能,而這些程式庫會知道每個執行階段的方法,將編碼的設定傳遞至元件。

這個選項的優勢在於減少我們現有的程式碼生成「變種版本」數量,因為不需要程式碼產生器就必須瞭解每個執行階段的提供設定方法,這些知識隨時都保存在可重複使用的程式庫中。此外,由於現有容器已經整合 FIDL 工具鍊,因此可減少整合結構化設定與 OOT 建構系統的負擔。

不過,這個選項有一個缺點。FIDL 工具鍊目前無法將繫結中的功能標示為不穩定或處於實驗階段。繫結產生的所有程式碼都有相同的穩定性需求,這表示所有 SDK 客戶都能立即將結構化設定屬性新增至 SDK 可用的 C++ 後端。這與我們逐步推行結構化設定的目標牴觸,不僅能夠盡可能保留修改 API 的功能,又將持續從使用者身上學習,這個問題就會迎刃而解。

此外,FIDL 工具鍊也不確定能否產生繫結層,而這些繫結會仰賴較高層級的 Fuchsia 概念,例如「檢查」目前產生的繫結本身用於實作這些較高層級的概念。

替代做法:用於設定的 fidlgen 後端

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

不過,為了避免將使用者暴露至多個產生的命名空間,我們需要在單一命名空間中同時發出設定支援代碼和 FIDL 網域物件。產生的程式庫無法連結至同一個程式庫的基本/非設定 FIDL 繫結所在的二進位檔。實際上,我們預期使用者不會嘗試針對特定程式庫產生「設定感知」和「基本」FIDL 繫結,但會在 FIDL 轉碼器上建立新限制來防範該用途或設計相關用途。在我們更廣泛地協調 CF 和 FIDL 技術之前,我們認為這項解決方案只是暫時的。我們傾向於技術債上提供更大的設計和實作自由,而不為 FIDL 團隊造成潛在的維護危害。

替代做法:公開含有額外的層的 FIDL 程式庫

我們可以產生「基本」FIDL 繫結,其內容與目前所有繫結都相同,然後產生額外的設定支援程式碼「層」,知道如何從元件執行階段擷取該 FIDL 類型。這有助於實現 FIDL IPC 與其他問題之間乾淨分離的實作目標,但也可以讓使用者接觸到兩個已產生的命名空間,而無需讓他們享有任何其他好處。

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

我們可以考慮沒有 FIDL 依附元件,並產生自己的剖析器。如果我們發現我們無法從存取者程式庫產生的 FIDL 依附元件將使用者隔離,這種方法會更具吸引力。

先前的圖片和參考資料

此處的工作在許多方面與 FIDL 類似,而且只要瞭解存取 Fuchsia 二進位檔格式,就能瞭解這個 RFC 的讀取者。

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

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