RFC-0061:可擴充聯集

RFC-0061:可擴充的聯集
狀態已接受
區域
  • FIDL
說明

為了提供更多方式來表示酬載,其形狀可能需要隨著時間演進,我們建議以可擴充的聯集取代現有的聯集。

作者
提交日期 (年-月-日)2018-09-26
審查日期 (年-月-日)2018-10-11

「Catering to Hawaii and Alaska」(為夏威夷和阿拉斯加提供服務)

摘要

為了提供更多方式來表示可能需要隨時間演變的酬載形狀,我們建議以可擴充的聯集取代現有的聯集

提振精神

目前,聯集無法隨時間演進,我們甚至警告「一般來說,變更聯集的定義會破壞二進位檔的相容性」。

目前定義了許多需要擴充性的聯集,例如 fuchsia.modular/TriggerCondition,其中欄位已淘汰但未移除,或是 fuchsia.modular/Interaction

後續章節所述,許多聯集目前的表示方式也相當合適,因為近期不太可能演變。不過,同時保留 static unionsextensible unions 會造成不必要的複雜性,請參閱優缺點

設計

如要導入可擴充的聯集,我們需要修改 FIDL 的多個部分:語言和 fidlc、JSON IR、連線格式和所有語言繫結。我們也需要在各處記錄這項新功能。我們會逐一討論各項變更。

語言

在語法上,可擴充聯集與靜態聯集完全相同:

union MyExtensibleUnion {
    Type1 field1;
    Type2 field2;
     ...
    TypeN fieldN;
}

在幕後,系統會為每個欄位指派序數,這與表格為每個欄位指派序數,以及方法序數自動指派序數的方式類似。

具體情形如下:

  • 序數是使用與方法序數相同的演算法計算 (詳細資料),我們會串連程式庫名稱、「.」、可擴充的聯集名稱、「/」和成員名稱,然後採用 SHA256,並以 0x7fffffff 遮蓋。
  • 序數為 uint32兩個欄位不得聲明相同的序數,且我們不允許 0。如果序數發生衝突,應使用 [Selector] 屬性提供替代名稱 (或重新命名成員)。
  • 序數可能稀疏,也就是說,序數與資料表不同,資料表需要密集序數。
  • 可擴充聯集「不得使用可為空值的欄位」
  • 可擴充聯集必須至少有一個成員

在語言中,目前可使用聯集的位置,都可以使用可擴充的聯集。特別是:

  • 結構體、表格和可擴充的聯集可以包含可擴充的聯集;
  • 可擴充的聯集可包含結構體、表格和可擴充的聯集;
  • 介面引數或回傳值可以是可擴充的聯集;
  • 可擴充的聯集可為可為空值。

JSON IR

在下方的表格中,我們會在每個聯集欄位宣告中新增一個鍵「ordinal」。

Wire 格式

在連線上,可擴充的聯集會以序數表示,用於區分選項 (填補至 8 個位元組),後面接著生產者已知的各種成員的封包。具體來說,就是:

  • 包含要編碼成員序數的 uint32 標記
  • uint32 邊框間距,可對齊 8 個位元組;
  • 儲存封包位元組數的 uint32 num_bytes,一律為 8 的倍數,且封包為空值時必須為 0;
  • uint32 num_handles,用於儲存封裝中的控制代碼數量,如果封裝為空值,則必須為 0;
  • uint64 資料指標,指出是否有行外資料:
    • 0 信封為空值時;
    • 如果存在封包,則為 FIDL_ALLOC_PRESENT (或 UINTPTR_MAX),以及下一個非行內物件;
  • 解碼供取用時,這個 data 指標不是 nullptr (如果封包為空值),就是封包的有效指標
  • 信封會為控點預留儲存空間,緊接在內容之後。

可為空的可擴充聯集標記為 0num_bytes 設為 0num_handles 設為 0,且資料指標為 FIDL_ALLOC_ABSENT, 也就是 0。 基本上,可擴充的空值聯集是 24 個位元組的 0。

語言繫結

可擴充的聯集與聯集類似,但讀取聯集時,也需要處理「不明」案例。理想情況下,大部分的語言繫結都會將

union Name { Type1 field1; ...; TypeN fieldN; };

因為它們會像可擴充的聯集一樣,讓程式碼可以輕鬆從一個切換到另一個,但未知案例的支援除外,這只在可擴充的聯集案例中有意義。

首先,我們建議不要公開保留成員的語言繫結:雖然這些成員會出現在 JSON IR 中,但我們認為在語言繫結中公開這些成員並無用處。

導入策略

導入程序分為兩個步驟。

首先,我們會建構可擴充聯集支援:

  1. 以語言 (fidlc) 介紹這項功能,並使用不同的關鍵字 (xunion) 區分靜態聯集和可擴充聯集。
  2. 實作各種核心語言繫結 (C、C++、Rust、Go、Dart)。視情況延長相容性測試和其他測試。

其次,我們會將所有靜態聯集遷移至可擴充聯集:

  1. 為靜態聯集產生序數,並將其放在 JSON IR 中。後端應先忽略這些序數。

  2. 在讀取路徑上,同時擁有讀取聯集模式,就像是靜態聯集,也像是可擴充的聯集 (需要序數才能實現)。根據交易訊息標頭中的旗標,選擇其中一個。

  3. 更新寫入路徑,將聯集編碼為可擴充的聯集,並在交易訊息標頭中設定標記,盡可能指出這點。

  4. 所有寫入器更新、部署及傳播完畢後,請移除靜態聯集處理作業,以及軟轉換的架構程式碼。

說明文件和範例

這至少需要在下列位置提供文件:

回溯相容性

可擴充聯集會與「靜態」聯集回溯相容。

效能

未使用時不會影響效能。 建構期間的效能影響微乎其微。

安全性

不會影響安全性。

測試

編譯器中的單元測試、各種語言繫結中的編碼/解碼單元測試,以及檢查各種語言繫結的相容性測試。

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

可擴充聯集比不可擴充聯集效率低。 此外,非可擴充的聯集無法透過語言中的其他方式表示。因此,我們建議這兩項功能並存。

不過,我們可能會決定只保留可擴充的聯集,並捨棄目前定義的聯集。這會違反 Fuchsia 中多個位置的規定,因為這些位置的聯集代表效能關鍵訊息,且擴充期望不高,例如 fuchsia.io/NodeInfofuchsia.net/IpAddress

保留靜態聯集的優缺點

優點

  • 相較於聯集,可擴充聯集會產生 8 位元組的費用 (適用於信封大小和控點數量)。此外,可擴充聯集資料一律會以行外方式儲存 (也就是資料指標額外需要 8 個位元組),而只有可為空值的聯集資料會以行外方式儲存。
  • 由於聯集編碼,無法在 FIDL 中以其他基本型別表示。因此,如果從語言中移除這些字詞,某些類別的訊息就無法再以簡潔有效的方式表達。
  • 在某些情況下,視其用途而定,聯集可以有效率但不同的方式表示;不過,這是例外情況,而非常態。舉例來說,fuchsia.net.stack/InterfaceAddressChangeEvent 只會用於 fuchsia.net.stack/InterfaceAddressChange,因此可直接寫入 InterfaceAddress,並使用 enum 指出是否新增或移除。

缺點

  • 同時保留靜態聯集和可擴充聯集,會導致編譯器、JSON IR、所有後端,以及編碼/解碼的複雜度增加。收穫甚微:在 FIDL 編碼本身並非特別節省空間的世界中,大小差異微不足道。此外,如有需要,可就地解碼可擴充的聯集。
  • 以下是 fuchsia.io/NodeInfo 的分析結果,可做為收益極少的範例:
    • 目前 NodeInfo 有 6 個選項:服務 (大小 1)、檔案 (大小 4)、目錄 (大小 1)、管道 (大小 4)、vmofile (大小 24)、裝置 (大小 4)。
    • 因此,NodeInfo 的總大小一律為 32 個位元組,也就是 標記 + max(選項大小) = 8 + 24 = 32。
    • 使用可擴充的聯集時,NodeInfo 的大小取決於編碼的選項。一律會有 16 個位元組的「稅項」(相較於 8 個位元組),因此相應的大小會是:服務 = 24、檔案 = 24、目錄 = 24、管道 = 24、vmofile = 40、裝置 = 24。
    • 因此,除了 vmofile 之外,我們在所有情況下都會減少 8 個位元組,而 vmofile 則會增加 8 個位元組。
  • 同時擁有靜態聯集和可擴充聯集,語言的複雜度也令人擔憂。我們預期程式庫作者會猶豫要使用哪一個,但從長遠來看,選擇可擴充的聯集是較安全的做法,而且成本很低。

總而言之,我們決定以可擴充的聯集取代靜態聯集。

標記與序數

我們會使用「序數」表示指派給欄位的內部數值,也就是透過雜湊計算出的值。我們使用 標記表示繫結中的變體:在 Go 中,這可能是 alias 類型的常數;在 Dart 中,這可能是 enum

fidlc 編譯器只會處理序數。 開發人員最有可能只處理標記。繫結則提供從高階標記到低階內部序數的轉換。

No Empty Extensible Unions

在設計階段,我們考慮讓可擴充的聯集為空白。 不過,我們最後選擇禁止這種做法:選擇具有單一變體的 Nullable 可擴充聯集 (例如空結構體),可清楚模擬意圖。這也能避免可擴充聯集出現兩個「單位」值,也就是空值和空白值。

既有技術和參考資料

  • 通訊協定緩衝區具有 oneof
  • 除非是特殊情況,否則 FlatBuffers 的聯集無法擴充。