| RFC-0061:可擴充的聯集 | |
|---|---|
| 狀態 | 已接受 |
| 區域 |
|
| 說明 | 為了提供更多方式來表示酬載,其形狀可能需要隨著時間演進,我們建議以可擴充的聯集取代現有的聯集。 |
| 作者 | |
| 提交日期 (年-月-日) | 2018-09-26 |
| 審查日期 (年-月-日) | 2018-10-11 |
「Catering to Hawaii and Alaska」(為夏威夷和阿拉斯加提供服務)
摘要
為了提供更多方式來表示可能需要隨時間演變的酬載形狀,我們建議以可擴充的聯集取代現有的聯集。
提振精神
目前,聯集無法隨時間演進,我們甚至警告「一般來說,變更聯集的定義會破壞二進位檔的相容性」。
目前定義了許多需要擴充性的聯集,例如 fuchsia.modular/TriggerCondition,其中欄位已淘汰但未移除,或是 fuchsia.modular/Interaction。
如後續章節所述,許多聯集目前的表示方式也相當合適,因為近期不太可能演變。不過,同時保留 static unions 和 extensible 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 個位元組;- 儲存封包位元組數的
uint32num_bytes,一律為 8 的倍數,且封包為空值時必須為 0; uint32num_handles,用於儲存封裝中的控制代碼數量,如果封裝為空值,則必須為 0;uint64資料指標,指出是否有行外資料:0信封為空值時;- 如果存在封包,則為 FIDL_ALLOC_PRESENT (或 UINTPTR_MAX),以及下一個非行內物件;
- 解碼供取用時,這個 data 指標不是 nullptr (如果封包為空值),就是封包的有效指標。
- 信封會為控點預留儲存空間,緊接在內容之後。
可為空的可擴充聯集的標記為 0,num_bytes 設為 0, num_handles 設為 0,且資料指標為 FIDL_ALLOC_ABSENT, 也就是 0。 基本上,可擴充的空值聯集是 24 個位元組的 0。
語言繫結
可擴充的聯集與聯集類似,但讀取聯集時,也需要處理「不明」案例。理想情況下,大部分的語言繫結都會將
union Name { Type1 field1; ...; TypeN fieldN; };
因為它們會像可擴充的聯集一樣,讓程式碼可以輕鬆從一個切換到另一個,但未知案例的支援除外,這只在可擴充的聯集案例中有意義。
首先,我們建議不要公開保留成員的語言繫結:雖然這些成員會出現在 JSON IR 中,但我們認為在語言繫結中公開這些成員並無用處。
導入策略
導入程序分為兩個步驟。
首先,我們會建構可擴充聯集支援:
- 以語言 (
fidlc) 介紹這項功能,並使用不同的關鍵字 (xunion) 區分靜態聯集和可擴充聯集。 - 實作各種核心語言繫結 (C、C++、Rust、Go、Dart)。視情況延長相容性測試和其他測試。
其次,我們會將所有靜態聯集遷移至可擴充聯集:
為靜態聯集產生序數,並將其放在 JSON IR 中。後端應先忽略這些序數。
在讀取路徑上,同時擁有讀取聯集模式,就像是靜態聯集,也像是可擴充的聯集 (需要序數才能實現)。根據交易訊息標頭中的旗標,選擇其中一個。
更新寫入路徑,將聯集編碼為可擴充的聯集,並在交易訊息標頭中設定標記,盡可能指出這點。
所有寫入器更新、部署及傳播完畢後,請移除靜態聯集處理作業,以及軟轉換的架構程式碼。
說明文件和範例
這至少需要在下列位置提供文件:
回溯相容性
可擴充聯集不會與「靜態」聯集回溯相容。
效能
未使用時不會影響效能。 建構期間的效能影響微乎其微。
安全性
不會影響安全性。
測試
編譯器中的單元測試、各種語言繫結中的編碼/解碼單元測試,以及檢查各種語言繫結的相容性測試。
缺點、替代方案和未知事項
可擴充聯集比不可擴充聯集效率低。 此外,非可擴充的聯集無法透過語言中的其他方式表示。因此,我們建議這兩項功能並存。
不過,我們可能會決定只保留可擴充的聯集,並捨棄目前定義的聯集。這會違反 Fuchsia 中多個位置的規定,因為這些位置的聯集代表效能關鍵訊息,且擴充期望不高,例如 fuchsia.io/NodeInfo、fuchsia.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 可擴充聯集 (例如空結構體),可清楚模擬意圖。這也能避免可擴充聯集出現兩個「單位」值,也就是空值和空白值。